Explorar el Código

fix(core): Update search index for all channels on updates

Relates to #629
Michael Bromley hace 5 años
padre
commit
85de52072f

+ 37 - 2
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -63,7 +63,9 @@ import { awaitRunningJobs } from './utils/await-running-jobs';
 
 describe('Default search plugin', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
-        mergeConfig(testConfig, { plugins: [DefaultSearchPlugin, DefaultJobQueuePlugin] }),
+        mergeConfig(testConfig, {
+            plugins: [DefaultSearchPlugin, DefaultJobQueuePlugin],
+        }),
     );
 
     beforeAll(async () => {
@@ -935,6 +937,8 @@ describe('Default search plugin', () => {
                     input: { channelId: secondChannel.id, productVariantIds: ['T_10', 'T_15'] },
                 });
                 await awaitRunningJobs(adminClient);
+                // The postgres test is kinda flaky so we stick in a pause for good measure
+                await new Promise(resolve => setTimeout(resolve, 500));
 
                 adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
 
@@ -952,7 +956,7 @@ describe('Default search plugin', () => {
                 ]);
             }, 10000);
 
-            it('removing product variant to channel', async () => {
+            it('removing product variant from channel', async () => {
                 adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
                 await adminClient.query<
                     RemoveProductVariantsFromChannel.Mutation,
@@ -975,6 +979,37 @@ describe('Default search plugin', () => {
                     'T_10',
                 ]);
             }, 10000);
+
+            it('updating product affects current channel', async () => {
+                adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
+                const { updateProduct } = await adminClient.query<
+                    UpdateProduct.Mutation,
+                    UpdateProduct.Variables
+                >(UPDATE_PRODUCT, {
+                    input: {
+                        id: 'T_3',
+                        enabled: true,
+                        translations: [{ languageCode: LanguageCode.en, name: 'xyz' }],
+                    },
+                });
+
+                await awaitRunningJobs(adminClient);
+
+                const { search: searchGrouped } = await doAdminSearchQuery({
+                    groupByProduct: true,
+                    term: 'xyz',
+                });
+                expect(searchGrouped.items.map(i => i.productName)).toEqual(['xyz']);
+            });
+
+            it('updating product affects other channels', async () => {
+                adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+                const { search: searchGrouped } = await doAdminSearchQuery({
+                    groupByProduct: true,
+                    term: 'xyz',
+                });
+                expect(searchGrouped.items.map(i => i.productName)).toEqual(['xyz']);
+            });
         });
 
         describe('multiple language handling', () => {

+ 67 - 56
packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts

@@ -9,12 +9,12 @@ import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
 import { RequestContext } from '../../../api/common/request-context';
 import { AsyncQueue } from '../../../common/async-queue';
 import { Translatable, Translation } from '../../../common/types/locale-types';
+import { idsAreEqual } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
 import { Logger } from '../../../config/logger/vendure-logger';
 import { FacetValue } from '../../../entity/facet-value/facet-value.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { Product } from '../../../entity/product/product.entity';
-import { translateDeep } from '../../../service/helpers/utils/translate-entity';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { TransactionalConnection } from '../../../service/transaction/transactional-connection';
 import { asyncObservable } from '../../../worker/async-observable';
@@ -47,6 +47,7 @@ export const variantRelations = [
     'collections',
     'taxCategory',
     'channels',
+    'channels.defaultTaxZone',
 ];
 
 export const workerLoggerCtx = 'DefaultSearchPlugin Worker';
@@ -84,8 +85,7 @@ export class IndexerController {
                     .take(BATCH_SIZE)
                     .skip(i * BATCH_SIZE)
                     .getMany();
-                const hydratedVariants = this.hydrateVariants(ctx, variants);
-                await this.saveVariants(ctx.channelId, hydratedVariants);
+                await this.saveVariants(variants);
                 observer.next({
                     total: count,
                     completed: Math.min((i + 1) * BATCH_SIZE, count),
@@ -123,8 +123,7 @@ export class IndexerController {
                         relations: variantRelations,
                         where: { deletedAt: null },
                     });
-                    const variants = this.hydrateVariants(ctx, batch);
-                    await this.saveVariants(ctx.channelId, variants);
+                    await this.saveVariants(batch);
                     observer.next({
                         total: ids.length,
                         completed: Math.min((i + 1) * BATCH_SIZE, ids.length),
@@ -263,7 +262,7 @@ export class IndexerController {
             relations: ['variants'],
         });
         if (product) {
-            let updatedVariants = await this.connection.getRepository(ProductVariant).findByIds(
+            const updatedVariants = await this.connection.getRepository(ProductVariant).findByIds(
                 product.variants.map(v => v.id),
                 {
                     relations: variantRelations,
@@ -273,10 +272,12 @@ export class IndexerController {
             if (product.enabled === false) {
                 updatedVariants.forEach(v => (v.enabled = false));
             }
-            Logger.verbose(`Updating ${updatedVariants.length} variants`, workerLoggerCtx);
-            updatedVariants = this.hydrateVariants(ctx, updatedVariants);
-            if (updatedVariants.length) {
-                await this.saveVariants(channelId, updatedVariants);
+            const variantsInCurrentChannel = updatedVariants.filter(
+                v => !!v.channels.find(c => idsAreEqual(c.id, ctx.channelId)),
+            );
+            Logger.verbose(`Updating ${variantsInCurrentChannel.length} variants`, workerLoggerCtx);
+            if (variantsInCurrentChannel.length) {
+                await this.saveVariants(variantsInCurrentChannel);
             }
         }
         return true;
@@ -292,9 +293,8 @@ export class IndexerController {
             where: { deletedAt: null },
         });
         if (variants) {
-            const updatedVariants = this.hydrateVariants(ctx, variants);
-            Logger.verbose(`Updating ${updatedVariants.length} variants`, workerLoggerCtx);
-            await this.saveVariants(channelId, updatedVariants);
+            Logger.verbose(`Updating ${variants.length} variants`, workerLoggerCtx);
+            await this.saveVariants(variants);
         }
         return true;
     }
@@ -334,55 +334,66 @@ export class IndexerController {
         return qb;
     }
 
-    /**
-     * Given an array of ProductVariants, this method applies the correct taxes and translations.
-     */
-    private hydrateVariants(ctx: RequestContext, variants: ProductVariant[]): ProductVariant[] {
-        return variants
-            .map(v => this.productVariantService.applyChannelPriceAndTax(v, ctx))
-            .map(v => translateDeep(v, ctx.languageCode, ['product', 'collections']));
-    }
-
-    private async saveVariants(channelId: ID, variants: ProductVariant[]) {
+    private async saveVariants(variants: ProductVariant[]) {
         const items: SearchIndexItem[] = [];
 
-        for (const v of variants) {
+        for (const variant of variants) {
             const languageVariants = unique([
-                ...v.translations.map(t => t.languageCode),
-                ...v.product.translations.map(t => t.languageCode),
+                ...variant.translations.map(t => t.languageCode),
+                ...variant.product.translations.map(t => t.languageCode),
             ]);
             for (const languageCode of languageVariants) {
-                const productTranslation = this.getTranslation(v.product, languageCode);
-                const variantTranslation = this.getTranslation(v, languageCode);
-                items.push(
-                    new SearchIndexItem({
-                        productVariantId: v.id,
-                        channelId,
-                        languageCode,
-                        sku: v.sku,
-                        enabled: v.product.enabled === false ? false : v.enabled,
-                        slug: productTranslation.slug,
-                        price: v.price,
-                        priceWithTax: v.priceWithTax,
-                        productId: v.product.id,
-                        productName: productTranslation.name,
-                        description: productTranslation.description,
-                        productVariantName: variantTranslation.name,
-                        productAssetId: v.product.featuredAsset ? v.product.featuredAsset.id : null,
-                        productPreviewFocalPoint: v.product.featuredAsset
-                            ? v.product.featuredAsset.focalPoint
-                            : null,
-                        productVariantPreviewFocalPoint: v.featuredAsset ? v.featuredAsset.focalPoint : null,
-                        productVariantAssetId: v.featuredAsset ? v.featuredAsset.id : null,
-                        productPreview: v.product.featuredAsset ? v.product.featuredAsset.preview : '',
-                        productVariantPreview: v.featuredAsset ? v.featuredAsset.preview : '',
-                        channelIds: v.channels.map(c => c.id as string),
-                        facetIds: this.getFacetIds(v),
-                        facetValueIds: this.getFacetValueIds(v),
-                        collectionIds: v.collections.map(c => c.id.toString()),
-                        collectionSlugs: v.collections.map(c => c.slug),
-                    }),
+                const productTranslation = this.getTranslation(variant.product, languageCode);
+                const variantTranslation = this.getTranslation(variant, languageCode);
+                const collectionTranslations = variant.collections.map(c =>
+                    this.getTranslation(c, languageCode),
                 );
+
+                for (const channel of variant.channels) {
+                    const ctx = new RequestContext({
+                        channel,
+                        apiType: 'admin',
+                        authorizedAsOwnerOnly: false,
+                        isAuthorized: true,
+                        session: {} as any,
+                    });
+                    this.productVariantService.applyChannelPriceAndTax(variant, ctx);
+                    items.push(
+                        new SearchIndexItem({
+                            channelId: channel.id,
+                            languageCode,
+                            productVariantId: variant.id,
+                            price: variant.price,
+                            priceWithTax: variant.priceWithTax,
+                            sku: variant.sku,
+                            enabled: variant.product.enabled === false ? false : variant.enabled,
+                            slug: productTranslation.slug,
+                            productId: variant.product.id,
+                            productName: productTranslation.name,
+                            description: productTranslation.description,
+                            productVariantName: variantTranslation.name,
+                            productAssetId: variant.product.featuredAsset
+                                ? variant.product.featuredAsset.id
+                                : null,
+                            productPreviewFocalPoint: variant.product.featuredAsset
+                                ? variant.product.featuredAsset.focalPoint
+                                : null,
+                            productVariantPreviewFocalPoint: variant.featuredAsset
+                                ? variant.featuredAsset.focalPoint
+                                : null,
+                            productVariantAssetId: variant.featuredAsset ? variant.featuredAsset.id : null,
+                            productPreview: variant.product.featuredAsset
+                                ? variant.product.featuredAsset.preview
+                                : '',
+                            productVariantPreview: variant.featuredAsset ? variant.featuredAsset.preview : '',
+                            channelIds: variant.channels.map(c => c.id as string),
+                            facetIds: this.getFacetIds(variant),
+                            facetValueIds: this.getFacetValueIds(variant),
+                            collectionIds: variant.collections.map(c => c.id.toString()),
+                            collectionSlugs: collectionTranslations.map(c => c.slug),
+                        }),
+                    );
+                }
             }
         }
 

+ 10 - 5
packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts

@@ -124,11 +124,16 @@ export class MysqlSearchStrategy implements SearchStrategy {
                      MATCH (description) AGAINST (:term)* 1`,
                     'score',
                 )
-                .where('sku LIKE :like_term')
-                .orWhere('MATCH (productName) AGAINST (:term)')
-                .orWhere('MATCH (productVariantName) AGAINST (:term)')
-                .orWhere('MATCH (description) AGAINST (:term)')
-                .setParameters({ term, like_term: `%${term}%` });
+                .where(
+                    new Brackets(qb1 => {
+                        qb1.where('sku LIKE :like_term')
+                            .orWhere('MATCH (productName) AGAINST (:term)')
+                            .orWhere('MATCH (productVariantName) AGAINST (:term)')
+                            .orWhere('MATCH (description) AGAINST (:term)');
+                    }),
+                )
+                .andWhere('channelId = :channelId')
+                .setParameters({ term, like_term: `%${term}%`, channelId: ctx.channelId });
 
             qb.innerJoin(`(${termScoreQuery.getQuery()})`, 'term_result', 'inner_productId = si.productId')
                 .addSelect(input.groupByProduct ? 'MAX(term_result.score)' : 'term_result.score', 'score')