@open-mercato/search 0.4.2-canary-c02407ff85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/AGENTS.md +678 -0
  2. package/build.mjs +92 -0
  3. package/dist/di.js +157 -0
  4. package/dist/di.js.map +7 -0
  5. package/dist/fulltext/drivers/index.js +21 -0
  6. package/dist/fulltext/drivers/index.js.map +7 -0
  7. package/dist/fulltext/drivers/meilisearch/index.js +320 -0
  8. package/dist/fulltext/drivers/meilisearch/index.js.map +7 -0
  9. package/dist/fulltext/index.js +7 -0
  10. package/dist/fulltext/index.js.map +7 -0
  11. package/dist/fulltext/types.js +1 -0
  12. package/dist/fulltext/types.js.map +7 -0
  13. package/dist/index.js +12 -0
  14. package/dist/index.js.map +7 -0
  15. package/dist/indexer/index.js +8 -0
  16. package/dist/indexer/index.js.map +7 -0
  17. package/dist/indexer/search-indexer.js +848 -0
  18. package/dist/indexer/search-indexer.js.map +7 -0
  19. package/dist/indexer/subscribers/delete.js +41 -0
  20. package/dist/indexer/subscribers/delete.js.map +7 -0
  21. package/dist/lib/debug.js +34 -0
  22. package/dist/lib/debug.js.map +7 -0
  23. package/dist/lib/fallback-presenter.js +107 -0
  24. package/dist/lib/fallback-presenter.js.map +7 -0
  25. package/dist/lib/field-policy.js +75 -0
  26. package/dist/lib/field-policy.js.map +7 -0
  27. package/dist/lib/index.js +19 -0
  28. package/dist/lib/index.js.map +7 -0
  29. package/dist/lib/merger.js +93 -0
  30. package/dist/lib/merger.js.map +7 -0
  31. package/dist/lib/presenter-enricher.js +192 -0
  32. package/dist/lib/presenter-enricher.js.map +7 -0
  33. package/dist/modules/search/acl.js +14 -0
  34. package/dist/modules/search/acl.js.map +7 -0
  35. package/dist/modules/search/ai-tools.js +284 -0
  36. package/dist/modules/search/ai-tools.js.map +7 -0
  37. package/dist/modules/search/api/embeddings/reindex/cancel/route.js +65 -0
  38. package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +7 -0
  39. package/dist/modules/search/api/embeddings/reindex/route.js +165 -0
  40. package/dist/modules/search/api/embeddings/reindex/route.js.map +7 -0
  41. package/dist/modules/search/api/embeddings/route.js +246 -0
  42. package/dist/modules/search/api/embeddings/route.js.map +7 -0
  43. package/dist/modules/search/api/index/route.js +245 -0
  44. package/dist/modules/search/api/index/route.js.map +7 -0
  45. package/dist/modules/search/api/reindex/cancel/route.js +65 -0
  46. package/dist/modules/search/api/reindex/cancel/route.js.map +7 -0
  47. package/dist/modules/search/api/reindex/route.js +332 -0
  48. package/dist/modules/search/api/reindex/route.js.map +7 -0
  49. package/dist/modules/search/api/search/global/route.js +100 -0
  50. package/dist/modules/search/api/search/global/route.js.map +7 -0
  51. package/dist/modules/search/api/search/route.js +101 -0
  52. package/dist/modules/search/api/search/route.js.map +7 -0
  53. package/dist/modules/search/api/settings/fulltext/route.js +55 -0
  54. package/dist/modules/search/api/settings/fulltext/route.js.map +7 -0
  55. package/dist/modules/search/api/settings/global-search/route.js +80 -0
  56. package/dist/modules/search/api/settings/global-search/route.js.map +7 -0
  57. package/dist/modules/search/api/settings/route.js +118 -0
  58. package/dist/modules/search/api/settings/route.js.map +7 -0
  59. package/dist/modules/search/api/settings/vector-store/route.js +77 -0
  60. package/dist/modules/search/api/settings/vector-store/route.js.map +7 -0
  61. package/dist/modules/search/backend/config/search/page.js +10 -0
  62. package/dist/modules/search/backend/config/search/page.js.map +7 -0
  63. package/dist/modules/search/backend/config/search/page.meta.js +24 -0
  64. package/dist/modules/search/backend/config/search/page.meta.js.map +7 -0
  65. package/dist/modules/search/cli.js +698 -0
  66. package/dist/modules/search/cli.js.map +7 -0
  67. package/dist/modules/search/di.js +32 -0
  68. package/dist/modules/search/di.js.map +7 -0
  69. package/dist/modules/search/frontend/components/GlobalSearchDialog.js +357 -0
  70. package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +7 -0
  71. package/dist/modules/search/frontend/components/HybridSearchTable.js +343 -0
  72. package/dist/modules/search/frontend/components/HybridSearchTable.js.map +7 -0
  73. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +303 -0
  74. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +7 -0
  75. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +360 -0
  76. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +7 -0
  77. package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js +101 -0
  78. package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js.map +7 -0
  79. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +608 -0
  80. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +7 -0
  81. package/dist/modules/search/frontend/index.js +9 -0
  82. package/dist/modules/search/frontend/index.js.map +7 -0
  83. package/dist/modules/search/frontend/utils.js +41 -0
  84. package/dist/modules/search/frontend/utils.js.map +7 -0
  85. package/dist/modules/search/i18n/de.json +61 -0
  86. package/dist/modules/search/i18n/en.json +72 -0
  87. package/dist/modules/search/i18n/es.json +61 -0
  88. package/dist/modules/search/i18n/pl.json +61 -0
  89. package/dist/modules/search/index.js +11 -0
  90. package/dist/modules/search/index.js.map +7 -0
  91. package/dist/modules/search/lib/auto-indexing.js +29 -0
  92. package/dist/modules/search/lib/auto-indexing.js.map +7 -0
  93. package/dist/modules/search/lib/embedding-config.js +131 -0
  94. package/dist/modules/search/lib/embedding-config.js.map +7 -0
  95. package/dist/modules/search/lib/global-search-config.js +45 -0
  96. package/dist/modules/search/lib/global-search-config.js.map +7 -0
  97. package/dist/modules/search/lib/reindex-lock.js +99 -0
  98. package/dist/modules/search/lib/reindex-lock.js.map +7 -0
  99. package/dist/modules/search/subscribers/fulltext_upsert.js +64 -0
  100. package/dist/modules/search/subscribers/fulltext_upsert.js.map +7 -0
  101. package/dist/modules/search/subscribers/vector_delete.js +58 -0
  102. package/dist/modules/search/subscribers/vector_delete.js.map +7 -0
  103. package/dist/modules/search/subscribers/vector_purge.js +142 -0
  104. package/dist/modules/search/subscribers/vector_purge.js.map +7 -0
  105. package/dist/modules/search/subscribers/vector_upsert.js +58 -0
  106. package/dist/modules/search/subscribers/vector_upsert.js.map +7 -0
  107. package/dist/modules/search/workers/fulltext-index.worker.js +240 -0
  108. package/dist/modules/search/workers/fulltext-index.worker.js.map +7 -0
  109. package/dist/modules/search/workers/vector-index.worker.js +234 -0
  110. package/dist/modules/search/workers/vector-index.worker.js.map +7 -0
  111. package/dist/queue/fulltext-indexing.js +15 -0
  112. package/dist/queue/fulltext-indexing.js.map +7 -0
  113. package/dist/queue/index.js +3 -0
  114. package/dist/queue/index.js.map +7 -0
  115. package/dist/queue/vector-indexing.js +15 -0
  116. package/dist/queue/vector-indexing.js.map +7 -0
  117. package/dist/service.js +286 -0
  118. package/dist/service.js.map +7 -0
  119. package/dist/strategies/fulltext.strategy.js +116 -0
  120. package/dist/strategies/fulltext.strategy.js.map +7 -0
  121. package/dist/strategies/index.js +12 -0
  122. package/dist/strategies/index.js.map +7 -0
  123. package/dist/strategies/token.strategy.js +80 -0
  124. package/dist/strategies/token.strategy.js.map +7 -0
  125. package/dist/strategies/vector.strategy.js +137 -0
  126. package/dist/strategies/vector.strategy.js.map +7 -0
  127. package/dist/types.js +1 -0
  128. package/dist/types.js.map +7 -0
  129. package/dist/vector/drivers/chromadb/index.js +44 -0
  130. package/dist/vector/drivers/chromadb/index.js.map +7 -0
  131. package/dist/vector/drivers/index.js +9 -0
  132. package/dist/vector/drivers/index.js.map +7 -0
  133. package/dist/vector/drivers/pgvector/index.js +509 -0
  134. package/dist/vector/drivers/pgvector/index.js.map +7 -0
  135. package/dist/vector/drivers/qdrant/index.js +44 -0
  136. package/dist/vector/drivers/qdrant/index.js.map +7 -0
  137. package/dist/vector/index.js +4 -0
  138. package/dist/vector/index.js.map +7 -0
  139. package/dist/vector/lib/vector-logs.js +33 -0
  140. package/dist/vector/lib/vector-logs.js.map +7 -0
  141. package/dist/vector/services/checksum.js +20 -0
  142. package/dist/vector/services/checksum.js.map +7 -0
  143. package/dist/vector/services/embedding.js +222 -0
  144. package/dist/vector/services/embedding.js.map +7 -0
  145. package/dist/vector/services/index.js +4 -0
  146. package/dist/vector/services/index.js.map +7 -0
  147. package/dist/vector/services/vector-index.service.js +960 -0
  148. package/dist/vector/services/vector-index.service.js.map +7 -0
  149. package/dist/vector/types/pg.d.js +1 -0
  150. package/dist/vector/types/pg.d.js.map +7 -0
  151. package/dist/vector/types.js +75 -0
  152. package/dist/vector/types.js.map +7 -0
  153. package/jest.config.cjs +19 -0
  154. package/package.json +142 -0
  155. package/src/__tests__/queue.test.ts +148 -0
  156. package/src/__tests__/service.test.ts +345 -0
  157. package/src/__tests__/workers.test.ts +319 -0
  158. package/src/di.ts +291 -0
  159. package/src/fulltext/drivers/index.ts +41 -0
  160. package/src/fulltext/drivers/meilisearch/index.ts +410 -0
  161. package/src/fulltext/index.ts +13 -0
  162. package/src/fulltext/types.ts +115 -0
  163. package/src/index.ts +36 -0
  164. package/src/indexer/index.ts +13 -0
  165. package/src/indexer/search-indexer.ts +1141 -0
  166. package/src/indexer/subscribers/delete.ts +49 -0
  167. package/src/lib/debug.ts +46 -0
  168. package/src/lib/fallback-presenter.ts +106 -0
  169. package/src/lib/field-policy.ts +169 -0
  170. package/src/lib/index.ts +13 -0
  171. package/src/lib/merger.ts +159 -0
  172. package/src/lib/presenter-enricher.ts +323 -0
  173. package/src/modules/search/README.md +694 -0
  174. package/src/modules/search/acl.ts +10 -0
  175. package/src/modules/search/ai-tools.ts +467 -0
  176. package/src/modules/search/api/embeddings/reindex/cancel/route.ts +77 -0
  177. package/src/modules/search/api/embeddings/reindex/route.ts +197 -0
  178. package/src/modules/search/api/embeddings/route.ts +304 -0
  179. package/src/modules/search/api/index/route.ts +297 -0
  180. package/src/modules/search/api/reindex/cancel/route.ts +77 -0
  181. package/src/modules/search/api/reindex/route.ts +419 -0
  182. package/src/modules/search/api/search/global/route.ts +120 -0
  183. package/src/modules/search/api/search/route.ts +121 -0
  184. package/src/modules/search/api/settings/fulltext/route.ts +82 -0
  185. package/src/modules/search/api/settings/global-search/route.ts +91 -0
  186. package/src/modules/search/api/settings/route.ts +187 -0
  187. package/src/modules/search/api/settings/vector-store/route.ts +105 -0
  188. package/src/modules/search/backend/config/search/page.meta.ts +22 -0
  189. package/src/modules/search/backend/config/search/page.tsx +12 -0
  190. package/src/modules/search/cli.ts +818 -0
  191. package/src/modules/search/di.ts +50 -0
  192. package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +436 -0
  193. package/src/modules/search/frontend/components/HybridSearchTable.tsx +418 -0
  194. package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +476 -0
  195. package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +624 -0
  196. package/src/modules/search/frontend/components/sections/GlobalSearchSection.tsx +124 -0
  197. package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +943 -0
  198. package/src/modules/search/frontend/index.ts +3 -0
  199. package/src/modules/search/frontend/utils.ts +82 -0
  200. package/src/modules/search/i18n/de.json +61 -0
  201. package/src/modules/search/i18n/en.json +72 -0
  202. package/src/modules/search/i18n/es.json +61 -0
  203. package/src/modules/search/i18n/pl.json +61 -0
  204. package/src/modules/search/index.ts +9 -0
  205. package/src/modules/search/lib/auto-indexing.ts +35 -0
  206. package/src/modules/search/lib/embedding-config.ts +161 -0
  207. package/src/modules/search/lib/global-search-config.ts +69 -0
  208. package/src/modules/search/lib/reindex-lock.ts +201 -0
  209. package/src/modules/search/subscribers/fulltext_upsert.ts +83 -0
  210. package/src/modules/search/subscribers/vector_delete.ts +75 -0
  211. package/src/modules/search/subscribers/vector_purge.ts +161 -0
  212. package/src/modules/search/subscribers/vector_upsert.ts +75 -0
  213. package/src/modules/search/workers/fulltext-index.worker.ts +318 -0
  214. package/src/modules/search/workers/vector-index.worker.ts +292 -0
  215. package/src/queue/fulltext-indexing.ts +87 -0
  216. package/src/queue/index.ts +2 -0
  217. package/src/queue/vector-indexing.ts +66 -0
  218. package/src/service.ts +397 -0
  219. package/src/strategies/fulltext.strategy.ts +155 -0
  220. package/src/strategies/index.ts +17 -0
  221. package/src/strategies/token.strategy.ts +153 -0
  222. package/src/strategies/vector.strategy.ts +234 -0
  223. package/src/types.ts +38 -0
  224. package/src/vector/drivers/chromadb/index.ts +49 -0
  225. package/src/vector/drivers/index.ts +4 -0
  226. package/src/vector/drivers/pgvector/index.ts +627 -0
  227. package/src/vector/drivers/qdrant/index.ts +49 -0
  228. package/src/vector/index.ts +3 -0
  229. package/src/vector/lib/vector-logs.ts +46 -0
  230. package/src/vector/services/checksum.ts +18 -0
  231. package/src/vector/services/embedding.ts +275 -0
  232. package/src/vector/services/index.ts +3 -0
  233. package/src/vector/services/vector-index.service.ts +1234 -0
  234. package/src/vector/types/pg.d.ts +1 -0
  235. package/src/vector/types.ts +220 -0
  236. package/tsconfig.json +9 -0
  237. package/watch.mjs +6 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/indexer/search-indexer.ts"],
