default-search-plugin.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import { SearchReindexResponse } from '@vendure/common/lib/generated-types';
  2. import { ID } from '@vendure/common/lib/shared-types';
  3. import { buffer, debounceTime, filter, map } from 'rxjs/operators';
  4. import { idsAreEqual } from '../../common/utils';
  5. import { EventBus } from '../../event-bus/event-bus';
  6. import { AssetEvent } from '../../event-bus/events/asset-event';
  7. import { CollectionModificationEvent } from '../../event-bus/events/collection-modification-event';
  8. import { ProductChannelEvent } from '../../event-bus/events/product-channel-event';
  9. import { ProductEvent } from '../../event-bus/events/product-event';
  10. import { ProductVariantEvent } from '../../event-bus/events/product-variant-event';
  11. import { TaxRateModificationEvent } from '../../event-bus/events/tax-rate-modification-event';
  12. import { PluginCommonModule } from '../plugin-common.module';
  13. import { OnVendureBootstrap, VendurePlugin } from '../vendure-plugin';
  14. import { AdminFulltextSearchResolver, ShopFulltextSearchResolver } from './fulltext-search.resolver';
  15. import { FulltextSearchService } from './fulltext-search.service';
  16. import { IndexerController } from './indexer/indexer.controller';
  17. import { SearchIndexService } from './indexer/search-index.service';
  18. import { SearchIndexItem } from './search-index-item.entity';
  19. export interface DefaultSearchReindexResponse extends SearchReindexResponse {
  20. timeTaken: number;
  21. indexedItemCount: number;
  22. }
  23. /**
  24. * @description
  25. * The DefaultSearchPlugin provides a full-text Product search based on the full-text searching capabilities of the
  26. * underlying database.
  27. *
  28. * The DefaultSearchPlugin is bundled with the `\@vendure/core` package. If you are not using an alternative search
  29. * plugin, then make sure this one is used, otherwise you will not be able to search products via the
  30. * [`search` query](/docs/graphql-api/shop/queries#search).
  31. *
  32. * {{% alert "warning" %}}
  33. * Note that the quality of the fulltext search capabilities varies depending on the underlying database being used. For example,
  34. * the MySQL & Postgres implementations will typically yield better results than the SQLite implementation.
  35. * {{% /alert %}}
  36. *
  37. *
  38. * @example
  39. * ```ts
  40. * import { DefaultSearchPlugin, VendureConfig } from '\@vendure/core';
  41. *
  42. * export const config: VendureConfig = {
  43. * // Add an instance of the plugin to the plugins array
  44. * plugins: [
  45. * DefaultSearchPlugin,
  46. * ],
  47. * };
  48. * ```
  49. *
  50. * @docsCategory DefaultSearchPlugin
  51. */
  52. @VendurePlugin({
  53. imports: [PluginCommonModule],
  54. providers: [FulltextSearchService, SearchIndexService],
  55. adminApiExtensions: { resolvers: [AdminFulltextSearchResolver] },
  56. shopApiExtensions: { resolvers: [ShopFulltextSearchResolver] },
  57. entities: [SearchIndexItem],
  58. workers: [IndexerController],
  59. })
  60. export class DefaultSearchPlugin implements OnVendureBootstrap {
  61. /** @internal */
  62. constructor(private eventBus: EventBus, private searchIndexService: SearchIndexService) {}
  63. /** @internal */
  64. async onVendureBootstrap() {
  65. this.searchIndexService.initJobQueue();
  66. this.eventBus.ofType(ProductEvent).subscribe(event => {
  67. if (event.type === 'deleted') {
  68. return this.searchIndexService.deleteProduct(event.ctx, event.product);
  69. } else {
  70. return this.searchIndexService.updateProduct(event.ctx, event.product);
  71. }
  72. });
  73. this.eventBus.ofType(ProductVariantEvent).subscribe(event => {
  74. if (event.type === 'deleted') {
  75. return this.searchIndexService.deleteVariant(event.ctx, event.variants);
  76. } else {
  77. return this.searchIndexService.updateVariants(event.ctx, event.variants);
  78. }
  79. });
  80. this.eventBus.ofType(AssetEvent).subscribe(event => {
  81. if (event.type === 'updated') {
  82. return this.searchIndexService.updateAsset(event.ctx, event.asset);
  83. }
  84. if (event.type === 'deleted') {
  85. return this.searchIndexService.deleteAsset(event.ctx, event.asset);
  86. }
  87. });
  88. this.eventBus.ofType(ProductChannelEvent).subscribe(event => {
  89. if (event.type === 'assigned') {
  90. return this.searchIndexService.assignProductToChannel(
  91. event.ctx,
  92. event.product.id,
  93. event.channelId,
  94. );
  95. } else {
  96. return this.searchIndexService.removeProductFromChannel(
  97. event.ctx,
  98. event.product.id,
  99. event.channelId,
  100. );
  101. }
  102. });
  103. const collectionModification$ = this.eventBus.ofType(CollectionModificationEvent);
  104. const closingNotifier$ = collectionModification$.pipe(debounceTime(50));
  105. collectionModification$
  106. .pipe(
  107. buffer(closingNotifier$),
  108. filter(events => 0 < events.length),
  109. map(events => ({
  110. ctx: events[0].ctx,
  111. ids: events.reduce((ids, e) => [...ids, ...e.productVariantIds], [] as ID[]),
  112. })),
  113. filter(e => 0 < e.ids.length),
  114. )
  115. .subscribe(events => {
  116. return this.searchIndexService.updateVariantsById(events.ctx, events.ids);
  117. });
  118. this.eventBus.ofType(TaxRateModificationEvent).subscribe(event => {
  119. const defaultTaxZone = event.ctx.channel.defaultTaxZone;
  120. if (defaultTaxZone && idsAreEqual(defaultTaxZone.id, event.taxRate.zone.id)) {
  121. return this.searchIndexService.reindex(event.ctx);
  122. }
  123. });
  124. }
  125. }