Browse Source

feat(server): Remove deleted products from search index

Michael Bromley 7 years ago
parent
commit
358fd72daf

File diff suppressed because it is too large
+ 0 - 0
schema.json


+ 1 - 1
server/src/api/resolvers/product.resolver.ts

@@ -91,7 +91,7 @@ export class ProductResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: DeleteProductMutationArgs,
     ): Promise<DeletionResponse> {
-        return this.productService.softDelete(args.id);
+        return this.productService.softDelete(ctx, args.id);
     }
 
     @Mutation()

+ 1 - 1
server/src/api/resolvers/search.resolver.ts

@@ -22,7 +22,7 @@ export class SearchResolver {
 
     @Mutation()
     @Allow(Permission.UpdateCatalog)
-    async reindex(...args: any[]): Promise<boolean> {
+    async reindex(...args: any[]): Promise<{ success: boolean } & { [key: string]: any }> {
         throw new InternalServerError(`error.no-search-plugin-configured`);
     }
 }

+ 5 - 1
server/src/api/types/search.api.graphql

@@ -3,7 +3,11 @@ type Query {
 }
 
 type Mutation {
-    reindex: Boolean!
+    reindex: SearchReindexResponse!
+}
+
+type SearchReindexResponse {
+    success: Boolean!
 }
 
 input SearchInput {

+ 2 - 2
server/src/event-bus/events/catalog-modification-event.ts

@@ -1,9 +1,9 @@
 import { RequestContext } from '../../api/common/request-context';
-import { VendureEntity } from '../../entity';
+import { Product, ProductVariant } from '../../entity';
 import { VendureEvent } from '../vendure-event';
 
 export class CatalogModificationEvent extends VendureEvent {
-    constructor(public ctx: RequestContext, public entity: VendureEntity) {
+    constructor(public ctx: RequestContext, public entity: Product | ProductVariant) {
         super();
     }
 }

+ 18 - 0
server/src/plugin/default-search-plugin/default-search-plugin.ts

@@ -1,3 +1,7 @@
+import { DocumentNode } from 'graphql';
+import gql from 'graphql-tag';
+
+import { SearchReindexResponse } from '../../../../shared/generated-types';
 import { Type } from '../../../../shared/shared-types';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { InjectorFn, VendureConfig, VendurePlugin } from '../../config';
@@ -6,6 +10,11 @@ import { FulltextSearchResolver } from './fulltext-search.resolver';
 import { FulltextSearchService } from './fulltext-search.service';
 import { SearchIndexItem } from './search-index-item.entity';
 
+export interface DefaultSearceReindexResonse extends SearchReindexResponse {
+    timeTaken: number;
+    indexedItemCount: number;
+}
+
 export class DefaultSearchPlugin implements VendurePlugin {
     private fulltextSearchService: FulltextSearchService;
 
@@ -18,6 +27,15 @@ export class DefaultSearchPlugin implements VendurePlugin {
         await searchService.checkIndex(DEFAULT_LANGUAGE_CODE);
     }
 
+    defineGraphQlTypes(): DocumentNode {
+        return gql`
+            extend type SearchReindexResponse {
+                timeTaken: Int!
+                indexedItemCount: Int!
+            }
+        `;
+    }
+
     defineEntities(): Array<Type<any>> {
         return [SearchIndexItem];
     }

+ 2 - 1
server/src/plugin/default-search-plugin/fulltext-search.resolver.ts

@@ -9,6 +9,7 @@ import { SearchResolver as BaseSearchResolver } from '../../api/resolvers/search
 import { Translated } from '../../common/types/locale-types';
 import { FacetValue } from '../../entity';
 
+import { DefaultSearceReindexResonse } from './default-search-plugin';
 import { FulltextSearchService } from './fulltext-search.service';
 
 @Resolver('SearchResponse')
@@ -36,7 +37,7 @@ export class FulltextSearchResolver extends BaseSearchResolver {
 
     @Mutation()
     @Allow(Permission.UpdateCatalog)
-    async reindex(@Ctx() ctx: RequestContext): Promise<boolean> {
+    async reindex(@Ctx() ctx: RequestContext): Promise<DefaultSearceReindexResonse> {
         return this.fulltextSearchService.reindex(ctx.languageCode);
     }
 }

+ 49 - 10
server/src/plugin/default-search-plugin/fulltext-search.service.ts

@@ -1,9 +1,11 @@
 import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import { Brackets, Connection, Like, SelectQueryBuilder } from 'typeorm';
+import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
 
 import { LanguageCode, SearchInput, SearchResponse } from '../../../../shared/generated-types';
 import { Omit } from '../../../../shared/omit';
+import { ID } from '../../../../shared/shared-types';
 import { unique } from '../../../../shared/unique';
 import { RequestContext } from '../../api/common/request-context';
 import { Translated } from '../../common/types/locale-types';
@@ -13,6 +15,7 @@ import { CatalogModificationEvent } from '../../event-bus/events/catalog-modific
 import { translateDeep } from '../../service/helpers/utils/translate-entity';
 import { FacetValueService } from '../../service/services/facet-value.service';
 
+import { DefaultSearceReindexResonse } from './default-search-plugin';
 import { SearchIndexItem } from './search-index-item.entity';
 
 /**
@@ -112,35 +115,57 @@ export class FulltextSearchService {
     /**
      * Rebuilds the full search index.
      */
-    async reindex(languageCode: LanguageCode): Promise<boolean> {
-        const variants = await this.connection.getRepository(ProductVariant).find({
+    async reindex(languageCode: LanguageCode): Promise<DefaultSearceReindexResonse> {
+        const timeStart = Date.now();
+        const qb = await this.connection.getRepository(ProductVariant).createQueryBuilder('variants');
+        FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, {
             relations: this.variantRelations,
         });
-
+        const variants = await qb.where('variants_product.deletedAt IS NULL').getMany();
         await this.connection.getRepository(SearchIndexItem).delete({ languageCode });
         await this.saveSearchIndexItems(languageCode, variants);
-        return true;
+        return {
+            success: true,
+            indexedItemCount: variants.length,
+            timeTaken: Date.now() - timeStart,
+        };
     }
 
     /**
      * Updates the search index only for the affected entities.
      */
     async update(ctx: RequestContext, updatedEntity: Product | ProductVariant) {
-        let variants: ProductVariant[] = [];
+        let updatedVariants: ProductVariant[] = [];
+        let removedVariantIds: ID[] = [];
         if (updatedEntity instanceof Product) {
-            variants = await this.connection.getRepository(ProductVariant).find({
-                relations: this.variantRelations,
-                where: { product: { id: updatedEntity.id } },
+            const product = await this.connection.getRepository(Product).findOne(updatedEntity.id, {
+                relations: ['variants'],
             });
+            if (product) {
+                if (product.deletedAt) {
+                    removedVariantIds = product.variants.map(v => v.id);
+                } else {
+                    updatedVariants = await this.connection
+                        .getRepository(ProductVariant)
+                        .findByIds(product.variants.map(v => v.id), {
+                            relations: this.variantRelations,
+                        });
+                }
+            }
         } else {
             const variant = await this.connection.getRepository(ProductVariant).findOne(updatedEntity.id, {
                 relations: this.variantRelations,
             });
             if (variant) {
-                variants = [variant];
+                updatedVariants = [variant];
             }
         }
-        await this.saveSearchIndexItems(ctx.languageCode, variants);
+        if (updatedVariants.length) {
+            await this.saveSearchIndexItems(ctx.languageCode, updatedVariants);
+        }
+        if (removedVariantIds.length) {
+            await this.removeSearchIndexItems(ctx.languageCode, removedVariantIds);
+        }
     }
 
     /**
@@ -195,6 +220,9 @@ export class FulltextSearchService {
         return qb;
     }
 
+    /**
+     * Add or update items in the search index
+     */
     private async saveSearchIndexItems(languageCode: LanguageCode, variants: ProductVariant[]) {
         const items = variants
             .map(v => translateDeep(v, languageCode, ['product']))
@@ -217,6 +245,17 @@ export class FulltextSearchService {
         await this.connection.getRepository(SearchIndexItem).save(items);
     }
 
+    /**
+     * Remove items from the search index
+     */
+    private async removeSearchIndexItems(languageCode: LanguageCode, variantIds: ID[]) {
+        const compositeKeys = variantIds.map(id => ({
+            productVariantId: id,
+            languageCode,
+        })) as any[];
+        await this.connection.getRepository(SearchIndexItem).delete(compositeKeys);
+    }
+
     private getFacetIds(variant: ProductVariant): string[] {
         const facetIds = (fv: FacetValue) => fv.facet.id.toString();
         const variantFacetIds = variant.facetValues.map(facetIds);

+ 5 - 1
server/src/service/services/product-variant.service.ts

@@ -17,6 +17,8 @@ import { ProductOption } from '../../entity/product-option/product-option.entity
 import { ProductVariantTranslation } from '../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Product } from '../../entity/product/product.entity';
+import { EventBus } from '../../event-bus/event-bus';
+import { CatalogModificationEvent } from '../../event-bus/events/catalog-modification-event';
 import { AssetUpdater } from '../helpers/asset-updater/asset-updater';
 import { TaxCalculator } from '../helpers/tax-calculator/tax-calculator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
@@ -40,6 +42,7 @@ export class ProductVariantService {
         private assetUpdater: AssetUpdater,
         private zoneService: ZoneService,
         private translatableSaver: TranslatableSaver,
+        private eventBus: EventBus,
     ) {}
 
     findOne(ctx: RequestContext, productVariantId: ID): Promise<Translated<ProductVariant> | undefined> {
@@ -147,6 +150,7 @@ export class ProductVariantService {
                 ],
             }),
         );
+        this.eventBus.publish(new CatalogModificationEvent(ctx, variant));
         return translateDeep(this.applyChannelPriceAndTax(variant, ctx), DEFAULT_LANGUAGE_CODE, [
             'options',
             'facetValues',
@@ -201,7 +205,7 @@ export class ProductVariantService {
             });
             variants.push(variant);
         }
-
+        this.eventBus.publish(new CatalogModificationEvent(ctx, product));
         return variants.map(v => translateDeep(v, DEFAULT_LANGUAGE_CODE));
     }
 

+ 4 - 2
server/src/service/services/product.service.ts

@@ -117,6 +117,7 @@ export class ProductService {
                 await this.assetUpdater.updateEntityAssets(p, input);
             },
         });
+        this.eventBus.publish(new CatalogModificationEvent(ctx, product));
         return assertFound(this.findOne(ctx, product.id));
     }
 
@@ -137,9 +138,10 @@ export class ProductService {
         return assertFound(this.findOne(ctx, product.id));
     }
 
-    async softDelete(productId: ID): Promise<DeletionResponse> {
-        await getEntityOrThrow(this.connection, Product, productId);
+    async softDelete(ctx: RequestContext, productId: ID): Promise<DeletionResponse> {
+        const product = await getEntityOrThrow(this.connection, Product, productId);
         await this.connection.getRepository(Product).update({ id: productId }, { deletedAt: new Date() });
+        this.eventBus.publish(new CatalogModificationEvent(ctx, product));
         return {
             result: DeletionResult.DELETED,
         };

+ 36 - 3
shared/generated-types.ts

@@ -724,7 +724,7 @@ export interface Mutation {
     deletePromotion: DeletionResponse;
     createRole: Role;
     updateRole: Role;
-    reindex: boolean;
+    reindex: SearchReindexResponse;
     createShippingMethod: ShippingMethod;
     updateShippingMethod: ShippingMethod;
     createTaxCategory: TaxCategory;
@@ -758,6 +758,10 @@ export interface ImportInfo {
     imported: number;
 }
 
+export interface SearchReindexResponse {
+    success: boolean;
+}
+
 export interface AdministratorListOptions {
     take?: number | null;
     skip?: number | null;
@@ -4472,7 +4476,7 @@ export namespace MutationResolvers {
         deletePromotion?: DeletePromotionResolver<DeletionResponse, any, Context>;
         createRole?: CreateRoleResolver<Role, any, Context>;
         updateRole?: UpdateRoleResolver<Role, any, Context>;
-        reindex?: ReindexResolver<boolean, any, Context>;
+        reindex?: ReindexResolver<SearchReindexResponse, any, Context>;
         createShippingMethod?: CreateShippingMethodResolver<ShippingMethod, any, Context>;
         updateShippingMethod?: UpdateShippingMethodResolver<ShippingMethod, any, Context>;
         createTaxCategory?: CreateTaxCategoryResolver<TaxCategory, any, Context>;
@@ -5066,7 +5070,11 @@ export namespace MutationResolvers {
         input: UpdateRoleInput;
     }
 
-    export type ReindexResolver<R = boolean, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type ReindexResolver<R = SearchReindexResponse, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
     export type CreateShippingMethodResolver<R = ShippingMethod, Parent = any, Context = any> = Resolver<
         R,
         Parent,
@@ -5258,6 +5266,14 @@ export namespace ImportInfoResolvers {
     export type ImportedResolver<R = number, Parent = any, Context = any> = Resolver<R, Parent, Context>;
 }
 
+export namespace SearchReindexResponseResolvers {
+    export interface Resolvers<Context = any> {
+        success?: SuccessResolver<boolean, any, Context>;
+    }
+
+    export type SuccessResolver<R = boolean, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+}
+
 export namespace GetAdministrators {
     export type Variables = {
         options?: AdministratorListOptions | null;
@@ -5836,6 +5852,23 @@ export namespace CreateProduct {
     export type CreateProduct = ProductWithVariants.Fragment;
 }
 
+export namespace DeleteProduct {
+    export type Variables = {
+        id: string;
+    };
+
+    export type Mutation = {
+        __typename?: 'Mutation';
+        deleteProduct: DeleteProduct;
+    };
+
+    export type DeleteProduct = {
+        __typename?: 'DeletionResponse';
+        result: DeletionResult;
+        message?: string | null;
+    };
+}
+
 export namespace GenerateProductVariants {
     export type Variables = {
         productId: string;

Some files were not shown because too many files changed in this diff