4
+ "sourcesContent": ["import type { SearchService } from '../service'\nimport type {\n SearchModuleConfig,\n SearchEntityConfig,\n SearchBuildContext,\n IndexableRecord,\n SearchResultPresenter,\n SearchResultLink,\n} from '../types'\nimport type { FullTextSearchStrategy } from '../strategies/fulltext.strategy'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { Queue } from '@open-mercato/queue'\nimport type { FulltextIndexJobPayload } from '../queue/fulltext-indexing'\nimport type { VectorIndexJobPayload, VectorBatchRecord } from '../queue/vector-indexing'\nimport { searchDebug, searchDebugWarn, searchError } from '../lib/debug'\n\n/**\n * Maximum number of pages to process during reindex to prevent infinite loops.\n * At 50 records per page, this allows up to 500,000 records per entity.\n */\nconst MAX_PAGES = 10000\n\n/**\n * Parameters for indexing a record.\n */\nexport type IndexRecordParams = {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n record: Record<string, unknown>\n customFields?: Record<string, unknown>\n}\n\n/**\n * Parameters for deleting a record from the search index.\n */\nexport type DeleteRecordParams = {\n entityId: EntityId\n recordId: string\n tenantId: string\n}\n\n/**\n * Parameters for purging all records of an entity type.\n */\nexport type PurgeEntityParams = {\n entityId: EntityId\n tenantId: string\n}\n\n/**\n * Parameters for reindexing an entity to fulltext search.\n */\nexport type ReindexEntityParams = {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n /** Whether to recreate the index first (default: true) */\n recreateIndex?: boolean\n /** Callback for progress tracking */\n onProgress?: (progress: ReindexProgress) => void\n /** Whether to use queue for batch processing (default: false) */\n useQueue?: boolean\n}\n\n/**\n * Parameters for reindexing all entities to fulltext search.\n */\nexport type ReindexAllParams = {\n tenantId: string\n organizationId?: string | null\n /** Whether to recreate the index first (default: true) */\n recreateIndex?: boolean\n /** Callback for progress tracking */\n onProgress?: (progress: ReindexProgress) => void\n /** Whether to use queue for batch processing (default: false) */\n useQueue?: boolean\n}\n\n/**\n * Progress information during reindex.\n */\nexport type ReindexProgress = {\n entityId: EntityId\n phase: 'starting' | 'fetching' | 'indexing' | 'complete'\n processed: number\n total?: number\n}\n\n/**\n * Result of a reindex operation.\n */\nexport type ReindexResult = {\n success: boolean\n entitiesProcessed: number\n recordsIndexed: number\n /** Number of records dropped due to missing id or other validation failures */\n recordsDropped?: number\n /** Number of jobs enqueued (when useQueue is true) */\n jobsEnqueued?: number\n errors: Array<{ entityId: EntityId; error: string }>\n}\n\n/**\n * Optional dependencies for SearchIndexer.\n */\nexport type SearchIndexerOptions = {\n queryEngine?: QueryEngine\n /** Queue for fulltext batch indexing */\n fulltextQueue?: Queue<FulltextIndexJobPayload>\n /** Queue for vector batch indexing */\n vectorQueue?: Queue<VectorIndexJobPayload>\n}\n\n/**\n * SearchIndexer orchestrates indexing operations by resolving entity configs\n * and building IndexableRecords for the SearchService.\n */\nexport class SearchIndexer {\n private readonly entityConfigMap: Map<EntityId, SearchEntityConfig>\n private readonly queryEngine?: QueryEngine\n private readonly fulltextQueue?: Queue<FulltextIndexJobPayload>\n private readonly vectorQueue?: Queue<VectorIndexJobPayload>\n\n constructor(\n private readonly searchService: SearchService,\n private readonly moduleConfigs: SearchModuleConfig[],\n options?: SearchIndexerOptions,\n ) {\n this.entityConfigMap = new Map()\n this.queryEngine = options?.queryEngine\n this.fulltextQueue = options?.fulltextQueue\n this.vectorQueue = options?.vectorQueue\n for (const moduleConfig of moduleConfigs) {\n for (const entityConfig of moduleConfig.entities) {\n if (entityConfig.enabled !== false) {\n this.entityConfigMap.set(entityConfig.entityId as EntityId, entityConfig)\n }\n }\n }\n }\n\n /**\n * Get the entity config for a given entity ID.\n */\n getEntityConfig(entityId: EntityId): SearchEntityConfig | undefined {\n return this.entityConfigMap.get(entityId)\n }\n\n /**\n * Get all configured entity configs.\n */\n getAllEntityConfigs(): SearchEntityConfig[] {\n return Array.from(this.entityConfigMap.values())\n }\n\n /**\n * Check if an entity is configured for search indexing.\n */\n isEntityEnabled(entityId: EntityId): boolean {\n const config = this.entityConfigMap.get(entityId)\n return config?.enabled !== false\n }\n\n /**\n * Index a record in the search service.\n */\n async indexRecord(params: IndexRecordParams): Promise<void> {\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return // Entity not configured for search\n }\n\n const buildContext: SearchBuildContext = {\n record: params.record,\n customFields: params.customFields ?? {},\n organizationId: params.organizationId,\n tenantId: params.tenantId,\n queryEngine: this.queryEngine,\n }\n\n // Try buildSource first (provides text, presenter, links, checksumSource)\n let text: string | string[] | undefined\n let presenter: SearchResultPresenter | undefined\n let url: string | undefined\n let links: SearchResultLink[] | undefined\n let checksumSource: unknown | undefined\n\n if (config.buildSource) {\n try {\n const source = await config.buildSource(buildContext)\n if (source) {\n text = source.text\n if (source.presenter) presenter = source.presenter\n if (source.links) links = source.links\n if (source.checksumSource !== undefined) checksumSource = source.checksumSource\n }\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'buildSource failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Fall back to formatResult if no presenter from buildSource\n if (!presenter && config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'formatResult failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Resolve URL if not already set\n if (!url && config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'resolveUrl failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Resolve links if not already set\n if (!links && config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'resolveLinks failed', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Build IndexableRecord\n const indexableRecord: IndexableRecord = {\n entityId: params.entityId,\n recordId: params.recordId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n fields: params.record,\n presenter,\n url,\n links,\n text,\n checksumSource,\n }\n\n await this.searchService.index(indexableRecord)\n }\n\n /**\n * Index a record by ID (loads the record from database first).\n * Used by workers that only have record identifiers.\n */\n async indexRecordById(params: {\n entityId: EntityId\n recordId: string\n tenantId: string\n organizationId?: string | null\n }): Promise<{ action: 'indexed' | 'skipped'; reason?: string }> {\n if (!this.queryEngine) {\n return { action: 'skipped', reason: 'queryEngine not available' }\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return { action: 'skipped', reason: 'entity not configured' }\n }\n\n // Load record from database\n try {\n const result = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n filters: { id: params.recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n })\n\n const record = result.items[0] as Record<string, unknown> | undefined\n if (!record) {\n return { action: 'skipped', reason: 'record not found' }\n }\n\n // Extract custom fields\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n customFields[key.slice(3)] = value\n }\n }\n\n await this.indexRecord({\n entityId: params.entityId,\n recordId: params.recordId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n record,\n customFields,\n })\n\n return { action: 'indexed' }\n } catch (error) {\n searchError('SearchIndexer', 'Failed to load record for indexing', {\n entityId: params.entityId,\n recordId: params.recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error\n }\n }\n\n /**\n * Delete a record from the search index.\n */\n async deleteRecord(params: DeleteRecordParams): Promise<void> {\n await this.searchService.delete(params.entityId, params.recordId, params.tenantId)\n }\n\n /**\n * Purge all records of an entity type from the search index.\n */\n async purgeEntity(params: PurgeEntityParams): Promise<void> {\n await this.searchService.purge(params.entityId, params.tenantId)\n }\n\n /**\n * Reindex an entity via all configured strategies (including vector).\n * This is the general reindex method that works with all search strategies.\n */\n async reindexEntity(params: {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n if (!this.queryEngine) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Query engine not available' }],\n }\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Entity not configured for search' }],\n }\n }\n\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 1,\n recordsIndexed: 0,\n errors: [],\n }\n\n // Optionally purge first\n if (params.purgeFirst) {\n try {\n await this.searchService.purge(params.entityId, params.tenantId)\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to purge entity before reindex', {\n entityId: params.entityId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Paginate through all records\n let page = 1\n const pageSize = 200\n let hasMore = true\n\n while (hasMore && page <= MAX_PAGES) {\n try {\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n includeCustomFields: true,\n page: { page, pageSize },\n })\n\n const items = queryResult.items as Record<string, unknown>[]\n if (items.length === 0) {\n hasMore = false\n break\n }\n\n // Build and index records\n const { records } = await this.buildIndexableRecords(\n params.entityId,\n params.tenantId,\n params.organizationId ?? null,\n items,\n config,\n )\n\n // Index each record via SearchService (sends to all strategies)\n for (const record of records) {\n try {\n await this.searchService.index(record)\n result.recordsIndexed++\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to index record', {\n entityId: params.entityId,\n recordId: record.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n page++\n hasMore = items.length === pageSize\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n break\n }\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities via all configured strategies.\n */\n async reindexAll(params: {\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [],\n }\n\n const enabledEntities = this.listEnabledEntities()\n\n for (const entityId of enabledEntities) {\n const entityResult = await this.reindexEntity({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n purgeFirst: params.purgeFirst,\n })\n\n result.entitiesProcessed++\n result.recordsIndexed += entityResult.recordsIndexed\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Bulk index multiple records.\n */\n async bulkIndexRecords(params: IndexRecordParams[]): Promise<void> {\n const indexableRecords: IndexableRecord[] = []\n\n for (const param of params) {\n const config = this.entityConfigMap.get(param.entityId)\n if (!config || config.enabled === false) {\n continue\n }\n\n const buildContext: SearchBuildContext = {\n record: param.record,\n customFields: param.customFields ?? {},\n organizationId: param.organizationId,\n tenantId: param.tenantId,\n }\n\n let presenter: SearchResultPresenter | undefined\n if (config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch {\n // Skip presenter on error\n }\n }\n\n let url: string | undefined\n if (config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch {\n // Skip URL on error\n }\n }\n\n let links: SearchResultLink[] | undefined\n if (config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch {\n // Skip links on error\n }\n }\n\n indexableRecords.push({\n entityId: param.entityId,\n recordId: param.recordId,\n tenantId: param.tenantId,\n organizationId: param.organizationId,\n fields: param.record,\n presenter,\n url,\n links,\n })\n }\n\n if (indexableRecords.length > 0) {\n await this.searchService.bulkIndex(indexableRecords)\n }\n }\n\n /**\n * List all enabled entity IDs from the module configurations.\n */\n listEnabledEntities(): EntityId[] {\n return Array.from(this.entityConfigMap.keys())\n }\n\n /**\n * Get the fulltext strategy from the search service.\n */\n private getFulltextStrategy(): FullTextSearchStrategy | undefined {\n const strategy = this.searchService.getStrategy('fulltext')\n if (!strategy) return undefined\n return strategy as FullTextSearchStrategy\n }\n\n /**\n * Reindex a single entity type to fulltext search.\n * This fetches all records from the database and re-indexes them to fulltext only.\n *\n * When `useQueue` is true, batches are enqueued for background processing by workers.\n * When `useQueue` is false (default), batches are indexed directly (blocking).\n */\n async reindexEntityToFulltext(params: ReindexEntityParams): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const fulltext = this.getFulltextStrategy()\n if (!fulltext) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Fulltext strategy not available' })\n return result\n }\n\n // If useQueue is requested but no queue is available, return error\n if (params.useQueue && !this.fulltextQueue) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Fulltext queue not configured for queue-based reindexing' })\n return result\n }\n\n if (!this.queryEngine) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'QueryEngine not available for reindexing' })\n return result\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Entity not configured for search' })\n return result\n }\n\n try {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'starting',\n processed: 0,\n })\n\n // Recreate index if requested (default: true)\n if (params.recreateIndex !== false) {\n await fulltext.recreateIndex(params.tenantId)\n }\n\n // Fetch and index records with pagination\n const pageSize = 200\n let page = 1\n let totalProcessed = 0\n let jobsEnqueued = 0\n\n for (;;) {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'fetching',\n processed: totalProcessed,\n })\n\n try {\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n page: { page, pageSize },\n })\n\n if (!queryResult.items.length) {\n break\n }\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'indexing',\n processed: totalProcessed,\n total: queryResult.total,\n })\n\n // Build IndexableRecords for this batch\n const { records: indexableRecords, dropped } = await this.buildIndexableRecords(\n params.entityId,\n params.tenantId,\n params.organizationId ?? null,\n queryResult.items,\n config,\n )\n result.recordsDropped = (result.recordsDropped ?? 0) + dropped\n\n // Index to fulltext - either via queue or directly\n if (indexableRecords.length > 0) {\n if (params.useQueue && this.fulltextQueue) {\n // Enqueue batch for background processing - only pass minimal references\n // Worker will load fresh data from entity_indexes table\n await this.fulltextQueue.enqueue({\n jobType: 'batch-index',\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n records: indexableRecords.map((r) => ({ entityId: r.entityId, recordId: r.recordId })),\n })\n jobsEnqueued += 1\n totalProcessed += indexableRecords.length\n } else {\n // Direct indexing (blocking)\n try {\n await fulltext.bulkIndex(indexableRecords)\n totalProcessed += indexableRecords.length\n } catch (indexError) {\n // Log error but continue with remaining batches\n const errorMsg = indexError instanceof Error ? indexError.message : String(indexError)\n result.errors.push({\n entityId: params.entityId,\n error: `Batch ${page} failed: ${errorMsg}`,\n })\n }\n }\n }\n\n if (queryResult.items.length < pageSize) {\n break\n }\n page += 1\n\n // Safety check to prevent infinite loops\n if (page > MAX_PAGES) {\n break\n }\n } catch (queryError) {\n const errorMsg = queryError instanceof Error ? queryError.message : String(queryError)\n result.errors.push({\n entityId: params.entityId,\n error: `Query failed: ${errorMsg}`,\n })\n break\n }\n }\n\n result.entitiesProcessed = 1\n result.recordsIndexed = totalProcessed\n result.jobsEnqueued = jobsEnqueued\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'complete',\n processed: totalProcessed,\n total: totalProcessed,\n })\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities to fulltext search.\n *\n * When `useQueue` is true, batches are enqueued for background processing by workers.\n * When `useQueue` is false (default), batches are indexed directly (blocking).\n */\n async reindexAllToFulltext(params: ReindexAllParams): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const fulltext = this.getFulltextStrategy()\n if (!fulltext) {\n result.success = false\n result.errors.push({ entityId: 'all' as EntityId, error: 'Fulltext strategy not available' })\n return result\n }\n\n // Recreate index once before processing all entities\n if (params.recreateIndex !== false) {\n await fulltext.recreateIndex(params.tenantId)\n }\n\n const entities = this.listEnabledEntities()\n\n for (const entityId of entities) {\n const entityResult = await this.reindexEntityToFulltext({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n recreateIndex: false, // Already recreated above\n onProgress: params.onProgress,\n useQueue: params.useQueue,\n })\n\n result.entitiesProcessed += entityResult.entitiesProcessed\n result.recordsIndexed += entityResult.recordsIndexed\n result.recordsDropped = (result.recordsDropped ?? 0) + (entityResult.recordsDropped ?? 0)\n result.jobsEnqueued = (result.jobsEnqueued ?? 0) + (entityResult.jobsEnqueued ?? 0)\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Reindex a single entity type to vector search.\n * This fetches all records from the database and enqueues them for vector indexing.\n *\n * When `useQueue` is true (default), record IDs are enqueued for background processing by workers.\n * When `useQueue` is false, records are indexed directly (blocking).\n */\n async reindexEntityToVector(params: ReindexEntityParams & { purgeFirst?: boolean }): Promise<ReindexResult> {\n searchDebug('SearchIndexer', 'reindexEntityToVector called', {\n entityId: params.entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n useQueue: params.useQueue,\n purgeFirst: params.purgeFirst,\n })\n\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n // If useQueue is requested but no queue is available, return error\n if (params.useQueue !== false && !this.vectorQueue) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Vector queue not configured for queue-based reindexing' })\n return result\n }\n\n if (!this.queryEngine) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'QueryEngine not available for reindexing' })\n return result\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Entity not configured for search' })\n return result\n }\n\n try {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'starting',\n processed: 0,\n })\n\n // Optionally purge vector index first\n if (params.purgeFirst) {\n try {\n await this.searchService.purge(params.entityId, params.tenantId)\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to purge entity before vector reindex', {\n entityId: params.entityId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Fetch and enqueue records with pagination\n const pageSize = 200\n let page = 1\n let totalProcessed = 0\n let jobsEnqueued = 0\n\n for (;;) {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'fetching',\n processed: totalProcessed,\n })\n\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n page: { page, pageSize },\n })\n\n if (!queryResult.items.length) break\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'indexing',\n processed: totalProcessed,\n total: queryResult.total,\n })\n\n // Build batch of record references\n const batchRecords: VectorBatchRecord[] = []\n for (const item of queryResult.items) {\n const recordId = String((item as Record<string, unknown>).id ?? '')\n if (!recordId) {\n result.recordsDropped = (result.recordsDropped ?? 0) + 1\n continue\n }\n batchRecords.push({\n entityId: params.entityId,\n recordId,\n })\n }\n\n // Enqueue batch for background processing or index directly\n if (batchRecords.length > 0) {\n if (params.useQueue !== false && this.vectorQueue) {\n await this.vectorQueue.enqueue({\n jobType: 'batch-index',\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n records: batchRecords,\n })\n jobsEnqueued += 1\n totalProcessed += batchRecords.length\n searchDebug('SearchIndexer', 'Enqueued batch for vector indexing', {\n entityId: params.entityId,\n batchSize: batchRecords.length,\n jobsEnqueued,\n totalProcessed,\n })\n } else {\n // Direct indexing (blocking) - index each record via SearchService\n for (const { entityId, recordId } of batchRecords) {\n try {\n await this.indexRecordById({\n entityId: entityId as EntityId,\n recordId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n })\n totalProcessed++\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to index record to vector', {\n entityId,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n }\n }\n\n if (queryResult.items.length < pageSize) break\n page += 1\n\n // Safety check to prevent infinite loops\n if (page > MAX_PAGES) {\n searchDebugWarn('SearchIndexer', 'Reached MAX_PAGES limit, stopping pagination', {\n entityId: params.entityId,\n maxPages: MAX_PAGES,\n totalProcessed,\n })\n break\n }\n }\n\n result.entitiesProcessed = 1\n result.recordsIndexed = totalProcessed\n result.jobsEnqueued = jobsEnqueued\n\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'complete',\n processed: totalProcessed,\n total: totalProcessed,\n })\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities to vector search.\n *\n * When `useQueue` is true (default), batches are enqueued for background processing by workers.\n * When `useQueue` is false, batches are indexed directly (blocking).\n */\n async reindexAllToVector(params: ReindexAllParams & { purgeFirst?: boolean }): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const entities = this.listEnabledEntities()\n for (const entityId of entities) {\n const entityResult = await this.reindexEntityToVector({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n onProgress: params.onProgress,\n useQueue: params.useQueue,\n purgeFirst: params.purgeFirst,\n })\n\n result.entitiesProcessed += entityResult.entitiesProcessed\n result.recordsIndexed += entityResult.recordsIndexed\n result.recordsDropped = (result.recordsDropped ?? 0) + (entityResult.recordsDropped ?? 0)\n result.jobsEnqueued = (result.jobsEnqueued ?? 0) + (entityResult.jobsEnqueued ?? 0)\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Build IndexableRecords from raw query results.\n * Returns records and count of dropped items (missing id or other validation failures).\n */\n private async buildIndexableRecords(\n entityId: EntityId,\n tenantId: string,\n organizationId: string | null,\n items: Record<string, unknown>[],\n config: SearchEntityConfig,\n ): Promise<{ records: IndexableRecord[]; dropped: number }> {\n const records: IndexableRecord[] = []\n let dropped = 0\n\n // Debug: log first item to see structure\n if (items.length > 0) {\n searchDebug('SearchIndexer', 'Sample item structure', {\n entityId,\n sampleKeys: Object.keys(items[0]),\n sampleId: items[0].id,\n hasId: 'id' in items[0],\n firstName: items[0].first_name,\n lastName: items[0].last_name,\n preferredName: items[0].preferred_name,\n sampleItem: JSON.stringify(items[0]).slice(0, 500),\n })\n }\n\n for (const item of items) {\n const recordId = String(item.id ?? '')\n if (!recordId) {\n searchDebugWarn('SearchIndexer', 'Skipping item without id', { entityId, itemKeys: Object.keys(item) })\n dropped++\n continue\n }\n\n // Extract custom fields from record\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(item)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n const cfKey = key.slice(3) // Remove 'cf:' or 'cf_' prefix (both are 3 chars)\n customFields[cfKey] = value\n }\n }\n\n const buildContext: SearchBuildContext = {\n record: item,\n customFields,\n organizationId,\n tenantId,\n queryEngine: this.queryEngine,\n }\n\n // Try buildSource first (provides text, presenter, links, checksumSource)\n let text: string | string[] | undefined\n let presenter: SearchResultPresenter | undefined\n let url: string | undefined\n let links: SearchResultLink[] | undefined\n let checksumSource: unknown | undefined\n\n if (config.buildSource) {\n try {\n const source = await config.buildSource(buildContext)\n if (source) {\n text = source.text\n if (source.presenter) presenter = source.presenter\n if (source.links) links = source.links\n if (source.checksumSource !== undefined) checksumSource = source.checksumSource\n }\n } catch (err) {\n searchDebugWarn('SearchIndexer', 'buildSource failed', {\n entityId,\n recordId,\n error: err instanceof Error ? err.message : err,\n })\n }\n }\n\n // Fall back to formatResult if no presenter from buildSource\n if (!presenter && config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch {\n // Skip presenter on error\n }\n }\n\n // Resolve URL if not already set\n if (!url && config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch {\n // Skip URL on error\n }\n }\n\n // Resolve links if not already set\n if (!links && config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch {\n // Skip links on error\n }\n }\n\n records.push({\n entityId,\n recordId,\n tenantId,\n organizationId,\n fields: item,\n presenter,\n url,\n links,\n text,\n checksumSource,\n })\n }\n\n searchDebug('SearchIndexer', 'Finished building records', {\n entityId,\n inputCount: items.length,\n outputCount: records.length,\n dropped,\n })\n\n return { records, dropped }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,41 @@
1
+ import { searchDebugWarn, searchError } from "../../lib/debug.js";
2
+ const metadata = {
3
+ event: "search.delete_record",
4
+ persistent: false
5
+ };
6
+ function createSearchDeleteSubscriber(indexer) {
7
+ return async function handle(payload) {
8
+ const entityId = String(payload?.entityId || "");
9
+ const recordId = String(payload?.recordId || "");
10
+ const tenantId = String(payload?.tenantId || "");
11
+ if (!entityId || !recordId || !tenantId) {
12
+ searchDebugWarn("search.delete_record", "Missing required fields", {
13
+ entityId,
14
+ recordId,
15
+ tenantId
16
+ });
17
+ return;
18
+ }
19
+ try {
20
+ await indexer.deleteRecord({
21
+ entityId,
22
+ recordId,
23
+ tenantId
24
+ });
25
+ } catch (error) {
26
+ searchError("search.delete_record", "Failed to delete record", {
27
+ entityId,
28
+ recordId,
29
+ error: error instanceof Error ? error.message : error
30
+ });
31
+ throw error;
32
+ }
33
+ };
34
+ }
35
+ var delete_default = createSearchDeleteSubscriber;
36
+ export {
37
+ createSearchDeleteSubscriber,
38
+ delete_default as default,
39
+ metadata
40
+ };
41
+ //# sourceMappingURL=delete.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/indexer/subscribers/delete.ts"],
4
+ "sourcesContent": ["import type { SearchIndexer } from '../search-indexer'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { SearchDeletePayload } from '@open-mercato/shared/modules/search'\nimport { searchDebugWarn, searchError } from '../../lib/debug'\n\n/**\n * Event subscriber metadata.\n */\nexport const metadata = {\n event: 'search.delete_record',\n persistent: false,\n}\n\n/**\n * Factory to create the search delete subscriber handler.\n */\nexport function createSearchDeleteSubscriber(indexer: SearchIndexer) {\n return async function handle(payload: SearchDeletePayload): Promise<void> {\n const entityId = String(payload?.entityId || '') as EntityId\n const recordId = String(payload?.recordId || '')\n const tenantId = String(payload?.tenantId || '')\n\n if (!entityId || !recordId || !tenantId) {\n searchDebugWarn('search.delete_record', 'Missing required fields', {\n entityId,\n recordId,\n tenantId,\n })\n return\n }\n\n try {\n await indexer.deleteRecord({\n entityId,\n recordId,\n tenantId,\n })\n } catch (error) {\n searchError('search.delete_record', 'Failed to delete record', {\n entityId,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n throw error\n }\n }\n}\n\nexport default createSearchDeleteSubscriber\n"],
5
+ "mappings": "AAGA,SAAS,iBAAiB,mBAAmB;AAKtC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AACd;AAKO,SAAS,6BAA6B,SAAwB;AACnE,SAAO,eAAe,OAAO,SAA6C;AACxE,UAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAC/C,UAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAC/C,UAAM,WAAW,OAAO,SAAS,YAAY,EAAE;AAE/C,QAAI,CAAC,YAAY,CAAC,YAAY,CAAC,UAAU;AACvC,sBAAgB,wBAAwB,2BAA2B;AAAA,QACjE;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA,QAAI;AACF,YAAM,QAAQ,aAAa;AAAA,QACzB;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,SAAS,OAAO;AACd,kBAAY,wBAAwB,2BAA2B;AAAA,QAC7D;AAAA,QACA;AAAA,QACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAClD,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,34 @@
1
+ function isSearchDebugEnabled() {
2
+ const raw = (process.env.OM_SEARCH_DEBUG ?? "").toLowerCase();
3
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
4
+ }
5
+ function searchDebug(prefix, message, data) {
6
+ if (!isSearchDebugEnabled()) return;
7
+ if (data) {
8
+ console.log(`[${prefix}] ${message}`, data);
9
+ } else {
10
+ console.log(`[${prefix}] ${message}`);
11
+ }
12
+ }
13
+ function searchDebugWarn(prefix, message, data) {
14
+ if (!isSearchDebugEnabled()) return;
15
+ if (data) {
16
+ console.warn(`[${prefix}] ${message}`, data);
17
+ } else {
18
+ console.warn(`[${prefix}] ${message}`);
19
+ }
20
+ }
21
+ function searchError(prefix, message, data) {
22
+ if (data) {
23
+ console.error(`[${prefix}] ${message}`, data);
24
+ } else {
25
+ console.error(`[${prefix}] ${message}`);
26
+ }
27
+ }
28
+ export {
29
+ isSearchDebugEnabled,
30
+ searchDebug,
31
+ searchDebugWarn,
32
+ searchError
33
+ };
34
+ //# sourceMappingURL=debug.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/debug.ts"],
4
+ "sourcesContent": ["/**\n * Debug utilities for search module.\n *\n * Set OM_SEARCH_DEBUG=true to enable debug logging.\n */\n\nexport function isSearchDebugEnabled(): boolean {\n const raw = (process.env.OM_SEARCH_DEBUG ?? '').toLowerCase()\n return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on'\n}\n\n/**\n * Log a debug message if OM_SEARCH_DEBUG is enabled.\n */\nexport function searchDebug(prefix: string, message: string, data?: Record<string, unknown>): void {\n if (!isSearchDebugEnabled()) return\n if (data) {\n console.log(`[${prefix}] ${message}`, data)\n } else {\n console.log(`[${prefix}] ${message}`)\n }\n}\n\n/**\n * Log a warning message if OM_SEARCH_DEBUG is enabled.\n */\nexport function searchDebugWarn(prefix: string, message: string, data?: Record<string, unknown>): void {\n if (!isSearchDebugEnabled()) return\n if (data) {\n console.warn(`[${prefix}] ${message}`, data)\n } else {\n console.warn(`[${prefix}] ${message}`)\n }\n}\n\n/**\n * Log an error message (always logs, not gated by debug flag).\n * Errors should always be visible for troubleshooting.\n */\nexport function searchError(prefix: string, message: string, data?: Record<string, unknown>): void {\n if (data) {\n console.error(`[${prefix}] ${message}`, data)\n } else {\n console.error(`[${prefix}] ${message}`)\n }\n}\n"],
5
+ "mappings": "AAMO,SAAS,uBAAgC;AAC9C,QAAM,OAAO,QAAQ,IAAI,mBAAmB,IAAI,YAAY;AAC5D,SAAO,QAAQ,OAAO,QAAQ,UAAU,QAAQ,SAAS,QAAQ;AACnE;AAKO,SAAS,YAAY,QAAgB,SAAiB,MAAsC;AACjG,MAAI,CAAC,qBAAqB,EAAG;AAC7B,MAAI,MAAM;AACR,YAAQ,IAAI,IAAI,MAAM,KAAK,OAAO,IAAI,IAAI;AAAA,EAC5C,OAAO;AACL,YAAQ,IAAI,IAAI,MAAM,KAAK,OAAO,EAAE;AAAA,EACtC;AACF;AAKO,SAAS,gBAAgB,QAAgB,SAAiB,MAAsC;AACrG,MAAI,CAAC,qBAAqB,EAAG;AAC7B,MAAI,MAAM;AACR,YAAQ,KAAK,IAAI,MAAM,KAAK,OAAO,IAAI,IAAI;AAAA,EAC7C,OAAO;AACL,YAAQ,KAAK,IAAI,MAAM,KAAK,OAAO,EAAE;AAAA,EACvC;AACF;AAMO,SAAS,YAAY,QAAgB,SAAiB,MAAsC;AACjG,MAAI,MAAM;AACR,YAAQ,MAAM,IAAI,MAAM,KAAK,OAAO,IAAI,IAAI;AAAA,EAC9C,OAAO;AACL,YAAQ,MAAM,IAAI,MAAM,KAAK,OAAO,EAAE;AAAA,EACxC;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,107 @@
1
+ const TITLE_FIELDS = [
2
+ "display_name",
3
+ "displayName",
4
+ "name",
5
+ "title",
6
+ "label",
7
+ "full_name",
8
+ "fullName",
9
+ "brand_name",
10
+ "brandName",
11
+ "legal_name",
12
+ "legalName",
13
+ "first_name",
14
+ "firstName",
15
+ "last_name",
16
+ "lastName",
17
+ "preferred_name",
18
+ "preferredName",
19
+ "email",
20
+ "primary_email",
21
+ "primaryEmail",
22
+ "code",
23
+ "sku",
24
+ "reference",
25
+ "identifier",
26
+ "slug"
27
+ ];
28
+ const SUBTITLE_FIELDS = [
29
+ "description",
30
+ "summary",
31
+ "notes",
32
+ "email",
33
+ "primary_email",
34
+ "primaryEmail",
35
+ "phone",
36
+ "primary_phone",
37
+ "primaryPhone",
38
+ "status",
39
+ "type",
40
+ "kind",
41
+ "category"
42
+ ];
43
+ function findFirstValue(doc, fields) {
44
+ for (const field of fields) {
45
+ const value = doc[field];
46
+ if (value != null && String(value).trim().length > 0) {
47
+ return String(value).trim();
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ function findAnyStringValue(doc, excludeFields) {
53
+ const skipFields = /* @__PURE__ */ new Set([
54
+ "id",
55
+ "tenant_id",
56
+ "tenantId",
57
+ "organization_id",
58
+ "organizationId",
59
+ "created_at",
60
+ "createdAt",
61
+ "updated_at",
62
+ "updatedAt",
63
+ "deleted_at",
64
+ "deletedAt",
65
+ ...excludeFields
66
+ ]);
67
+ for (const [key, value] of Object.entries(doc)) {
68
+ if (skipFields.has(key)) continue;
69
+ if (key.startsWith("cf:") || key.startsWith("cf_")) continue;
70
+ if (typeof value === "string" && value.trim().length > 0 && value.length < 200) {
71
+ return value.trim();
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ function formatEntityLabel(entityId) {
77
+ const entityName = entityId.split(":")[1] ?? entityId;
78
+ return entityName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
79
+ }
80
+ function extractFallbackPresenter(doc, entityId, recordId) {
81
+ const entityLabel = formatEntityLabel(entityId);
82
+ let title = findFirstValue(doc, TITLE_FIELDS);
83
+ if (!title) {
84
+ title = findAnyStringValue(doc, new Set(SUBTITLE_FIELDS));
85
+ }
86
+ if (!title) {
87
+ const shortId = recordId.length > 8 ? recordId.slice(0, 8) + "..." : recordId;
88
+ title = `${entityLabel} ${shortId}`;
89
+ }
90
+ const subtitleParts = [];
91
+ for (const field of SUBTITLE_FIELDS) {
92
+ const value = doc[field];
93
+ if (value != null && String(value).trim().length > 0 && String(value) !== title) {
94
+ subtitleParts.push(String(value).trim());
95
+ if (subtitleParts.length >= 3) break;
96
+ }
97
+ }
98
+ return {
99
+ title,
100
+ subtitle: subtitleParts.length > 0 ? subtitleParts.join(" \xB7 ").slice(0, 120) : void 0,
101
+ badge: entityLabel
102
+ };
103
+ }
104
+ export {
105
+ extractFallbackPresenter
106
+ };
107
+ //# sourceMappingURL=fallback-presenter.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/fallback-presenter.ts"],
4
+ "sourcesContent": ["import type { SearchResultPresenter } from '@open-mercato/shared/modules/search'\n\n// Fields to check for title, in priority order\nconst TITLE_FIELDS = [\n 'display_name', 'displayName',\n 'name', 'title', 'label',\n 'full_name', 'fullName',\n 'brand_name', 'brandName',\n 'legal_name', 'legalName',\n 'first_name', 'firstName',\n 'last_name', 'lastName',\n 'preferred_name', 'preferredName',\n 'email', 'primary_email', 'primaryEmail',\n 'code', 'sku', 'reference',\n 'identifier', 'slug',\n]\n\n// Fields to check for subtitle\nconst SUBTITLE_FIELDS = [\n 'description', 'summary', 'notes',\n 'email', 'primary_email', 'primaryEmail',\n 'phone', 'primary_phone', 'primaryPhone',\n 'status', 'type', 'kind', 'category',\n]\n\nfunction findFirstValue(doc: Record<string, unknown>, fields: string[]): string | null {\n for (const field of fields) {\n const value = doc[field]\n if (value != null && String(value).trim().length > 0) {\n return String(value).trim()\n }\n }\n return null\n}\n\nfunction findAnyStringValue(doc: Record<string, unknown>, excludeFields: Set<string>): string | null {\n // Skip these fields as they're not meaningful for display\n const skipFields = new Set([\n 'id', 'tenant_id', 'tenantId', 'organization_id', 'organizationId',\n 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt',\n ...excludeFields,\n ])\n\n for (const [key, value] of Object.entries(doc)) {\n if (skipFields.has(key)) continue\n if (key.startsWith('cf:') || key.startsWith('cf_')) continue\n if (typeof value === 'string' && value.trim().length > 0 && value.length < 200) {\n return value.trim()\n }\n }\n return null\n}\n\nfunction formatEntityLabel(entityId: string): string {\n const entityName = entityId.split(':')[1] ?? entityId\n return entityName\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (c) => c.toUpperCase())\n}\n\n/**\n * Extract a presenter from doc fields when no search.ts config exists.\n *\n * TODO: This is a basic implementation. Future improvements could include:\n * - Entity-type specific field mappings\n * - Smarter field combination (e.g., first_name + last_name)\n * - Custom field (cf:*) inspection for user-defined display fields\n * - Configuration for default presenter fields per entity type\n */\nexport function extractFallbackPresenter(\n doc: Record<string, unknown>,\n entityId: string,\n recordId: string,\n): SearchResultPresenter {\n const entityLabel = formatEntityLabel(entityId)\n\n // 1. Try common title fields\n let title = findFirstValue(doc, TITLE_FIELDS)\n\n // 2. If no title found, try any string field\n if (!title) {\n title = findAnyStringValue(doc, new Set(SUBTITLE_FIELDS))\n }\n\n // 3. Last resort: use entity label + truncated record ID\n if (!title) {\n const shortId = recordId.length > 8 ? recordId.slice(0, 8) + '...' : recordId\n title = `${entityLabel} ${shortId}`\n }\n\n // Build subtitle from multiple relevant fields to show more context\n const subtitleParts: string[] = []\n for (const field of SUBTITLE_FIELDS) {\n const value = doc[field]\n if (value != null && String(value).trim().length > 0 && String(value) !== title) {\n subtitleParts.push(String(value).trim())\n if (subtitleParts.length >= 3) break // Limit to 3 parts\n }\n }\n\n return {\n title,\n subtitle: subtitleParts.length > 0 ? subtitleParts.join(' \u00B7 ').slice(0, 120) : undefined,\n badge: entityLabel,\n }\n}\n"],
5
+ "mappings": "AAGA,MAAM,eAAe;AAAA,EACnB;AAAA,EAAgB;AAAA,EAChB;AAAA,EAAQ;AAAA,EAAS;AAAA,EACjB;AAAA,EAAa;AAAA,EACb;AAAA,EAAc;AAAA,EACd;AAAA,EAAc;AAAA,EACd;AAAA,EAAc;AAAA,EACd;AAAA,EAAa;AAAA,EACb;AAAA,EAAkB;AAAA,EAClB;AAAA,EAAS;AAAA,EAAiB;AAAA,EAC1B;AAAA,EAAQ;AAAA,EAAO;AAAA,EACf;AAAA,EAAc;AAChB;AAGA,MAAM,kBAAkB;AAAA,EACtB;AAAA,EAAe;AAAA,EAAW;AAAA,EAC1B;AAAA,EAAS;AAAA,EAAiB;AAAA,EAC1B;AAAA,EAAS;AAAA,EAAiB;AAAA,EAC1B;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAQ;AAC5B;AAEA,SAAS,eAAe,KAA8B,QAAiC;AACrF,aAAW,SAAS,QAAQ;AAC1B,UAAM,QAAQ,IAAI,KAAK;AACvB,QAAI,SAAS,QAAQ,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,GAAG;AACpD,aAAO,OAAO,KAAK,EAAE,KAAK;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,KAA8B,eAA2C;AAEnG,QAAM,aAAa,oBAAI,IAAI;AAAA,IACzB;AAAA,IAAM;AAAA,IAAa;AAAA,IAAY;AAAA,IAAmB;AAAA,IAClD;AAAA,IAAc;AAAA,IAAa;AAAA,IAAc;AAAA,IAAa;AAAA,IAAc;AAAA,IACpE,GAAG;AAAA,EACL,CAAC;AAED,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,QAAI,WAAW,IAAI,GAAG,EAAG;AACzB,QAAI,IAAI,WAAW,KAAK,KAAK,IAAI,WAAW,KAAK,EAAG;AACpD,QAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,KAAK,MAAM,SAAS,KAAK;AAC9E,aAAO,MAAM,KAAK;AAAA,IACpB;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,UAA0B;AACnD,QAAM,aAAa,SAAS,MAAM,GAAG,EAAE,CAAC,KAAK;AAC7C,SAAO,WACJ,QAAQ,MAAM,GAAG,EACjB,QAAQ,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC;AAC5C;AAWO,SAAS,yBACd,KACA,UACA,UACuB;AACvB,QAAM,cAAc,kBAAkB,QAAQ;AAG9C,MAAI,QAAQ,eAAe,KAAK,YAAY;AAG5C,MAAI,CAAC,OAAO;AACV,YAAQ,mBAAmB,KAAK,IAAI,IAAI,eAAe,CAAC;AAAA,EAC1D;AAGA,MAAI,CAAC,OAAO;AACV,UAAM,UAAU,SAAS,SAAS,IAAI,SAAS,MAAM,GAAG,CAAC,IAAI,QAAQ;AACrE,YAAQ,GAAG,WAAW,IAAI,OAAO;AAAA,EACnC;AAGA,QAAM,gBAA0B,CAAC;AACjC,aAAW,SAAS,iBAAiB;AACnC,UAAM,QAAQ,IAAI,KAAK;AACvB,QAAI,SAAS,QAAQ,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,KAAK,OAAO,KAAK,MAAM,OAAO;AAC/E,oBAAc,KAAK,OAAO,KAAK,EAAE,KAAK,CAAC;AACvC,UAAI,cAAc,UAAU,EAAG;AAAA,IACjC;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,UAAU,cAAc,SAAS,IAAI,cAAc,KAAK,QAAK,EAAE,MAAM,GAAG,GAAG,IAAI;AAAA,IAC/E,OAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,75 @@
1
+ function extractSearchableFields(fields, config) {
2
+ const encryptedFieldSet = new Set(
3
+ config?.encryptedFields?.map((e) => e.field) ?? []
4
+ );
5
+ const policy = config?.fieldPolicy;
6
+ const searchableWhitelist = policy?.searchable ? new Set(policy.searchable) : null;
7
+ const excludedBlacklist = /* @__PURE__ */ new Set([
8
+ ...policy?.excluded ?? [],
9
+ ...policy?.hashOnly ?? []
10
+ ]);
11
+ const result = {};
12
+ for (const [field, value] of Object.entries(fields)) {
13
+ if (value == null) continue;
14
+ if (encryptedFieldSet.has(field)) continue;
15
+ if (excludedBlacklist.has(field)) continue;
16
+ if (searchableWhitelist && !searchableWhitelist.has(field)) continue;
17
+ result[field] = value;
18
+ }
19
+ return result;
20
+ }
21
+ function extractHashOnlyFields(fields, config) {
22
+ const hashOnlyFromPolicy = new Set(config?.fieldPolicy?.hashOnly ?? []);
23
+ const hashFieldsFromEncryption = new Set(
24
+ config?.encryptedFields?.filter((e) => e.hashField).map((e) => e.field) ?? []
25
+ );
26
+ const result = {};
27
+ for (const [field, value] of Object.entries(fields)) {
28
+ if (value == null) continue;
29
+ if (hashOnlyFromPolicy.has(field) || hashFieldsFromEncryption.has(field)) {
30
+ result[field] = value;
31
+ }
32
+ }
33
+ return result;
34
+ }
35
+ function classifyFields(fields, config) {
36
+ const searchable = [];
37
+ const hashOnly = [];
38
+ const excluded = [];
39
+ const encryptedFieldSet = new Set(
40
+ config?.encryptedFields?.map((e) => e.field) ?? []
41
+ );
42
+ const hashFieldsFromEncryption = new Set(
43
+ config?.encryptedFields?.filter((e) => e.hashField).map((e) => e.field) ?? []
44
+ );
45
+ const policy = config?.fieldPolicy;
46
+ const searchableWhitelist = policy?.searchable ? new Set(policy.searchable) : null;
47
+ const hashOnlyFromPolicy = new Set(policy?.hashOnly ?? []);
48
+ const excludedFromPolicy = new Set(policy?.excluded ?? []);
49
+ for (const field of Object.keys(fields)) {
50
+ if (excludedFromPolicy.has(field)) {
51
+ excluded.push(field);
52
+ continue;
53
+ }
54
+ if (hashOnlyFromPolicy.has(field) || hashFieldsFromEncryption.has(field)) {
55
+ hashOnly.push(field);
56
+ continue;
57
+ }
58
+ if (encryptedFieldSet.has(field) && !hashFieldsFromEncryption.has(field)) {
59
+ excluded.push(field);
60
+ continue;
61
+ }
62
+ if (searchableWhitelist && !searchableWhitelist.has(field)) {
63
+ excluded.push(field);
64
+ continue;
65
+ }
66
+ searchable.push(field);
67
+ }
68
+ return { searchable, hashOnly, excluded };
69
+ }
70
+ export {
71
+ classifyFields,
72
+ extractHashOnlyFields,
73
+ extractSearchableFields
74
+ };
75
+ //# sourceMappingURL=field-policy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/field-policy.ts"],
4
+ "sourcesContent": ["import type { SearchFieldPolicy } from '../types'\n\n/**\n * Encryption map entry as stored in the database.\n * Matches the structure from entities/data/entities EncryptionMap.\n */\nexport type EncryptionMapEntry = {\n field: string\n hashField?: string | null\n}\n\n/**\n * Configuration for field extraction.\n */\nexport type FieldExtractionConfig = {\n /** Encryption map entries from the database */\n encryptedFields?: EncryptionMapEntry[]\n /** Additional field policy from entity search config */\n fieldPolicy?: SearchFieldPolicy\n}\n\n/**\n * Extract only searchable (non-sensitive) fields from a record.\n * This ensures encrypted and sensitive fields are never sent to external search providers.\n *\n * Field filtering logic:\n * 1. Exclude fields in encryption map (they contain encrypted data)\n * 2. Exclude fields in fieldPolicy.excluded\n * 3. Exclude fields in fieldPolicy.hashOnly (should only use hash-based search)\n * 4. If fieldPolicy.searchable is defined, only include those fields (whitelist mode)\n *\n * @param fields - All fields from the record\n * @param config - Extraction configuration with encryption map and field policy\n * @returns Object containing only safe-to-index fields\n */\nexport function extractSearchableFields(\n fields: Record<string, unknown>,\n config?: FieldExtractionConfig,\n): Record<string, unknown> {\n const encryptedFieldSet = new Set<string>(\n config?.encryptedFields?.map((e) => e.field) ?? [],\n )\n\n const policy = config?.fieldPolicy\n const searchableWhitelist = policy?.searchable ? new Set(policy.searchable) : null\n const excludedBlacklist = new Set([\n ...(policy?.excluded ?? []),\n ...(policy?.hashOnly ?? []),\n ])\n\n const result: Record<string, unknown> = {}\n\n for (const [field, value] of Object.entries(fields)) {\n // Skip null/undefined values\n if (value == null) continue\n\n // Skip encrypted fields\n if (encryptedFieldSet.has(field)) continue\n\n // Skip explicitly excluded fields\n if (excludedBlacklist.has(field)) continue\n\n // If whitelist is defined, only include whitelisted fields\n if (searchableWhitelist && !searchableWhitelist.has(field)) continue\n\n result[field] = value\n }\n\n return result\n}\n\n/**\n * Extract fields that should use hash-based search only.\n * These are typically encrypted fields that have corresponding hash columns.\n *\n * @param fields - All fields from the record\n * @param config - Extraction configuration with encryption map and field policy\n * @returns Object containing field values for hash-based search\n */\nexport function extractHashOnlyFields(\n fields: Record<string, unknown>,\n config?: FieldExtractionConfig,\n): Record<string, unknown> {\n const hashOnlyFromPolicy = new Set(config?.fieldPolicy?.hashOnly ?? [])\n\n // Fields with hashField in encryption map are also hash-searchable\n const hashFieldsFromEncryption = new Set<string>(\n config?.encryptedFields\n ?.filter((e) => e.hashField)\n .map((e) => e.field) ?? [],\n )\n\n const result: Record<string, unknown> = {}\n\n for (const [field, value] of Object.entries(fields)) {\n if (value == null) continue\n\n if (hashOnlyFromPolicy.has(field) || hashFieldsFromEncryption.has(field)) {\n result[field] = value\n }\n }\n\n return result\n}\n\n/**\n * Build a complete field classification for a record.\n * Useful for debugging and understanding how fields will be indexed.\n *\n * @param fields - All fields from the record\n * @param config - Extraction configuration\n * @returns Classification of each field\n */\nexport function classifyFields(\n fields: Record<string, unknown>,\n config?: FieldExtractionConfig,\n): {\n searchable: string[]\n hashOnly: string[]\n excluded: string[]\n} {\n const searchable: string[] = []\n const hashOnly: string[] = []\n const excluded: string[] = []\n\n const encryptedFieldSet = new Set<string>(\n config?.encryptedFields?.map((e) => e.field) ?? [],\n )\n const hashFieldsFromEncryption = new Set<string>(\n config?.encryptedFields\n ?.filter((e) => e.hashField)\n .map((e) => e.field) ?? [],\n )\n\n const policy = config?.fieldPolicy\n const searchableWhitelist = policy?.searchable ? new Set(policy.searchable) : null\n const hashOnlyFromPolicy = new Set(policy?.hashOnly ?? [])\n const excludedFromPolicy = new Set(policy?.excluded ?? [])\n\n for (const field of Object.keys(fields)) {\n // Check explicit exclusions\n if (excludedFromPolicy.has(field)) {\n excluded.push(field)\n continue\n }\n\n // Check hash-only\n if (hashOnlyFromPolicy.has(field) || hashFieldsFromEncryption.has(field)) {\n hashOnly.push(field)\n continue\n }\n\n // Check encrypted (without hash)\n if (encryptedFieldSet.has(field) && !hashFieldsFromEncryption.has(field)) {\n excluded.push(field)\n continue\n }\n\n // Check whitelist if defined\n if (searchableWhitelist && !searchableWhitelist.has(field)) {\n excluded.push(field)\n continue\n }\n\n searchable.push(field)\n }\n\n return { searchable, hashOnly, excluded }\n}\n"],
5
+ "mappings": "AAmCO,SAAS,wBACd,QACA,QACyB;AACzB,QAAM,oBAAoB,IAAI;AAAA,IAC5B,QAAQ,iBAAiB,IAAI,CAAC,MAAM,EAAE,KAAK,KAAK,CAAC;AAAA,EACnD;AAEA,QAAM,SAAS,QAAQ;AACvB,QAAM,sBAAsB,QAAQ,aAAa,IAAI,IAAI,OAAO,UAAU,IAAI;AAC9E,QAAM,oBAAoB,oBAAI,IAAI;AAAA,IAChC,GAAI,QAAQ,YAAY,CAAC;AAAA,IACzB,GAAI,QAAQ,YAAY,CAAC;AAAA,EAC3B,CAAC;AAED,QAAM,SAAkC,CAAC;AAEzC,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAEnD,QAAI,SAAS,KAAM;AAGnB,QAAI,kBAAkB,IAAI,KAAK,EAAG;AAGlC,QAAI,kBAAkB,IAAI,KAAK,EAAG;AAGlC,QAAI,uBAAuB,CAAC,oBAAoB,IAAI,KAAK,EAAG;AAE5D,WAAO,KAAK,IAAI;AAAA,EAClB;AAEA,SAAO;AACT;AAUO,SAAS,sBACd,QACA,QACyB;AACzB,QAAM,qBAAqB,IAAI,IAAI,QAAQ,aAAa,YAAY,CAAC,CAAC;AAGtE,QAAM,2BAA2B,IAAI;AAAA,IACnC,QAAQ,iBACJ,OAAO,CAAC,MAAM,EAAE,SAAS,EAC1B,IAAI,CAAC,MAAM,EAAE,KAAK,KAAK,CAAC;AAAA,EAC7B;AAEA,QAAM,SAAkC,CAAC;AAEzC,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACnD,QAAI,SAAS,KAAM;AAEnB,QAAI,mBAAmB,IAAI,KAAK,KAAK,yBAAyB,IAAI,KAAK,GAAG;AACxE,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,EACF;AAEA,SAAO;AACT;AAUO,SAAS,eACd,QACA,QAKA;AACA,QAAM,aAAuB,CAAC;AAC9B,QAAM,WAAqB,CAAC;AAC5B,QAAM,WAAqB,CAAC;AAE5B,QAAM,oBAAoB,IAAI;AAAA,IAC5B,QAAQ,iBAAiB,IAAI,CAAC,MAAM,EAAE,KAAK,KAAK,CAAC;AAAA,EACnD;AACA,QAAM,2BAA2B,IAAI;AAAA,IACnC,QAAQ,iBACJ,OAAO,CAAC,MAAM,EAAE,SAAS,EAC1B,IAAI,CAAC,MAAM,EAAE,KAAK,KAAK,CAAC;AAAA,EAC7B;AAEA,QAAM,SAAS,QAAQ;AACvB,QAAM,sBAAsB,QAAQ,aAAa,IAAI,IAAI,OAAO,UAAU,IAAI;AAC9E,QAAM,qBAAqB,IAAI,IAAI,QAAQ,YAAY,CAAC,CAAC;AACzD,QAAM,qBAAqB,IAAI,IAAI,QAAQ,YAAY,CAAC,CAAC;AAEzD,aAAW,SAAS,OAAO,KAAK,MAAM,GAAG;AAEvC,QAAI,mBAAmB,IAAI,KAAK,GAAG;AACjC,eAAS,KAAK,KAAK;AACnB;AAAA,IACF;AAGA,QAAI,mBAAmB,IAAI,KAAK,KAAK,yBAAyB,IAAI,KAAK,GAAG;AACxE,eAAS,KAAK,KAAK;AACnB;AAAA,IACF;AAGA,QAAI,kBAAkB,IAAI,KAAK,KAAK,CAAC,yBAAyB,IAAI,KAAK,GAAG;AACxE,eAAS,KAAK,KAAK;AACnB;AAAA,IACF;AAGA,QAAI,uBAAuB,CAAC,oBAAoB,IAAI,KAAK,GAAG;AAC1D,eAAS,KAAK,KAAK;AACnB;AAAA,IACF;AAEA,eAAW,KAAK,KAAK;AAAA,EACvB;AAEA,SAAO,EAAE,YAAY,UAAU,SAAS;AAC1C;",
6
+ "names": []
7
+ }
@@ -0,0 +1,19 @@
1
+ import {
2
+ mergeAndRankResults,
3
+ deduplicateResults,
4
+ normalizeScores
5
+ } from "./merger.js";
6
+ import {
7
+ extractSearchableFields,
8
+ extractHashOnlyFields,
9
+ classifyFields
10
+ } from "./field-policy.js";
11
+ export {
12
+ classifyFields,
13
+ deduplicateResults,
14
+ extractHashOnlyFields,
15
+ extractSearchableFields,
16
+ mergeAndRankResults,
17
+ normalizeScores
18
+ };
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/index.ts"],
4
+ "sourcesContent": ["export {\n mergeAndRankResults,\n deduplicateResults,\n normalizeScores,\n} from './merger'\n\nexport {\n extractSearchableFields,\n extractHashOnlyFields,\n classifyFields,\n type EncryptionMapEntry,\n type FieldExtractionConfig,\n} from './field-policy'\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;",
6
+ "names": []
7
+ }
@@ -0,0 +1,93 @@
1
+ const RRF_K = 60;
2
+ function mergeAndRankResults(results, config) {
3
+ if (results.length === 0) return [];
4
+ const bySource = /* @__PURE__ */ new Map();
5
+ for (const result of results) {
6
+ const list = bySource.get(result.source) ?? [];
7
+ list.push(result);
8
+ bySource.set(result.source, list);
9
+ }
10
+ const seen = /* @__PURE__ */ new Map();
11
+ for (const [source, sourceResults] of bySource) {
12
+ const weight = config.strategyWeights?.[source] ?? 1;
13
+ for (let rank = 0; rank < sourceResults.length; rank++) {
14
+ const result = sourceResults[rank];
15
+ const key = `${result.entityId}:${result.recordId}`;
16
+ const rrfScore = weight / (RRF_K + rank + 1);
17
+ const existing = seen.get(key);
18
+ if (existing) {
19
+ existing.rrf += rrfScore;
20
+ existing.sources.add(source);
21
+ const hasExistingPresenter = existing.result.presenter?.title != null;
22
+ const hasNewPresenter = result.presenter?.title != null;
23
+ if (!hasExistingPresenter && hasNewPresenter) {
24
+ existing.result = {
25
+ ...existing.result,
26
+ presenter: result.presenter,
27
+ url: existing.result.url ?? result.url,
28
+ links: existing.result.links ?? result.links
29
+ };
30
+ existing.bestContribution = Math.max(existing.bestContribution, rrfScore);
31
+ } else if (hasExistingPresenter && hasNewPresenter && rrfScore > existing.bestContribution) {
32
+ existing.result = { ...result };
33
+ existing.bestContribution = rrfScore;
34
+ } else if (!hasExistingPresenter && !hasNewPresenter && rrfScore > existing.bestContribution) {
35
+ existing.result = { ...result };
36
+ existing.bestContribution = rrfScore;
37
+ }
38
+ } else {
39
+ seen.set(key, {
40
+ result: { ...result },
41
+ rrf: rrfScore,
42
+ sources: /* @__PURE__ */ new Set([source]),
43
+ bestContribution: rrfScore
44
+ });
45
+ }
46
+ }
47
+ }
48
+ let merged = Array.from(seen.values()).map(({ result, rrf, sources }) => ({
49
+ ...result,
50
+ score: rrf,
51
+ metadata: {
52
+ ...result.metadata,
53
+ _sources: Array.from(sources),
54
+ _rrfScore: rrf
55
+ }
56
+ }));
57
+ if (config.minScore != null) {
58
+ merged = merged.filter((r) => r.score >= config.minScore);
59
+ }
60
+ merged.sort((a, b) => b.score - a.score);
61
+ return merged;
62
+ }
63
+ function deduplicateResults(results) {
64
+ const seen = /* @__PURE__ */ new Map();
65
+ for (const result of results) {
66
+ const key = `${result.entityId}:${result.recordId}`;
67
+ const existing = seen.get(key);
68
+ if (!existing || result.score > existing.score) {
69
+ seen.set(key, result);
70
+ }
71
+ }
72
+ return Array.from(seen.values()).sort((a, b) => b.score - a.score);
73
+ }
74
+ function normalizeScores(results) {
75
+ if (results.length === 0) return [];
76
+ const scores = results.map((r) => r.score);
77
+ const minScore = Math.min(...scores);
78
+ const maxScore = Math.max(...scores);
79
+ const range = maxScore - minScore;
80
+ if (range === 0) {
81
+ return results.map((r) => ({ ...r, score: 1 }));
82
+ }
83
+ return results.map((r) => ({
84
+ ...r,
85
+ score: (r.score - minScore) / range
86
+ }));
87
+ }
88
+ export {
89
+ deduplicateResults,
90
+ mergeAndRankResults,
91
+ normalizeScores
92
+ };
93
+ //# sourceMappingURL=merger.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/merger.ts"],
4
+ "sourcesContent": ["import type { SearchResult, ResultMergeConfig, SearchStrategyId } from '../types'\n\n/**\n * Default RRF constant (k=60 is standard in literature).\n * Higher values reduce the influence of ranking position.\n */\nconst RRF_K = 60\n\n/**\n * Reciprocal Rank Fusion (RRF) algorithm for combining results from multiple search strategies.\n *\n * RRF is a simple but effective method for combining ranked lists. For each result,\n * it computes: score = sum(weight / (k + rank)) across all lists containing that result.\n *\n * Reference: Cormack, G.V., Clarke, C.L.A., & Buettcher, S. (2009).\n * \"Reciprocal rank fusion outperforms condorcet and individual rank learning methods\"\n * https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf\n *\n * @param results - Array of search results from multiple strategies\n * @param config - Merge configuration with weights and thresholds\n * @returns Merged and ranked results\n */\nexport function mergeAndRankResults(\n results: SearchResult[],\n config: ResultMergeConfig,\n): SearchResult[] {\n if (results.length === 0) return []\n\n // Group results by source strategy for rank calculation\n const bySource = new Map<SearchStrategyId, SearchResult[]>()\n for (const result of results) {\n const list = bySource.get(result.source) ?? []\n list.push(result)\n bySource.set(result.source, list)\n }\n\n // Track seen results with their RRF scores\n // bestContribution tracks the highest single RRF contribution for the kept result object\n const seen = new Map<string, { result: SearchResult; rrf: number; sources: Set<SearchStrategyId>; bestContribution: number }>()\n\n // Calculate RRF score for each result\n for (const [source, sourceResults] of bySource) {\n const weight = config.strategyWeights?.[source] ?? 1.0\n\n for (let rank = 0; rank < sourceResults.length; rank++) {\n const result = sourceResults[rank]\n const key = `${result.entityId}:${result.recordId}`\n const rrfScore = weight / (RRF_K + rank + 1)\n\n const existing = seen.get(key)\n if (existing) {\n // Combine RRF scores for duplicates found in multiple strategies\n existing.rrf += rrfScore\n existing.sources.add(source)\n\n // Merge presenter data - prefer result that has it\n // This ensures token results get enriched with presenter from meilisearch/vector\n const hasExistingPresenter = existing.result.presenter?.title != null\n const hasNewPresenter = result.presenter?.title != null\n\n if (!hasExistingPresenter && hasNewPresenter) {\n // Current result has no presenter, new one does - take new one's presenter\n existing.result = {\n ...existing.result,\n presenter: result.presenter,\n url: existing.result.url ?? result.url,\n links: existing.result.links ?? result.links,\n }\n existing.bestContribution = Math.max(existing.bestContribution, rrfScore)\n } else if (hasExistingPresenter && hasNewPresenter && rrfScore > existing.bestContribution) {\n // Both have presenter, keep the one with better RRF contribution (not raw score)\n existing.result = { ...result }\n existing.bestContribution = rrfScore\n } else if (!hasExistingPresenter && !hasNewPresenter && rrfScore > existing.bestContribution) {\n // Neither has presenter, keep result with better RRF contribution\n existing.result = { ...result }\n existing.bestContribution = rrfScore\n }\n // If existing has presenter and new doesn't, keep existing (do nothing)\n } else {\n seen.set(key, {\n result: { ...result },\n rrf: rrfScore,\n sources: new Set([source]),\n bestContribution: rrfScore,\n })\n }\n }\n }\n\n // Convert to array with final RRF scores\n let merged = Array.from(seen.values()).map(({ result, rrf, sources }) => ({\n ...result,\n score: rrf,\n metadata: {\n ...result.metadata,\n _sources: Array.from(sources),\n _rrfScore: rrf,\n },\n }))\n\n // Apply minimum score threshold\n if (config.minScore != null) {\n merged = merged.filter((r) => r.score >= config.minScore!)\n }\n\n // Sort by RRF score descending\n merged.sort((a, b) => b.score - a.score)\n\n return merged\n}\n\n/**\n * Simple deduplication without RRF scoring.\n * Keeps the highest-scored result for each entity+record pair.\n *\n * @param results - Array of search results\n * @returns Deduplicated results sorted by score\n */\nexport function deduplicateResults(results: SearchResult[]): SearchResult[] {\n const seen = new Map<string, SearchResult>()\n\n for (const result of results) {\n const key = `${result.entityId}:${result.recordId}`\n const existing = seen.get(key)\n\n if (!existing || result.score > existing.score) {\n seen.set(key, result)\n }\n }\n\n return Array.from(seen.values()).sort((a, b) => b.score - a.score)\n}\n\n/**\n * Normalize scores to 0-1 range using min-max normalization.\n * Useful when combining strategies with different score scales.\n *\n * @param results - Array of search results\n * @returns Results with normalized scores\n */\nexport function normalizeScores(results: SearchResult[]): SearchResult[] {\n if (results.length === 0) return []\n\n const scores = results.map((r) => r.score)\n const minScore = Math.min(...scores)\n const maxScore = Math.max(...scores)\n const range = maxScore - minScore\n\n if (range === 0) {\n // All scores are the same, normalize to 1.0\n return results.map((r) => ({ ...r, score: 1.0 }))\n }\n\n return results.map((r) => ({\n ...r,\n score: (r.score - minScore) / range,\n }))\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }