Bläddra i källkod

fix(elastic-search-plugin): Refactor indexing to allow searching by fields on all variants (#1086)

Closes #1018, closes #1017 

This commit changes the way products and variants are indexed in Elasticsearch. Previously we used 2 separate indices - one for Products and one for ProductVariants. This caused issues like #1017 which could not be overcome without changing the way we index. Now we have a single index and use the Elasticsearch "collapse" feature in order to do the grouping of variants into products.

This simplifies the logic for indexing quite a bit, and also speeds up indexing. The downside is that querying is slightly less efficient now, since a separate query is now required in order to get the total results count. However, this slight performance hit is worth it for having correct functioning, which was not possible before.
Artem Danilov 4 år sedan
förälder
incheckning
27994d81b8

+ 8 - 7
packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts

@@ -38,7 +38,7 @@ import {
     UpdateCollection,
     UpdateProduct,
     UpdateProductVariants,
-    UpdateTaxRate
+    UpdateTaxRate,
 } from '../../core/e2e/graphql/generated-e2e-admin-types';
 import { SearchProductsShop } from '../../core/e2e/graphql/generated-e2e-shop-types';
 import {
@@ -277,8 +277,8 @@ describe('Elasticsearch plugin', () => {
                 },
             );
             expect(result.search.collections).toEqual([
-                {collection: {id: 'T_2', name: 'Plants',},count: 3,},
-        ]);
+                { collection: { id: 'T_2', name: 'Plants' }, count: 3 },
+            ]);
         });
 
         it('returns correct collections when grouped by product', async () => {
@@ -291,7 +291,7 @@ describe('Elasticsearch plugin', () => {
                 },
             );
             expect(result.search.collections).toEqual([
-                {collection: {id: 'T_2', name: 'Plants',},count: 3,},
+                { collection: { id: 'T_2', name: 'Plants' }, count: 3 },
             ]);
         });
 
@@ -1188,9 +1188,10 @@ describe('Elasticsearch plugin', () => {
 
             it('indexes product variant-level languages', async () => {
                 const { search: search1 } = await searchInLanguage(LanguageCode.fr, false);
-
-                expect(search1.items[0].productName).toBe('Laptop');
-                expect(search1.items[0].productVariantName).toBe('laptop variant fr');
+                expect(search1.items.length ? search1.items[0].productName : undefined).toBe('Laptop');
+                expect(search1.items.length ? search1.items[0].productVariantName : undefined).toBe(
+                    'laptop variant fr',
+                );
             });
         });
     });

+ 8 - 23
packages/elasticsearch-plugin/src/build-elastic-body.spec.ts

