Browse Source

test(elasticsearch-plugin): Get tests passing

Michael Bromley 4 years ago
parent
commit
152b3f0704

+ 65 - 59
packages/elasticsearch-plugin/e2e/e2e-helpers.ts

@@ -126,14 +126,9 @@ export async function testMatchFacetValueFiltersAnd(client: SimpleGraphQLClient)
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual([
-        'Laptop',
-        'Curvy Monitor',
-        'Gaming PC',
-        'Hard Drive',
-        'Clacky Keyboard',
-        'USB Cable',
-    ].sort());
+    expect(result.search.items.map(i => i.productName).sort()).toEqual(
+        ['Laptop', 'Curvy Monitor', 'Gaming PC', 'Hard Drive', 'Clacky Keyboard', 'USB Cable'].sort(),
+    );
 }
 
 export async function testMatchFacetValueFiltersOr(client: SimpleGraphQLClient) {
@@ -142,7 +137,7 @@ export async function testMatchFacetValueFiltersOr(client: SimpleGraphQLClient)
         {
             input: {
                 groupByProduct: true,
-                facetValueFilters: [ { or: ['T_1', 'T_5'] }],
+                facetValueFilters: [{ or: ['T_1', 'T_5'] }],
                 sort: {
                     name: SortOrder.ASC,
                 },
@@ -150,21 +145,23 @@ export async function testMatchFacetValueFiltersOr(client: SimpleGraphQLClient)
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual([
-        'Bonsai Tree',
-        'Camera Lens',
-        'Clacky Keyboard',
-        'Curvy Monitor',
-        'Gaming PC',
-        'Hard Drive',
-        'Instant Camera',
-        'Laptop',
-        'Orchid',
-        'SLR Camera',
-        'Spiky Cactus',
-        'Tripod',
-        'USB Cable',
-    ].sort());
+    expect(result.search.items.map(i => i.productName).sort()).toEqual(
+        [
+            'Bonsai Tree',
+            'Camera Lens',
+            'Clacky Keyboard',
+            'Curvy Monitor',
+            'Gaming PC',
+            'Hard Drive',
+            'Instant Camera',
+            'Laptop',
+            'Orchid',
+            'SLR Camera',
+            'Spiky Cactus',
+            'Tripod',
+            'USB Cable',
+        ].sort(),
+    );
 }
 
 export async function testMatchFacetValueFiltersOrWithAnd(client: SimpleGraphQLClient) {
@@ -173,22 +170,24 @@ export async function testMatchFacetValueFiltersOrWithAnd(client: SimpleGraphQLC
         {
             input: {
                 groupByProduct: true,
-                facetValueFilters: [{and: 'T_1'}, { or: ['T_2', 'T_3'] }],
+                facetValueFilters: [{ and: 'T_1' }, { or: ['T_2', 'T_3'] }],
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual([
-        'Laptop',
-        'Curvy Monitor',
-        'Gaming PC',
-        'Hard Drive',
-        'Clacky Keyboard',
-        'USB Cable',
-        'Instant Camera',
-        'Camera Lens',
-        'Tripod',
-        'SLR Camera'
-    ].sort());
+    expect(result.search.items.map(i => i.productName).sort()).toEqual(
+        [
+            'Laptop',
+            'Curvy Monitor',
+            'Gaming PC',
+            'Hard Drive',
+            'Clacky Keyboard',
+            'USB Cable',
+            'Instant Camera',
+            'Camera Lens',
+            'Tripod',
+            'SLR Camera',
+        ].sort(),
+    );
 }
 
 export async function testMatchFacetValueFiltersWithFacetIdsOr(client: SimpleGraphQLClient) {
@@ -198,23 +197,25 @@ export async function testMatchFacetValueFiltersWithFacetIdsOr(client: SimpleGra
             input: {
                 facetValueIds: ['T_2', 'T_3'],
                 facetValueOperator: LogicalOperator.OR,
-                facetValueFilters: [{and:'T_1'}],
+                facetValueFilters: [{ and: 'T_1' }],
                 groupByProduct: true,
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual([
-        'Laptop',
-        'Curvy Monitor',
-        'Gaming PC',
-        'Hard Drive',
-        'Clacky Keyboard',
-        'USB Cable',
-        'Instant Camera',
-        'Camera Lens',
-        'Tripod',
-        'SLR Camera',
-    ].sort());
+    expect(result.search.items.map(i => i.productName).sort()).toEqual(
+        [
+            'Laptop',
+            'Curvy Monitor',
+            'Gaming PC',
+            'Hard Drive',
+            'Clacky Keyboard',
+            'USB Cable',
+            'Instant Camera',
+            'Camera Lens',
+            'Tripod',
+            'SLR Camera',
+        ].sort(),
+    );
 }
 
 export async function testMatchFacetValueFiltersWithFacetIdsAnd(client: SimpleGraphQLClient) {
@@ -223,18 +224,15 @@ export async function testMatchFacetValueFiltersWithFacetIdsAnd(client: SimpleGr
         {
             input: {
                 facetValueIds: ['T_1'],
-                facetValueFilters: [{and:'T_3'}],
+                facetValueFilters: [{ and: 'T_3' }],
                 facetValueOperator: LogicalOperator.AND,
                 groupByProduct: true,
             },
         },
     );
-    expect(result.search.items.map(i => i.productName).sort()).toEqual([
-        'Instant Camera',
-        'Camera Lens',
-        'Tripod',
-        'SLR Camera'
-    ].sort());
+    expect(result.search.items.map(i => i.productName).sort()).toEqual(
+        ['Instant Camera', 'Camera Lens', 'Tripod', 'SLR Camera'].sort(),
+    );
 }
 
 export async function testMatchCollectionId(client: SimpleGraphQLClient) {
@@ -247,7 +245,11 @@ export async function testMatchCollectionId(client: SimpleGraphQLClient) {
             },
         },
     );
-    expect(result.search.items.map(i => i.productName)).toEqual(['Spiky Cactus', 'Orchid', 'Bonsai Tree']);
+    expect(result.search.items.map(i => i.productName).sort()).toEqual([
+        'Bonsai Tree',
+        'Orchid',
+        'Spiky Cactus',
+    ]);
 }
 
 export async function testMatchCollectionSlug(client: SimpleGraphQLClient) {
@@ -260,7 +262,11 @@ export async function testMatchCollectionSlug(client: SimpleGraphQLClient) {
             },
         },
     );
-    expect(result.search.items.map(i => i.productName)).toEqual(['Spiky Cactus', 'Orchid', 'Bonsai Tree']);
+    expect(result.search.items.map(i => i.productName).sort()).toEqual([
+        'Bonsai Tree',
+        'Orchid',
+        'Spiky Cactus',
+    ]);
 }
 
 export async function testSinglePrices(client: SimpleGraphQLClient) {

+ 27 - 20
packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts

@@ -169,9 +169,11 @@ describe('Elasticsearch plugin', () => {
 
         it('matches by FacetValueFilters OR and AND', () => testMatchFacetValueFiltersOrWithAnd(shopClient));
 
-        it('matches by FacetValueFilters with facetId OR operator', () => testMatchFacetValueFiltersWithFacetIdsOr(shopClient));
+        it('matches by FacetValueFilters with facetId OR operator', () =>
+            testMatchFacetValueFiltersWithFacetIdsOr(shopClient));
 
-        it('matches by FacetValueFilters with facetId AND operator', () => testMatchFacetValueFiltersWithFacetIdsAnd(shopClient));
+        it('matches by FacetValueFilters with facetId AND operator', () =>
+            testMatchFacetValueFiltersWithFacetIdsAnd(shopClient));
 
         it('matches by collectionId', () => testMatchCollectionId(shopClient));
 
@@ -269,6 +271,7 @@ describe('Elasticsearch plugin', () => {
                 SEARCH_PRODUCTS_SHOP,
                 {
                     input: {
+                        term: 'Laptop 13 inch 8GB',
                         groupByProduct: false,
                         take: 1,
                     },
@@ -293,11 +296,11 @@ describe('Elasticsearch plugin', () => {
                 {
                     input: {
                         groupByProduct: false,
-                        take: 3,
+                        take: 100,
                     },
                 },
             );
-            expect(result.search.items.map(i => i.productVariantId)).toEqual(['T_1', 'T_2', 'T_4']);
+            expect(result.search.items.map(i => i.productVariantId).includes('T_3')).toBe(false);
         });
 
         it('encodes collectionIds', async () => {
@@ -333,9 +336,11 @@ describe('Elasticsearch plugin', () => {
 
         it('matches by FacetValueFilters OR and AND', () => testMatchFacetValueFiltersOrWithAnd(shopClient));
 
-        it('matches by FacetValueFilters with facetId OR operator', () => testMatchFacetValueFiltersWithFacetIdsOr(shopClient));
+        it('matches by FacetValueFilters with facetId OR operator', () =>
+            testMatchFacetValueFiltersWithFacetIdsOr(shopClient));
 
-        it('matches by FacetValueFilters with facetId AND operator', () => testMatchFacetValueFiltersWithFacetIdsAnd(shopClient));
+        it('matches by FacetValueFilters with facetId AND operator', () =>
+            testMatchFacetValueFiltersWithFacetIdsAnd(shopClient));
 
         it('matches by collectionId', () => testMatchCollectionId(adminClient));
 
@@ -488,14 +493,14 @@ describe('Elasticsearch plugin', () => {
                     groupByProduct: true,
                 });
 
-                expect(result1.search.items.map(i => i.productName)).toEqual([
-                    'Road Bike',
-                    'Skipping Rope',
+                expect(result1.search.items.map(i => i.productName).sort()).toEqual([
                     'Boxing Gloves',
-                    'Tent',
                     'Cruiser Skateboard',
                     'Football',
+                    'Road Bike',
                     'Running Shoe',
+                    'Skipping Rope',
+                    'Tent',
                 ]);
 
                 const result2 = await doAdminSearchQuery(adminClient, {
@@ -503,14 +508,14 @@ describe('Elasticsearch plugin', () => {
                     groupByProduct: true,
                 });
 
-                expect(result2.search.items.map(i => i.productName)).toEqual([
-                    'Road Bike',
-                    'Skipping Rope',
+                expect(result2.search.items.map(i => i.productName).sort()).toEqual([
                     'Boxing Gloves',
-                    'Tent',
                     'Cruiser Skateboard',
                     'Football',
+                    'Road Bike',
                     'Running Shoe',
+                    'Skipping Rope',
+                    'Tent',
                 ]);
             });
 
@@ -552,11 +557,11 @@ describe('Elasticsearch plugin', () => {
                     collectionId: createCollection.id,
                     groupByProduct: true,
                 });
-                expect(result.search.items.map(i => i.productName)).toEqual([
-                    'Instant Camera',
+                expect(result.search.items.map(i => i.productName).sort()).toEqual([
                     'Camera Lens',
-                    'Tripod',
+                    'Instant Camera',
                     'SLR Camera',
+                    'Tripod',
                 ]);
             });
 
@@ -742,11 +747,13 @@ describe('Elasticsearch plugin', () => {
                 await adminClient.query<Reindex.Mutation>(REINDEX);
 
                 await awaitRunningJobs(adminClient);
-                const result = await doAdminSearchQuery(adminClient, { groupByProduct: true, take: 3 });
+                const result = await doAdminSearchQuery(adminClient, {
+                    term: 'laptop',
+                    groupByProduct: true,
+                    take: 3,
+                });
                 expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
                     { productId: 'T_1', enabled: false },
-                    { productId: 'T_2', enabled: true },
-                    { productId: 'T_3', enabled: false },
                 ]);
             });
         });

+ 4 - 7
packages/elasticsearch-plugin/src/elasticsearch-resolver.ts

@@ -6,8 +6,7 @@ import {
     SearchResponse,
 } from '@vendure/common/lib/generated-types';
 import { Omit } from '@vendure/common/lib/omit';
-import { Allow, Ctx, FacetValue, FacetValueService, RequestContext, SearchResolver } from '@vendure/core';
-import { countBy, uniq } from 'lodash';
+import { Allow, Ctx, FacetValue, RequestContext, SearchResolver } from '@vendure/core';
 
 import { ElasticsearchService } from './elasticsearch.service';
 import { ElasticSearchInput, SearchPriceData } from './types';
@@ -62,16 +61,14 @@ export class AdminElasticSearchResolver implements Omit<SearchResolver, 'facetVa
 
 @Resolver('SearchResponse')
 export class EntityElasticSearchResolver implements Pick<SearchResolver, 'facetValues'> {
-    constructor(private facetValueService: FacetValueService) {}
+    constructor(private elasticsearchService: ElasticsearchService) {}
 
     @ResolveField()
     async facetValues(
         @Ctx() ctx: RequestContext,
         @Parent() parent: Omit<SearchResponse, 'facetValues'>,
     ): Promise<Array<{ facetValue: FacetValue; count: number }>> {
-        const facetValueIds = parent.items.map(item => item.facetValueIds).flat();
-        const facetValueCounts = countBy(facetValueIds);
-        const facetValues = await this.facetValueService.findByIds(ctx, uniq(facetValueIds));
-        return facetValues.map(facetValue => ({ facetValue, count: facetValueCounts[facetValue.id] }));
+        const facetValues = await this.elasticsearchService.facetValues(ctx, (parent as any).input, true);
+        return facetValues.filter(i => !i.facetValue.facet.isPrivate);
     }
 }

+ 56 - 0
packages/elasticsearch-plugin/src/elasticsearch.service.ts

@@ -4,6 +4,8 @@ import { SearchResult, SearchResultAsset } from '@vendure/common/lib/generated-t
 import {
     ConfigService,
     DeepRequired,
+    FacetValue,
+    FacetValueService,
     InternalServerError,
     Job,
     Logger,
@@ -36,6 +38,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         private searchService: SearchService,
         private elasticsearchIndexService: ElasticsearchIndexService,
         private configService: ConfigService,
+        private facetValueService: FacetValueService,
     ) {
         searchService.adopt(this);
     }
@@ -152,6 +155,59 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         }
     }
 
+    /**
+     * Return a list of all FacetValues which appear in the result set.
+     */
+    async facetValues(
+        ctx: RequestContext,
+        input: ElasticSearchInput,
+        enabledOnly: boolean = false,
+    ): Promise<Array<{ facetValue: FacetValue; count: number }>> {
+        const { indexPrefix } = this.options;
+        const elasticSearchBody = buildElasticBody(
+            input,
+            this.options.searchConfig,
+            ctx.channelId,
+            ctx.languageCode,
+            enabledOnly,
+        );
+        elasticSearchBody.from = 0;
+        elasticSearchBody.size = 0;
+        elasticSearchBody.aggs = {
+            facetValue: {
+                terms: {
+                    field: 'facetValueIds',
+                    size: this.options.searchConfig.facetValueMaxSize,
+                },
+            },
+        };
+        let body: SearchResponseBody<VariantIndexItem>;
+        try {
+            const result = await this.client.search<SearchResponseBody<VariantIndexItem>>({
+                index: indexPrefix + (input.groupByProduct ? PRODUCT_INDEX_NAME : VARIANT_INDEX_NAME),
+                body: elasticSearchBody,
+            });
+            body = result.body;
+        } catch (e) {
+            Logger.error(e.message, loggerCtx, e.stack);
+            throw e;
+        }
+
+        const buckets = body.aggregations ? body.aggregations.facetValue.buckets : [];
+
+        const facetValues = await this.facetValueService.findByIds(
+            ctx,
+            buckets.map(b => b.key),
+        );
+        return facetValues.map((facetValue, index) => {
+            const bucket = buckets.find(b => b.key.toString() === facetValue.id.toString());
+            return {
+                facetValue,
+                count: bucket ? bucket.doc_count : 0,
+            };
+        });
+    }
+
     async priceRange(ctx: RequestContext, input: ElasticSearchInput): Promise<SearchPriceData> {
         const { indexPrefix, searchConfig } = this.options;
         const { groupByProduct } = input;

+ 13 - 6
packages/elasticsearch-plugin/src/indexer.controller.ts

@@ -162,7 +162,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
     async deleteVariants({ ctx: rawContext, variantIds }: UpdateVariantMessageData): Promise<boolean> {
         const productIds = await this.getProductIdsByVariantIds(variantIds);
         for (const productId of productIds) {
-            await this.deleteProductInternal(productId);
+            await this.updateProductsInternal([productId]);
         }
         return true;
     }
@@ -229,6 +229,8 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                     .where('product.deletedAt IS NULL')
                     .getMany();
 
+                Logger.verbose(`Reindexing ${productIds.length} Products`, loggerCtx);
+
                 let finishedProductsCount = 0;
                 for (const { id: productId } of productIds) {
                     await this.updateProductsInternal([productId]);
@@ -396,7 +398,9 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                     updatedProductVariants.forEach(v => (v.enabled = false));
                 }
                 Logger.verbose(`Updating Product (${productId})`, loggerCtx);
-                if (updatedProductVariants.length) await this.updateVariantsInternal(updatedProductVariants);
+                if (updatedProductVariants.length) {
+                    await this.updateVariantsInternal(updatedProductVariants);
+                }
 
                 const languageVariants = product.translations.map(t => t.languageCode);
 
@@ -441,7 +445,6 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
     }
 
     private async deleteProductInternal(productId: ID) {
-        Logger.verbose(`Deleting 1 Product (${productId})`, loggerCtx);
         const channels = await this.connection
             .getRepository(Channel)
             .createQueryBuilder('channel')
@@ -451,6 +454,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
             relations: ['variants'],
         });
         if (product) {
+            Logger.verbose(`Deleting 1 Product (id: ${productId})`, loggerCtx);
             const operations: BulkOperation[] = [];
             for (const { id: channelId } of channels) {
                 const languageVariants = product.translations.map(t => t.languageCode);
@@ -494,6 +498,9 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         indexName: string,
         operations: Array<BulkOperation | BulkOperationDoc<VariantIndexItem | ProductIndexItem>>,
     ) {
+        if (operations.length === 0) {
+            return;
+        }
         try {
             const fullIndexName = this.options.indexPrefix + indexName;
             const { body }: { body: BulkResponseBody } = await this.client.bulk({
@@ -519,7 +526,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                     }
                 });
             } else {
-                Logger.verbose(
+                Logger.debug(
                     `Executed ${body.items.length} bulk operations on index [${fullIndexName}]`,
                     loggerCtx,
                 );
@@ -527,7 +534,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
             return body;
         } catch (e) {
             Logger.error(`Error when attempting to run bulk operations [${e.toString()}]`, loggerCtx);
-            Logger.error('Error details: ' + JSON.stringify(e.body && e.body.error, null, 2), loggerCtx);
+            Logger.error('Error details: ' + JSON.stringify(e.body?.error, null, 2), loggerCtx);
         }
     }
 
@@ -645,7 +652,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         const productTranslation = this.getTranslation(product, ctx.languageCode);
         return {
             channelId: ctx.channelId,
-            languageCode: languageCode,
+            languageCode,
             sku: '',
             slug: productTranslation.slug,
             productId: product.id,