@@ -116,7 +116,7 @@ describe('buildElasticBody()', () => {
 
     it('facetValueFilters OR', () => {
         const result = buildElasticBody(
-            { facetValueFilters: [ { or: ['1', '2'] }] },
+            { facetValueFilters: [{ or: ['1', '2'] }] },
             searchConfig,
             CHANNEL_ID,
             LanguageCode.en,
@@ -262,7 +262,7 @@ describe('buildElasticBody()', () => {
                 CHANNEL_ID,
                 LanguageCode.en,
             );
-            expect(result.sort).toEqual([{ priceMin: { order: 'asc' } }]);
+            expect(result.sort).toEqual([{ price: { order: 'asc' } }]);
         });
     });
 
@@ -302,6 +302,9 @@ describe('buildElasticBody()', () => {
         );
 
         expect(result).toEqual({
+            collapse: {
+                field: 'productId',
+            },
             from: 0,
             size: 25,
             query: {
@@ -451,14 +454,8 @@ describe('buildElasticBody()', () => {
                         LANGUAGE_CODE_TERM,
                         {
                             range: {
-                                priceMin: {
+                                price: {
                                     gte: 500,
-                                },
-                            },
-                        },
-                        {
-                            range: {
-                                priceMax: {
                                     lte: 1500,
                                 },
                             },
@@ -482,14 +479,8 @@ describe('buildElasticBody()', () => {
                         LANGUAGE_CODE_TERM,
                         {
                             range: {
-                                priceWithTaxMin: {
+                                priceWithTax: {
                                     gte: 500,
-                                },
-                            },
-                        },
-                        {
-                            range: {
-                                priceWithTaxMax: {
                                     lte: 1500,
                                 },
                             },
@@ -524,14 +515,8 @@ describe('buildElasticBody()', () => {
                         { term: { collectionIds: '3' } },
                         {
                             range: {
-                                priceWithTaxMin: {
+                                priceWithTax: {
                                     gte: 500,
-                                },
-                            },
-                        },
-                        {
-                            range: {
-                                priceWithTaxMax: {
                                     lte: 1500,
                                 },
                             },

+ 19 - 34
packages/elasticsearch-plugin/src/build-elastic-body.ts

@@ -90,15 +90,14 @@ export function buildElasticBody(
         ensureBoolFilterExists(query);
         query.bool.filter.push({ term: { enabled: true } });
     }
+
     if (priceRange) {
         ensureBoolFilterExists(query);
-        query.bool.filter = query.bool.filter.concat(createPriceFilters(priceRange, false, !!groupByProduct));
+        query.bool.filter = query.bool.filter.concat(createPriceFilters(priceRange, false));
     }
     if (priceRangeWithTax) {
         ensureBoolFilterExists(query);
-        query.bool.filter = query.bool.filter.concat(
-            createPriceFilters(priceRangeWithTax, true, !!groupByProduct),
-        );
+        query.bool.filter = query.bool.filter.concat(createPriceFilters(priceRangeWithTax, true));
     }
 
     const sortArray = [];
@@ -109,11 +108,12 @@ export function buildElasticBody(
             });
         }
         if (sort.price) {
-            const priceField = groupByProduct ? 'priceMin' : 'price';
+            const priceField = 'price';
             sortArray.push({ [priceField]: { order: sort.price === SortOrder.ASC ? 'asc' : 'desc' } });
         }
     }
-    return {
+
+    const body: SearchRequestBody = {
         query: searchConfig.mapQuery
             ? searchConfig.mapQuery(query, input, searchConfig, channelId, enabledOnly)
             : query,
@@ -122,6 +122,10 @@ export function buildElasticBody(
         size: take || 10,
         track_total_hits: searchConfig.totalItemsMaxSize,
     };
+    if (groupByProduct) {
+        body.collapse = { field: `productId` };
+    }
+    return body;
 }
 
 function ensureBoolFilterExists(query: { bool: { filter?: any } }) {
@@ -130,35 +134,16 @@ function ensureBoolFilterExists(query: { bool: { filter?: any } }) {
     }
 }
 
-function createPriceFilters(range: PriceRange, withTax: boolean, groupByProduct: boolean): any[] {
+function createPriceFilters(range: PriceRange, withTax: boolean): any[] {
     const withTaxFix = withTax ? 'WithTax' : '';
-    if (groupByProduct) {
-        return [
-            {
-                range: {
-                    [`price${withTaxFix}Min`]: {
-                        gte: range.min,
-                    },
+    return [
+        {
+            range: {
+                ['price' + withTaxFix]: {
+                    gte: range.min,
+                    lte: range.max,
                 },
             },
-            {
-                range: {
-                    [`price${withTaxFix}Max`]: {
-                        lte: range.max,
-                    },
-                },
-            },
-        ];
-    } else {
-        return [
-            {
-                range: {
-                    ['price' + withTaxFix]: {
-                        gte: range.min,
-                        lte: range.max,
-                    },
-                },
-            },
-        ];
-    }
+        },
+    ];
 }

+ 0 - 1
packages/elasticsearch-plugin/src/constants.ts

@@ -1,4 +1,3 @@
 export const ELASTIC_SEARCH_OPTIONS = Symbol('ELASTIC_SEARCH_OPTIONS');
 export const VARIANT_INDEX_NAME = 'variants';
-export const PRODUCT_INDEX_NAME = 'products';
 export const loggerCtx = 'ElasticsearchPlugin';

+ 118 - 69
packages/elasticsearch-plugin/src/elasticsearch.service.ts

@@ -17,7 +17,7 @@ import {
 import equal from 'fast-deep-equal/es6';
 
 import { buildElasticBody } from './build-elastic-body';
-import { ELASTIC_SEARCH_OPTIONS, loggerCtx, PRODUCT_INDEX_NAME, VARIANT_INDEX_NAME } from './constants';
+import { ELASTIC_SEARCH_OPTIONS, loggerCtx, VARIANT_INDEX_NAME } from './constants';
 import { ElasticsearchIndexService } from './elasticsearch-index.service';
 import { createIndices } from './indexing-utils';
 import { ElasticsearchOptions } from './options';
@@ -165,13 +165,12 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
                 }
 
                 await this.client.indices.delete({
-                    index: [tempPrefix + `products`, tempPrefix + `variants`],
+                    index: [tempPrefix + `variants`],
                 });
             }
         };
 
         await createIndex(VARIANT_INDEX_NAME);
-        await createIndex(PRODUCT_INDEX_NAME);
     }
 
     /**
@@ -193,13 +192,14 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         );
         if (groupByProduct) {
             try {
-                const { body }: { body: SearchResponseBody<ProductIndexItem> } = await this.client.search({
-                    index: indexPrefix + PRODUCT_INDEX_NAME,
+                const { body }: { body: SearchResponseBody<VariantIndexItem> } = await this.client.search({
+                    index: indexPrefix + VARIANT_INDEX_NAME,
                     body: elasticSearchBody,
                 });
+                const totalItems = await this.totalHits(ctx, input, groupByProduct);
                 return {
                     items: body.hits.hits.map(hit => this.mapProductToSearchResult(hit)),
-                    totalItems: body.hits.total ? body.hits.total.value : 0,
+                    totalItems,
                 };
             } catch (e) {
                 Logger.error(e.message, loggerCtx, e.stack);
@@ -222,18 +222,15 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         }
     }
 
-    /**
-     * Return a list of all FacetValues which appear in the result set.
-     */
-    async facetValues(
+    async totalHits(
         ctx: RequestContext,
         input: ElasticSearchInput,
         enabledOnly: boolean = false,
-    ): Promise<Array<{ facetValue: FacetValue; count: number }>> {
-        const { indexPrefix } = this.options;
+    ): Promise<number> {
+        const { indexPrefix, searchConfig } = this.options;
         const elasticSearchBody = buildElasticBody(
             input,
-            this.options.searchConfig,
+            searchConfig,
             ctx.channelId,
             ctx.languageCode,
             enabledOnly,
@@ -241,36 +238,52 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         elasticSearchBody.from = 0;
         elasticSearchBody.size = 0;
         elasticSearchBody.aggs = {
-            facetValue: {
-                terms: {
-                    field: 'facetValueIds',
-                    size: this.options.searchConfig.facetValueMaxSize,
+            total: {
+                cardinality: {
+                    field: `productId`,
                 },
             },
         };
-        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 { body }: { body: SearchResponseBody<VariantIndexItem> } = await this.client.search({
+            index: indexPrefix + VARIANT_INDEX_NAME,
+            body: elasticSearchBody,
+        });
+
+        const { aggregations } = body;
+        if (!aggregations) {
+            throw new InternalServerError(
+                'An error occurred when querying Elasticsearch for priceRange aggregations',
+            );
         }
+        return aggregations.total ? aggregations.total.value : 0;
+    }
 
-        const buckets = body.aggregations ? body.aggregations.facetValue.buckets : [];
+    /**
+     * 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 { groupByProduct } = input;
+        const buckets = await this.getDistinctBucketsOfField(ctx, input, enabledOnly, `facetValueIds`);
 
         const facetValues = await this.facetValueService.findByIds(
             ctx,
             buckets.map(b => b.key),
         );
-        return facetValues.map((facetValue, index) => {
+        return facetValues.map(facetValue => {
             const bucket = buckets.find(b => b.key.toString() === facetValue.id.toString());
+            let count;
+            if (groupByProduct) {
+                count = bucket ? bucket.total.value : 0;
+            } else {
+                count = bucket ? bucket.doc_count : 0;
+            }
             return {
                 facetValue,
-                count: bucket ? bucket.doc_count : 0,
+                count,
             };
         });
     }
@@ -283,7 +296,36 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         input: ElasticSearchInput,
         enabledOnly: boolean = false,
     ): Promise<Array<{ collection: Collection; count: number }>> {
+        const { groupByProduct } = input;
+        const buckets = await this.getDistinctBucketsOfField(ctx, input, enabledOnly, `collectionIds`);
+
+        const collections = await this.collectionService.findByIds(
+            ctx,
+            buckets.map(b => b.key),
+        );
+        return collections.map(collection => {
+            const bucket = buckets.find(b => b.key.toString() === collection.id.toString());
+            let count;
+            if (groupByProduct) {
+                count = bucket ? bucket.total.value : 0;
+            } else {
+                count = bucket ? bucket.doc_count : 0;
+            }
+            return {
+                collection,
+                count,
+            };
+        });
+    }
+
+    async getDistinctBucketsOfField(
+        ctx: RequestContext,
+        input: ElasticSearchInput,
+        enabledOnly: boolean = false,
+        field: string,
+    ): Promise<Array<{ key: string; doc_count: number; total: { value: number } }>> {
         const { indexPrefix } = this.options;
+        const { groupByProduct } = input;
         const elasticSearchBody = buildElasticBody(
             input,
             this.options.searchConfig,
@@ -296,15 +338,26 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         elasticSearchBody.aggs = {
             collection: {
                 terms: {
-                    field: 'collectionIds',
+                    field,
                     size: this.options.searchConfig.collectionMaxSize,
                 },
             },
         };
+
+        if (groupByProduct) {
+            elasticSearchBody.aggs.collection.aggs = {
+                total: {
+                    cardinality: {
+                        field: `productId`,
+                    },
+                },
+            };
+        }
+
         let body: SearchResponseBody<VariantIndexItem>;
         try {
             const result = await this.client.search<SearchResponseBody<VariantIndexItem>>({
-                index: indexPrefix + (input.groupByProduct ? PRODUCT_INDEX_NAME : VARIANT_INDEX_NAME),
+                index: indexPrefix + VARIANT_INDEX_NAME,
                 body: elasticSearchBody,
             });
             body = result.body;
@@ -313,24 +366,11 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
             throw e;
         }
 
-        const buckets = body.aggregations ? body.aggregations.collection.buckets : [];
-
-        const collections = await this.collectionService.findByIds(
-            ctx,
-            buckets.map(b => b.key),
-        );
-        return collections.map(collection => {
-            const bucket = buckets.find(b => b.key.toString() === collection.id.toString());
-            return {
-                collection,
-                count: bucket ? bucket.doc_count : 0,
-            };
-        });
+        return body.aggregations ? body.aggregations.collection.buckets : [];
     }
 
     async priceRange(ctx: RequestContext, input: ElasticSearchInput): Promise<SearchPriceData> {
         const { indexPrefix, searchConfig } = this.options;
-        const { groupByProduct } = input;
         const elasticSearchBody = buildElasticBody(
             input,
             searchConfig,
@@ -343,39 +383,39 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         elasticSearchBody.aggs = {
             minPrice: {
                 min: {
-                    field: groupByProduct ? 'priceMin' : 'price',
+                    field: 'price',
                 },
             },
             minPriceWithTax: {
                 min: {
-                    field: groupByProduct ? 'priceWithTaxMin' : 'priceWithTax',
+                    field: 'priceWithTax',
                 },
             },
             maxPrice: {
                 max: {
-                    field: groupByProduct ? 'priceMax' : 'price',
+                    field: 'price',
                 },
             },
             maxPriceWithTax: {
                 max: {
-                    field: groupByProduct ? 'priceWithTaxMax' : 'priceWithTax',
+                    field: 'priceWithTax',
                 },
             },
             prices: {
                 histogram: {
-                    field: groupByProduct ? 'priceMin' : 'price',
+                    field: 'price',
                     interval: searchConfig.priceRangeBucketInterval,
                 },
             },
             pricesWithTax: {
                 histogram: {
-                    field: groupByProduct ? 'priceWithTaxMin' : 'priceWithTax',
+                    field: 'priceWithTax',
                     interval: searchConfig.priceRangeBucketInterval,
                 },
             },
         };
         const { body }: { body: SearchResponseBody<VariantIndexItem> } = await this.client.search({
-            index: indexPrefix + (input.groupByProduct ? PRODUCT_INDEX_NAME : VARIANT_INDEX_NAME),
+            index: indexPrefix + VARIANT_INDEX_NAME,
             body: elasticSearchBody,
         });
 
@@ -408,7 +448,6 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
      * Rebuilds the full search index.
      */
     async reindex(ctx: RequestContext): Promise<Job> {
-        const { indexPrefix } = this.options;
         const job = await this.elasticsearchIndexService.reindex(ctx);
         // tslint:disable-next-line:no-non-null-assertion
         return job!;
@@ -430,44 +469,51 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
             score: hit._score || 0,
         };
 
-        this.addCustomMappings(result, source, this.options.customProductVariantMappings);
+        ElasticsearchService.addCustomMappings(
+            result,
+            source,
+            this.options.customProductVariantMappings,
+            false,
+        );
         return result;
     }
 
-    private mapProductToSearchResult(hit: SearchHit<ProductIndexItem>): SearchResult {
+    private mapProductToSearchResult(hit: SearchHit<VariantIndexItem>): SearchResult {
         const source = hit._source;
         const { productAsset, productVariantAsset } = this.getSearchResultAssets(source);
         const result = {
             ...source,
             productAsset,
             productVariantAsset,
+            enabled: source.productEnabled,
             productId: source.productId.toString(),
             productName: source.productName,
             productVariantId: source.productVariantId.toString(),
             productVariantName: source.productVariantName,
-            facetIds: source.facetIds as string[],
-            facetValueIds: source.facetValueIds as string[],
-            collectionIds: source.collectionIds as string[],
+            facetIds: source.productFacetIds as string[],
+            facetValueIds: source.productFacetValueIds as string[],
+            collectionIds: source.productCollectionIds as string[],
             sku: source.sku,
             slug: source.slug,
             price: {
-                min: source.priceMin,
-                max: source.priceMax,
+                min: source.productPriceMin,
+                max: source.productPriceMax,
             },
             priceWithTax: {
-                min: source.priceWithTaxMin,
-                max: source.priceWithTaxMax,
+                min: source.productPriceWithTaxMin,
+                max: source.productPriceWithTaxMax,
             },
             channelIds: [],
             score: hit._score || 0,
         };
-        this.addCustomMappings(result, source, this.options.customProductMappings);
+        ElasticsearchService.addCustomMappings(result, source, this.options.customProductMappings, true);
         return result;
     }
 
-    private getSearchResultAssets(
-        source: ProductIndexItem | VariantIndexItem,
-    ): { productAsset: SearchResultAsset | undefined; productVariantAsset: SearchResultAsset | undefined } {
+    private getSearchResultAssets(source: ProductIndexItem | VariantIndexItem): {
+        productAsset: SearchResultAsset | undefined;
+        productVariantAsset: SearchResultAsset | undefined;
+    } {
         const productAsset: SearchResultAsset | undefined = source.productAssetId
             ? {
                   id: source.productAssetId.toString(),
@@ -485,16 +531,19 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         return { productAsset, productVariantAsset };
     }
 
-    private addCustomMappings(
+    private static addCustomMappings(
         result: any,
         source: any,
         mappings: { [fieldName: string]: CustomMapping<any> },
+        groupByProduct: boolean,
     ): any {
         const customMappings = Object.keys(mappings);
         if (customMappings.length) {
             const customMappingsResult: any = {};
             for (const name of customMappings) {
-                customMappingsResult[name] = (source as any)[name];
+                customMappingsResult[name] = (source as any)[
+                    groupByProduct ? `product-${name}` : `variant-${name}`
+                ];
             }
             (result as any).customMappings = customMappingsResult;
         }

+ 157 - 261
packages/elasticsearch-plugin/src/indexer.controller.ts

@@ -22,7 +22,7 @@ import {
 } from '@vendure/core';
 import { Observable } from 'rxjs';
 
-import { ELASTIC_SEARCH_OPTIONS, loggerCtx, PRODUCT_INDEX_NAME, VARIANT_INDEX_NAME } from './constants';
+import { ELASTIC_SEARCH_OPTIONS, loggerCtx, VARIANT_INDEX_NAME } from './constants';
 import { createIndices, getIndexNameByAlias } from './indexing-utils';
 import { ElasticsearchOptions } from './options';
 import {
@@ -65,10 +65,6 @@ export interface ReindexMessageResponse {
     duration: number;
 }
 
-type BulkProductOperation = {
-    index: typeof PRODUCT_INDEX_NAME;
-    operation: BulkOperation | BulkOperationDoc<ProductIndexItem>;
-};
 type BulkVariantOperation = {
     index: typeof VARIANT_INDEX_NAME;
     operation: BulkOperation | BulkOperationDoc<VariantIndexItem>;
@@ -211,12 +207,10 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         return asyncObservable(async observer => {
             return this.asyncQueue.push(async () => {
                 const timeStart = Date.now();
-                const operations: Array<BulkProductOperation | BulkVariantOperation> = [];
+                const operations: BulkVariantOperation[] = [];
 
                 const reindexTempName = new Date().getTime();
-                const productIndexName = this.options.indexPrefix + PRODUCT_INDEX_NAME;
                 const variantIndexName = this.options.indexPrefix + VARIANT_INDEX_NAME;
-                const reindexProductAliasName = productIndexName + `-reindex-${reindexTempName}`;
                 const reindexVariantAliasName = variantIndexName + `-reindex-${reindexTempName}`;
                 try {
                     await createIndices(
@@ -229,29 +223,17 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                         `-reindex-${reindexTempName}`,
                     );
 
-                    const reindexProductIndexName = await getIndexNameByAlias(
-                        this.client,
-                        reindexProductAliasName,
-                    );
                     const reindexVariantIndexName = await getIndexNameByAlias(
                         this.client,
                         reindexVariantAliasName,
                     );
-
-                    const originalProductAliasExist = await this.client.indices.existsAlias({
-                        name: productIndexName,
-                    });
                     const originalVariantAliasExist = await this.client.indices.existsAlias({
                         name: variantIndexName,
                     });
-                    const originalProductIndexExist = await this.client.indices.exists({
-                        index: productIndexName,
-                    });
                     const originalVariantIndexExist = await this.client.indices.exists({
                         index: variantIndexName,
                     });
 
-                    const originalProductIndexName = await getIndexNameByAlias(this.client, productIndexName);
                     const originalVariantIndexName = await getIndexNameByAlias(this.client, variantIndexName);
 
                     if (originalVariantAliasExist.body || originalVariantIndexExist.body) {
@@ -267,19 +249,6 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                             },
                         });
                     }
-                    if (originalProductAliasExist.body || originalProductIndexExist.body) {
-                        await this.client.reindex({
-                            refresh: true,
-                            body: {
-                                source: {
-                                    index: productIndexName,
-                                },
-                                dest: {
-                                    index: reindexProductAliasName,
-                                },
-                            },
-                        });
-                    }
 
                     const actions = [
                         {
@@ -288,39 +257,14 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                                 alias: reindexVariantAliasName,
                             },
                         },
-                        {
-                            remove: {
-                                index: reindexProductIndexName,
-                                alias: reindexProductAliasName,
-                            },
-                        },
                         {
                             add: {
                                 index: reindexVariantIndexName,
                                 alias: variantIndexName,
                             },
                         },
-                        {
-                            add: {
-                                index: reindexProductIndexName,
-                                alias: productIndexName,
-                            },
-                        },
                     ];
 
-                    if (originalProductAliasExist.body) {
-                        actions.push({
-                            remove: {
-                                index: originalProductIndexName,
-                                alias: productIndexName,
-                            },
-                        });
-                    } else if (originalProductIndexExist.body) {
-                        await this.client.indices.delete({
-                            index: [productIndexName],
-                        });
-                    }
-
                     if (originalVariantAliasExist.body) {
                         actions.push({
                             remove: {
@@ -340,11 +284,6 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                         },
                     });
 
-                    if (originalProductAliasExist.body) {
-                        await this.client.indices.delete({
-                            index: [originalProductIndexName],
-                        });
-                    }
                     if (originalVariantAliasExist.body) {
                         await this.client.indices.delete({
                             index: [originalVariantIndexName],
@@ -369,18 +308,6 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                             index: [reindexVariantIndexName],
                         });
                     }
-                    const reindexProductAliasExist = await this.client.indices.existsAlias({
-                        name: reindexProductAliasName,
-                    });
-                    if (reindexProductAliasExist.body) {
-                        const reindexProductAliasResult = await this.client.indices.getAlias({
-                            name: reindexProductAliasName,
-                        });
-                        const reindexProductIndexName = Object.keys(reindexProductAliasResult.body)[0];
-                        await this.client.indices.delete({
-                            index: [reindexProductIndexName],
-                        });
-                    }
                 }
 
                 const deletedProductIds = await this.connection
@@ -426,27 +353,19 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
     }
 
     async updateAsset(data: UpdateAssetMessageData): Promise<boolean> {
-        const result1 = await this.updateAssetFocalPointForIndex(PRODUCT_INDEX_NAME, data.asset);
-        const result2 = await this.updateAssetFocalPointForIndex(VARIANT_INDEX_NAME, data.asset);
+        const result = await this.updateAssetFocalPointForIndex(VARIANT_INDEX_NAME, data.asset);
         await this.client.indices.refresh({
-            index: [
-                this.options.indexPrefix + PRODUCT_INDEX_NAME,
-                this.options.indexPrefix + VARIANT_INDEX_NAME,
-            ],
+            index: [this.options.indexPrefix + VARIANT_INDEX_NAME],
         });
-        return result1 && result2;
+        return result;
     }
 
     async deleteAsset(data: UpdateAssetMessageData): Promise<boolean> {
-        const result1 = await this.deleteAssetForIndex(PRODUCT_INDEX_NAME, data.asset);
-        const result2 = await this.deleteAssetForIndex(VARIANT_INDEX_NAME, data.asset);
+        const result = await this.deleteAssetForIndex(VARIANT_INDEX_NAME, data.asset);
         await this.client.indices.refresh({
-            index: [
-                this.options.indexPrefix + PRODUCT_INDEX_NAME,
-                this.options.indexPrefix + VARIANT_INDEX_NAME,
-            ],
+            index: [this.options.indexPrefix + VARIANT_INDEX_NAME],
         });
-        return result1 && result2;
+        return result;
     }
 
     private async updateAssetFocalPointForIndex(indexName: string, asset: Asset): Promise<boolean> {
@@ -517,11 +436,9 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         await this.executeBulkOperations(operations);
     }
 
-    private async updateProductsOperations(
-        productIds: ID[],
-    ): Promise<Array<BulkProductOperation | BulkVariantOperation>> {
+    private async updateProductsOperations(productIds: ID[]): Promise<BulkVariantOperation[]> {
         Logger.verbose(`Updating ${productIds.length} Products`, loggerCtx);
-        const operations: Array<BulkProductOperation | BulkVariantOperation> = [];
+        const operations: BulkVariantOperation[] = [];
 
         for (const productId of productIds) {
             operations.push(...(await this.deleteProductOperations(productId)));
@@ -549,11 +466,12 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                     updatedProductVariants.forEach(v => (v.enabled = false));
                 }
                 Logger.verbose(`Updating Product (${productId})`, loggerCtx);
-                if (updatedProductVariants.length) {
-                    operations.push(...(await this.updateVariantsOperations(updatedProductVariants)));
+                const languageVariants: LanguageCode[] = [];
+                languageVariants.push(...product.translations.map(t => t.languageCode));
+                for (const variant of product.variants) {
+                    languageVariants.push(...variant.translations.map(t => t.languageCode));
                 }
-
-                const languageVariants = product.translations.map(t => t.languageCode);
+                const uniqueLanguageVariants = unique(languageVariants);
 
                 for (const channel of product.channels) {
                     const channelCtx = new RequestContext({
@@ -570,34 +488,63 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                     for (const variant of variantsInChannel) {
                         await this.productVariantService.applyChannelPriceAndTax(variant, channelCtx);
                     }
-                    for (const languageCode of languageVariants) {
-                        operations.push(
-                            {
-                                index: PRODUCT_INDEX_NAME,
-                                operation: {
-                                    update: {
-                                        _id: this.getId(product.id, channelCtx.channelId, languageCode),
+                    for (const languageCode of uniqueLanguageVariants) {
+                        if (variantsInChannel.length) {
+                            for (const variant of variantsInChannel) {
+                                operations.push(
+                                    {
+                                        index: VARIANT_INDEX_NAME,
+                                        operation: {
+                                            update: {
+                                                _id: ElasticsearchIndexerController.getId(
+                                                    variant.id,
+                                                    channelCtx.channelId,
+                                                    languageCode,
+                                                ),
+                                            },
+                                        },
+                                    },
+                                    {
+                                        index: VARIANT_INDEX_NAME,
+                                        operation: {
+                                            doc: this.createVariantIndexItem(
+                                                variant,
+                                                variantsInChannel,
+                                                channelCtx,
+                                                languageCode,
+                                            ),
+                                            doc_as_upsert: true,
+                                        },
+                                    },
+                                );
+                            }
+                        } else {
+                            operations.push(
+                                {
+                                    index: VARIANT_INDEX_NAME,
+                                    operation: {
+                                        update: {
+                                            _id: ElasticsearchIndexerController.getId(
+                                                -product.id,
+                                                channelCtx.channelId,
+                                                languageCode,
+                                            ),
+                                        },
                                     },
                                 },
-                            },
-                            {
-                                index: PRODUCT_INDEX_NAME,
-                                operation: {
-                                    doc: variantsInChannel.length
-                                        ? this.createProductIndexItem(
-                                              variantsInChannel,
-                                              channelCtx.channelId,
-                                              languageCode,
-                                          )
-                                        : this.createSyntheticProductIndexItem(
-                                              channelCtx,
-                                              product,
-                                              languageCode,
-                                          ),
-                                    doc_as_upsert: true,
+                                {
+                                    index: VARIANT_INDEX_NAME,
+                                    operation: {
+                                        doc: this.createSyntheticProductIndexItem(
+                                            product,
+                                            channelCtx,
+                                            languageCode,
+                                        ),
+                                        doc_as_upsert: true,
+                                    },
                                 },
-                            },
-                        );
+                            );
+                        }
                     }
                 }
             }
@@ -605,50 +552,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         return operations;
     }
 
-    private async updateVariantsOperations(
-        productVariants: ProductVariant[],
-    ): Promise<BulkVariantOperation[]> {
-        if (productVariants.length === 0) {
-            return [];
-        }
-        const operations: BulkVariantOperation[] = [];
-        for (const variant of productVariants) {
-            const languageVariants = variant.translations.map(t => t.languageCode);
-            for (const channel of variant.channels) {
-                const channelCtx = new RequestContext({
-                    channel,
-                    apiType: 'admin',
-                    authorizedAsOwnerOnly: false,
-                    isAuthorized: true,
-                    session: {} as any,
-                });
-                await this.productVariantService.applyChannelPriceAndTax(variant, channelCtx);
-                for (const languageCode of languageVariants) {
-                    operations.push(
-                        {
-                            index: VARIANT_INDEX_NAME,
-                            operation: {
-                                update: { _id: this.getId(variant.id, channelCtx.channelId, languageCode) },
-                            },
-                        },
-                        {
-                            index: VARIANT_INDEX_NAME,
-                            operation: {
-                                doc: this.createVariantIndexItem(variant, channelCtx.channelId, languageCode),
-                                doc_as_upsert: true,
-                            },
-                        },
-                    );
-                }
-            }
-        }
-        Logger.verbose(`Updating ${productVariants.length} ProductVariants`, loggerCtx);
-        return operations;
-    }
-
-    private async deleteProductOperations(
-        productId: ID,
-    ): Promise<Array<BulkProductOperation | BulkVariantOperation>> {
+    private async deleteProductOperations(productId: ID): Promise<BulkVariantOperation[]> {
         const channels = await this.connection
             .getRepository(Channel)
             .createQueryBuilder('channel')
@@ -662,13 +566,23 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         }
 
         Logger.verbose(`Deleting 1 Product (id: ${productId})`, loggerCtx);
-        const operations: Array<BulkProductOperation | BulkVariantOperation> = [];
+        const operations: BulkVariantOperation[] = [];
+        const languageVariants: LanguageCode[] = [];
+        languageVariants.push(...product.translations.map(t => t.languageCode));
+        for (const variant of product.variants) {
+            languageVariants.push(...variant.translations.map(t => t.languageCode));
+        }
+        const uniqueLanguageVariants = unique(languageVariants);
+
         for (const { id: channelId } of channels) {
-            const languageVariants = product.translations.map(t => t.languageCode);
-            for (const languageCode of languageVariants) {
+            for (const languageCode of uniqueLanguageVariants) {
                 operations.push({
-                    index: PRODUCT_INDEX_NAME,
-                    operation: { delete: { _id: this.getId(product.id, channelId, languageCode) } },
+                    index: VARIANT_INDEX_NAME,
+                    operation: {
+                        delete: {
+                            _id: ElasticsearchIndexerController.getId(-product.id, channelId, languageCode),
+                        },
+                    },
                 });
             }
         }
@@ -676,6 +590,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
             ...(await this.deleteVariantsInternalOperations(
                 product.variants,
                 channels.map(c => c.id),
+                uniqueLanguageVariants,
             )),
         );
         return operations;
@@ -684,17 +599,23 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
     private async deleteVariantsInternalOperations(
         variants: ProductVariant[],
         channelIds: ID[],
+        languageVariants: LanguageCode[],
     ): Promise<BulkVariantOperation[]> {
         Logger.verbose(`Deleting ${variants.length} ProductVariants`, loggerCtx);
         const operations: BulkVariantOperation[] = [];
         for (const variant of variants) {
             for (const channelId of channelIds) {
-                const languageVariants = variant.translations.map(t => t.languageCode);
                 for (const languageCode of languageVariants) {
                     operations.push({
                         index: VARIANT_INDEX_NAME,
                         operation: {
-                            delete: { _id: this.getId(variant.id, channelId, languageCode) },
+                            delete: {
+                                _id: ElasticsearchIndexerController.getId(
+                                    variant.id,
+                                    channelId,
+                                    languageCode,
+                                ),
+                            },
                         },
                     });
                 }
@@ -711,22 +632,14 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         return unique(variants.map(v => v.product.id));
     }
 
-    private async executeBulkOperations(operations: Array<BulkProductOperation | BulkVariantOperation>) {
-        const productOperations: Array<BulkOperation | BulkOperationDoc<ProductIndexItem>> = [];
+    private async executeBulkOperations(operations: BulkVariantOperation[]) {
         const variantOperations: Array<BulkOperation | BulkOperationDoc<VariantIndexItem>> = [];
 
         for (const operation of operations) {
-            if (operation.index === PRODUCT_INDEX_NAME) {
-                productOperations.push(operation.operation);
-            } else {
-                variantOperations.push(operation.operation);
-            }
+            variantOperations.push(operation.operation);
         }
 
-        return Promise.all([
-            this.runBulkOperationsOnIndex(PRODUCT_INDEX_NAME, productOperations),
-            this.runBulkOperationsOnIndex(VARIANT_INDEX_NAME, variantOperations),
-        ]);
+        return Promise.all([this.runBulkOperationsOnIndex(VARIANT_INDEX_NAME, variantOperations)]);
     }
 
     private async runBulkOperationsOnIndex(
@@ -775,7 +688,8 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
 
     private createVariantIndexItem(
         v: ProductVariant,
-        channelId: ID,
+        variants: ProductVariant[],
+        ctx: RequestContext,
         languageCode: LanguageCode,
     ): VariantIndexItem {
         const productAsset = v.product.featuredAsset;
@@ -784,8 +698,18 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         const variantTranslation = this.getTranslation(v, languageCode);
         const collectionTranslations = v.collections.map(c => this.getTranslation(c, languageCode));
 
+        const productCollectionTranslations = variants.reduce(
+            (translations, variant) => [
+                ...translations,
+                ...variant.collections.map(c => this.getTranslation(c, languageCode)),
+            ],
+            [] as Array<Translation<Collection>>,
+        );
+        const prices = variants.map(variant => variant.price);
+        const pricesWithTax = variants.map(variant => variant.priceWithTax);
+
         const item: VariantIndexItem = {
-            channelId,
+            channelId: ctx.channelId,
             languageCode,
             productVariantId: v.id,
             sku: v.sku,
@@ -798,7 +722,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
             productVariantName: variantTranslation.name,
             productVariantAssetId: variantAsset ? variantAsset.id : undefined,
             productVariantPreview: variantAsset ? variantAsset.preview : '',
-            productVariantPreviewFocalPoint: productAsset ? productAsset.focalPoint || undefined : undefined,
+            productVariantPreviewFocalPoint: variantAsset ? variantAsset.focalPoint || undefined : undefined,
             price: v.price,
             priceWithTax: v.priceWithTax,
             currencyCode: v.currencyCode,
@@ -809,68 +733,30 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
             collectionIds: v.collections.map(c => c.id.toString()),
             collectionSlugs: collectionTranslations.map(c => c.slug),
             enabled: v.enabled && v.product.enabled,
+            productEnabled: variants.some(variant => variant.enabled) && v.product.enabled,
+            productPriceMin: Math.min(...prices),
+            productPriceMax: Math.max(...prices),
+            productPriceWithTaxMin: Math.min(...pricesWithTax),
+            productPriceWithTaxMax: Math.max(...pricesWithTax),
+            productFacetIds: this.getFacetIds(variants),
+            productFacetValueIds: this.getFacetValueIds(variants),
+            productCollectionIds: unique(
+                variants.reduce(
+                    (ids, variant) => [...ids, ...variant.collections.map(c => c.id)],
+                    [] as ID[],
+                ),
+            ),
+            productCollectionSlugs: unique(productCollectionTranslations.map(c => c.slug)),
+            productChannelIds: v.product.channels.map(c => c.id),
         };
-        const customMappings = Object.entries(this.options.customProductVariantMappings);
-        for (const [name, def] of customMappings) {
-            item[name] = def.valueFn(v, languageCode);
+        const variantCustomMappings = Object.entries(this.options.customProductVariantMappings);
+        for (const [name, def] of variantCustomMappings) {
+            item[`variant-${name}`] = def.valueFn(v, languageCode);
         }
-        return item;
-    }
-
-    private createProductIndexItem(
-        variants: ProductVariant[],
-        channelId: ID,
-        languageCode: LanguageCode,
-    ): ProductIndexItem {
-        const first = variants[0];
-        const prices = variants.map(v => v.price);
-        const pricesWithTax = variants.map(v => v.priceWithTax);
-        const productAsset = first.product.featuredAsset;
-        const variantAsset = variants.filter(v => v.featuredAsset).length
-            ? variants.filter(v => v.featuredAsset)[0].featuredAsset
-            : null;
-        const productTranslation = this.getTranslation(first.product, languageCode);
-        const variantTranslation = this.getTranslation(first, languageCode);
-        const collectionTranslations = variants.reduce(
-            (translations, variant) => [
-                ...translations,
-                ...variant.collections.map(c => this.getTranslation(c, languageCode)),
-            ],
-            [] as Array<Translation<Collection>>,
-        );
-
-        const item: ProductIndexItem = {
-            channelId,
-            languageCode,
-            sku: first.sku,
-            slug: productTranslation.slug,
-            productId: first.product.id,
-            productName: productTranslation.name,
-            productAssetId: productAsset ? productAsset.id : undefined,
-            productPreview: productAsset ? productAsset.preview : '',
-            productPreviewFocalPoint: productAsset ? productAsset.focalPoint || undefined : undefined,
-            productVariantId: first.id,
-            productVariantName: variantTranslation.name,
-            productVariantAssetId: variantAsset ? variantAsset.id : undefined,
-            productVariantPreview: variantAsset ? variantAsset.preview : '',
-            productVariantPreviewFocalPoint: productAsset ? productAsset.focalPoint || undefined : undefined,
-            priceMin: Math.min(...prices),
-            priceMax: Math.max(...prices),
-            priceWithTaxMin: Math.min(...pricesWithTax),
-            priceWithTaxMax: Math.max(...pricesWithTax),
-            currencyCode: first.currencyCode,
-            description: productTranslation.description,
-            facetIds: this.getFacetIds(variants),
-            facetValueIds: this.getFacetValueIds(variants),
-            collectionIds: variants.reduce((ids, v) => [...ids, ...v.collections.map(c => c.id)], [] as ID[]),
-            collectionSlugs: collectionTranslations.map(c => c.slug),
-            channelIds: first.product.channels.map(c => c.id),
-            enabled: variants.some(v => v.enabled) && first.product.enabled,
-        };
 
-        const customMappings = Object.entries(this.options.customProductMappings);
-        for (const [name, def] of customMappings) {
-            item[name] = def.valueFn(variants[0].product, variants, languageCode);
+        const productCustomMappings = Object.entries(this.options.customProductMappings);
+        for (const [name, def] of productCustomMappings) {
+            item[`product-${name}`] = def.valueFn(v.product, variants, languageCode);
         }
         return item;
     }
@@ -880,38 +766,48 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
      * of making that product visible via the search query.
      */
     private createSyntheticProductIndexItem(
-        ctx: RequestContext,
         product: Product,
+        ctx: RequestContext,
         languageCode: LanguageCode,
-    ): ProductIndexItem {
-        const productTranslation = this.getTranslation(product, ctx.languageCode);
+    ): VariantIndexItem {
+        const productTranslation = this.getTranslation(product, languageCode);
+        const productAsset = product.featuredAsset;
+
         return {
             channelId: ctx.channelId,
             languageCode,
+            productVariantId: 0,
             sku: '',
             slug: productTranslation.slug,
             productId: product.id,
             productName: productTranslation.name,
-            productAssetId: product.featuredAsset?.id ?? undefined,
-            productPreview: product.featuredAsset?.preview ?? '',
-            productPreviewFocalPoint: product.featuredAsset?.focalPoint ?? undefined,
-            productVariantId: 0,
+            productAssetId: productAsset ? productAsset.id : undefined,
+            productPreview: productAsset ? productAsset.preview : '',
+            productPreviewFocalPoint: productAsset ? productAsset.focalPoint || undefined : undefined,
             productVariantName: productTranslation.name,
             productVariantAssetId: undefined,
             productVariantPreview: '',
             productVariantPreviewFocalPoint: undefined,
-            priceMin: 0,
-            priceMax: 0,
-            priceWithTaxMin: 0,
-            priceWithTaxMax: 0,
+            price: 0,
+            priceWithTax: 0,
             currencyCode: ctx.channel.currencyCode,
             description: productTranslation.description,
             facetIds: product.facetValues?.map(fv => fv.facet.id.toString()) ?? [],
+            channelIds: [ctx.channelId],
             facetValueIds: product.facetValues?.map(fv => fv.id.toString()) ?? [],
             collectionIds: [],
             collectionSlugs: [],
-            channelIds: [ctx.channelId],
             enabled: false,
+            productEnabled: false,
+            productPriceMin: 0,
+            productPriceMax: 0,
+            productPriceWithTaxMin: 0,
+            productPriceWithTaxMax: 0,
+            productFacetIds: product.facetValues?.map(fv => fv.facet.id.toString()) ?? [],
+            productFacetValueIds: product.facetValues?.map(fv => fv.id.toString()) ?? [],
+            productCollectionIds: [],
+            productCollectionSlugs: [],
+            productChannelIds: product.channels.map(c => c.id),
         };
     }
 
@@ -919,9 +815,9 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         translatable: T,
         languageCode: LanguageCode,
     ): Translation<T> {
-        return ((translatable.translations.find(t => t.languageCode === languageCode) ||
+        return (translatable.translations.find(t => t.languageCode === languageCode) ||
             translatable.translations.find(t => t.languageCode === this.configService.defaultLanguageCode) ||
-            translatable.translations[0]) as unknown) as Translation<T>;
+            translatable.translations[0]) as unknown as Translation<T>;
     }
 
     private getFacetIds(variants: ProductVariant[]): string[] {
@@ -944,7 +840,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         return unique([...variantFacetValueIds, ...productFacetValueIds]);
     }
 
-    private getId(entityId: ID, channelId: ID, languageCode: LanguageCode): string {
+    private static getId(entityId: ID, channelId: ID, languageCode: LanguageCode): string {
         return `${channelId.toString()}_${entityId.toString()}_${languageCode}`;
     }
 }

+ 14 - 46
packages/elasticsearch-plugin/src/indexing-utils.ts

@@ -1,8 +1,8 @@
 import { Client } from '@elastic/elasticsearch';
 import { ID, Logger } from '@vendure/core';
 
-import { loggerCtx, PRODUCT_INDEX_NAME, VARIANT_INDEX_NAME } from './constants';
-import { ProductIndexItem, VariantIndexItem } from './types';
+import { loggerCtx, VARIANT_INDEX_NAME } from './constants';
+import { VariantIndexItem } from './types';
 
 export async function createIndices(
     client: Client,
@@ -23,7 +23,8 @@ export async function createIndices(
         },
     };
     const keyword = { type: 'keyword' };
-    const commonMappings = {
+
+    const variantMappings: { [prop in keyof VariantIndexItem]: any } = {
         sku: textWithKeyword,
         slug: textWithKeyword,
         productId: keyword,
@@ -40,25 +41,22 @@ export async function createIndices(
         collectionSlugs: keyword,
         channelIds: keyword,
         enabled: { type: 'boolean' },
+        productEnabled: { type: 'boolean' },
         productAssetId: keyword,
         productPreview: textWithKeyword,
         productPreviewFocalPoint: { type: 'object' },
         productVariantAssetId: keyword,
         productVariantPreview: textWithKeyword,
         productVariantPreviewFocalPoint: { type: 'object' },
-    };
-
-    const productMappings: { [prop in keyof ProductIndexItem]: any } = {
-        ...commonMappings,
-        priceMin: { type: 'long' },
-        priceMax: { type: 'long' },
-        priceWithTaxMin: { type: 'long' },
-        priceWithTaxMax: { type: 'long' },
-        ...indexMappingProperties,
-    };
-
-    const variantMappings: { [prop in keyof VariantIndexItem]: any } = {
-        ...commonMappings,
+        productChannelIds: keyword,
+        productCollectionIds: keyword,
+        productCollectionSlugs: keyword,
+        productFacetIds: keyword,
+        productFacetValueIds: keyword,
+        productPriceMax: { type: 'long' },
+        productPriceMin: { type: 'long' },
+        productPriceWithTaxMax: { type: 'long' },
+        productPriceWithTaxMin: { type: 'long' },
         price: { type: 'long' },
         priceWithTax: { type: 'long' },
         ...indexMappingProperties,
@@ -104,15 +102,6 @@ export async function createIndices(
     } catch (e) {
         Logger.error(JSON.stringify(e, null, 2), loggerCtx);
     }
-
-    try {
-        const index = prefix + PRODUCT_INDEX_NAME + `${unixtimestampPostfix}`;
-        const alias = prefix + PRODUCT_INDEX_NAME + aliasPostfix;
-
-        await createIndex(productMappings, index, alias);
-    } catch (e) {
-        Logger.error(JSON.stringify(e, null, 2), loggerCtx);
-    }
 }
 
 export async function deleteIndices(client: Client, prefix: string) {
@@ -123,13 +112,6 @@ export async function deleteIndices(client: Client, prefix: string) {
     } catch (e) {
         Logger.error(e, loggerCtx);
     }
-    try {
-        const index = await getIndexNameByAlias(client, prefix + PRODUCT_INDEX_NAME);
-        await client.indices.delete({ index });
-        Logger.verbose(`Deleted index "${index}"`, loggerCtx);
-    } catch (e) {
-        Logger.error(e, loggerCtx);
-    }
 }
 
 export async function deleteByChannel(client: Client, prefix: string, channelId: ID) {
@@ -147,20 +129,6 @@ export async function deleteByChannel(client: Client, prefix: string, channelId:
     } catch (e) {
         Logger.error(e, loggerCtx);
     }
-    try {
-        const index = prefix + PRODUCT_INDEX_NAME;
-        await client.deleteByQuery({
-            index,
-            body: {
-                query: {
-                    match: { channelId },
-                },
-            },
-        });
-        Logger.verbose(`Deleted index "${index}" for channel "${channelId}"`, loggerCtx);
-    } catch (e) {
-        Logger.error(e, loggerCtx);
-    }
 }
 
 export async function getIndexNameByAlias(client: Client, aliasName: string) {

+ 13 - 1
packages/elasticsearch-plugin/src/types.ts

@@ -50,6 +50,16 @@ export type VariantIndexItem = Omit<
         price: number;
         priceWithTax: number;
         collectionSlugs: string[];
+        productEnabled: boolean;
+        productPriceMin: number;
+        productPriceMax: number;
+        productPriceWithTaxMin: number;
+        productPriceWithTaxMax: number;
+        productFacetIds: ID[];
+        productFacetValueIds: ID[];
+        productCollectionIds: ID[];
+        productCollectionSlugs: string[];
+        productChannelIds: ID[];
         [customMapping: string]: any;
     };
 
@@ -70,6 +80,7 @@ export type ProductIndexItem = IndexItemAssets & {
     collectionSlugs: string[];
     channelIds: ID[];
     enabled: boolean;
+    productEnabled: boolean;
     priceMin: number;
     priceMax: number;
     priceWithTaxMin: number;
@@ -92,6 +103,7 @@ export type SearchRequestBody = {
     size?: number;
     track_total_hits?: number | boolean;
     aggs?: any;
+    collapse?: any;
 };
 
 export type SearchResponseBody<T = any> = {
@@ -115,7 +127,7 @@ export type SearchResponseBody<T = any> = {
         [key: string]: {
             doc_count_error_upper_bound: 0;
             sum_other_doc_count: 89;
-            buckets: Array<{ key: string; doc_count: number }>;
+            buckets: Array<{ key: string; doc_count: number; total: { value: number } }>;
             value: any;
         };
     };