@open-mercato/search 0.10.1-develop.7769.1.0e6458bb99 → 0.10.1-develop.7771.1.615a6c9ba9
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.
- package/dist/di.js +8 -2
- package/dist/di.js.map +2 -2
- package/dist/indexer/search-indexer.js +15 -50
- package/dist/indexer/search-indexer.js.map +2 -2
- package/dist/lib/presenter-enricher.js +2 -1
- package/dist/lib/presenter-enricher.js.map +2 -2
- package/dist/strategies/vector.strategy.js +2 -2
- package/dist/strategies/vector.strategy.js.map +2 -2
- package/dist/vector/services/vector-index.service.js +9 -5
- package/dist/vector/services/vector-index.service.js.map +2 -2
- package/package.json +4 -4
- package/src/__tests__/presenter-enricher.test.ts +20 -0
- package/src/__tests__/vector-index-security.test.ts +280 -1
- package/src/di.ts +10 -4
- package/src/indexer/search-indexer.ts +16 -60
- package/src/lib/presenter-enricher.ts +2 -1
- package/src/strategies/vector.strategy.ts +2 -2
- package/src/vector/services/vector-index.service.ts +9 -5
package/dist/di.js
CHANGED
|
@@ -22,6 +22,13 @@ function createUnavailableEncryptionMapResolver() {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
function registerSearchModule(container, options) {
|
|
25
|
+
const moduleConfigs = (options?.moduleConfigs ?? []).map((moduleConfig) => ({
|
|
26
|
+
...moduleConfig,
|
|
27
|
+
entities: moduleConfig.entities.map((entityConfig) => {
|
|
28
|
+
const buildSource = entityConfig.buildSource;
|
|
29
|
+
return buildSource ? { ...entityConfig, buildSource: (ctx) => buildSource.call(entityConfig, { ...ctx, container }) } : entityConfig;
|
|
30
|
+
})
|
|
31
|
+
}));
|
|
25
32
|
const strategies = [];
|
|
26
33
|
if (!options?.skipTokens) {
|
|
27
34
|
try {
|
|
@@ -43,7 +50,7 @@ function registerSearchModule(container, options) {
|
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
const entityConfigMap = /* @__PURE__ */ new Map();
|
|
46
|
-
for (const moduleConfig of
|
|
53
|
+
for (const moduleConfig of moduleConfigs) {
|
|
47
54
|
for (const entityConfig of moduleConfig.entities) {
|
|
48
55
|
if (entityConfig.enabled !== false) {
|
|
49
56
|
entityConfigMap.set(entityConfig.entityId, entityConfig);
|
|
@@ -114,7 +121,6 @@ function registerSearchModule(container, options) {
|
|
|
114
121
|
},
|
|
115
122
|
presenterEnricher
|
|
116
123
|
});
|
|
117
|
-
const moduleConfigs = options?.moduleConfigs ?? [];
|
|
118
124
|
let fulltextQueue;
|
|
119
125
|
try {
|
|
120
126
|
fulltextQueue = container.resolve("fulltextIndexQueue");
|
package/dist/di.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { Kysely } from 'kysely'\nimport type { FullTextSearchDriver } from './fulltext/types'\nimport { SearchService } from './service'\nimport { TokenSearchStrategy } from './strategies/token.strategy'\nimport { VectorSearchStrategy, type EmbeddingService } from './strategies/vector.strategy'\nimport { FullTextSearchStrategy } from './strategies/fulltext.strategy'\nimport { createFulltextDriver } from './fulltext/drivers'\nimport { SearchIndexer } from './indexer/search-indexer'\nimport type {\n SearchStrategy,\n ResultMergeConfig,\n SearchModuleConfig,\n SearchFieldPolicy,\n SearchEntityConfig,\n PresenterEnricherFn,\n} from './types'\nimport type { VectorDriver } from './vector/types'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport type { Queue } from '@open-mercato/queue'\nimport type { FulltextIndexJobPayload } from './queue/fulltext-indexing'\nimport type { VectorIndexJobPayload } from './queue/vector-indexing'\nimport type { EncryptionMapEntry } from './lib/field-policy'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { createPresenterEnricher } from './lib/presenter-enricher'\nimport { createEncryptionMapResolver } from './lib/encryption-map-resolver'\n\nexport { createEncryptionMapResolver } from './lib/encryption-map-resolver'\n\nconst FULLTEXT_DRIVER_KEY = '__omSearchFulltextDriver__'\n\ntype SearchModuleGlobals = {\n [FULLTEXT_DRIVER_KEY]?: FullTextSearchDriver\n}\n\nfunction getSearchModuleGlobals(): SearchModuleGlobals {\n return globalThis as unknown as SearchModuleGlobals\n}\n\n/**\n * Check if encrypted fields should be excluded from search indexing.\n * Controlled by SEARCH_EXCLUDE_ENCRYPTED_FIELDS environment variable.\n * Default: false (index all fields including decrypted data)\n */\nfunction shouldExcludeEncryptedFields(): boolean {\n const raw = (process.env.SEARCH_EXCLUDE_ENCRYPTED_FIELDS ?? '').toLowerCase()\n return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on'\n}\n\nfunction createUnavailableEncryptionMapResolver(): (entityId: EntityId) => Promise<EncryptionMapEntry[]> {\n return async () => {\n throw new Error('[internal] Encryption map lookup is unavailable; refusing to index potentially encrypted fields')\n }\n}\n\n/**\n * Container interface - minimal subset needed for registration.\n */\nexport interface SearchContainer {\n resolve<T = unknown>(name: string): T\n register(registrations: Record<string, unknown>): void\n}\n\n/**\n * Configuration options for search module registration.\n */\nexport type SearchModuleOptions = {\n /** Override default strategies to use */\n defaultStrategies?: SearchStrategyId[]\n /** Override merge configuration */\n mergeConfig?: ResultMergeConfig\n /** Skip token strategy registration */\n skipTokens?: boolean\n /** Skip vector strategy registration */\n skipVector?: boolean\n /** Skip fulltext strategy registration */\n skipFulltext?: boolean\n /** Module configurations (from generated/search.generated.ts) */\n moduleConfigs?: SearchModuleConfig[]\n}\n\n/**\n * Register the search module in the DI container.\n *\n * This creates and registers:\n * - SearchService instance\n * - All configured search strategies\n *\n * @param container - Awilix container\n * @param options - Optional configuration overrides\n */\nexport function registerSearchModule(\n container: SearchContainer,\n options?: SearchModuleOptions,\n): void {\n const strategies: SearchStrategy[] = []\n\n // Token strategy (always available unless explicitly skipped)\n if (!options?.skipTokens) {\n try {\n const em = container.resolve<any>('em')\n const db = em.getKysely() as Kysely<any>\n strategies.push(new TokenSearchStrategy(db))\n } catch {\n // Kysely not available via em, skipping TokenSearchStrategy\n }\n }\n\n // Vector strategy (requires embedding service and driver)\n // Note: We register even if not currently available - availability is checked at search time\n // via isAvailable(). The embedding config may be loaded later from the database.\n if (!options?.skipVector) {\n try {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const primaryDriver = drivers?.[0]\n\n if (embeddingService && primaryDriver) {\n strategies.push(new VectorSearchStrategy(embeddingService, primaryDriver))\n }\n } catch {\n // Vector module not available, skipping VectorSearchStrategy\n }\n }\n\n // Build entity config map for field policy resolution\n const entityConfigMap = new Map<EntityId, SearchEntityConfig>()\n for (const moduleConfig of (options?.moduleConfigs ?? [])) {\n for (const entityConfig of moduleConfig.entities) {\n if (entityConfig.enabled !== false) {\n entityConfigMap.set(entityConfig.entityId as EntityId, entityConfig)\n }\n }\n }\n\n // Fulltext strategy (requires driver configuration, e.g., MEILISEARCH_HOST)\n if (!options?.skipFulltext) {\n const excludeEncrypted = shouldExcludeEncryptedFields()\n const singletonCacheEnabled = process.env.SEARCH_DISABLE_SINGLETON_CACHE !== '1'\n const g = getSearchModuleGlobals()\n\n // The fulltext driver is safe to memoize only when SEARCH_EXCLUDE_ENCRYPTED_FIELDS is\n // OFF \u2014 in that case the driver holds no request-scoped Kysely reference. When the flag\n // is ON the encryptionMapResolver captures a per-request db handle and must stay\n // per-request to avoid cross-request data leaks.\n const canMemoize = singletonCacheEnabled && !excludeEncrypted\n\n let fulltextDriver: FullTextSearchDriver | null = canMemoize\n ? (g[FULLTEXT_DRIVER_KEY] ?? null)\n : null\n\n if (!fulltextDriver) {\n let encryptionMapResolver: ((entityId: EntityId) => Promise<EncryptionMapEntry[]>) | undefined\n if (excludeEncrypted) {\n try {\n const em = container.resolve<any>('em')\n const db = em.getKysely() as Kysely<any>\n encryptionMapResolver = createEncryptionMapResolver(db)\n } catch {\n encryptionMapResolver = createUnavailableEncryptionMapResolver()\n }\n }\n\n fulltextDriver = createFulltextDriver({\n fieldPolicyResolver: (entityId: EntityId): SearchFieldPolicy | undefined => {\n const config = entityConfigMap.get(entityId)\n return config?.fieldPolicy\n },\n encryptionMapResolver,\n })\n\n if (canMemoize && fulltextDriver) {\n g[FULLTEXT_DRIVER_KEY] = fulltextDriver\n }\n }\n\n if (fulltextDriver) {\n strategies.push(new FullTextSearchStrategy(fulltextDriver))\n }\n }\n\n // Determine default strategies based on what's available\n const defaultStrategies = options?.defaultStrategies ?? determineDefaultStrategies(strategies)\n\n // Try to resolve queryEngine for reindex support and presenter enrichment\n let queryEngine: QueryEngine | undefined\n try {\n queryEngine = container.resolve<QueryEngine>('queryEngine')\n } catch {\n // QueryEngine not available, reindex will be disabled\n }\n\n // Resolve encryption service for decrypting presenter data\n let encryptionService: TenantDataEncryptionService | null = null\n try {\n encryptionService = container.resolve<TenantDataEncryptionService>('tenantEncryptionService')\n } catch {\n // Encryption service not available, presenters won't be decrypted\n }\n\n // Create presenter enricher for database-based presenter resolution\n let presenterEnricher: PresenterEnricherFn | undefined\n try {\n const em = container.resolve<any>('em')\n const db = em.getKysely() as Kysely<any>\n presenterEnricher = createPresenterEnricher(db, entityConfigMap, queryEngine, encryptionService)\n } catch {\n // Kysely not available, presenter enrichment disabled\n }\n\n // Create search service\n const searchService = new SearchService({\n strategies,\n defaultStrategies,\n fallbackStrategy: 'tokens',\n mergeConfig: options?.mergeConfig ?? {\n duplicateHandling: 'highest_score',\n strategyWeights: {\n fulltext: 1.2,\n vector: 1.0,\n tokens: 0.8,\n },\n },\n presenterEnricher,\n })\n\n // Create search indexer with module configs\n const moduleConfigs = options?.moduleConfigs ?? []\n\n // Try to resolve fulltextIndexQueue for queue-based reindexing\n let fulltextQueue: Queue<FulltextIndexJobPayload> | undefined\n try {\n fulltextQueue = container.resolve<Queue<FulltextIndexJobPayload>>('fulltextIndexQueue')\n } catch {\n // Queue not available, queue-based fulltext reindex will be disabled\n }\n\n // Try to resolve vectorIndexQueue for queue-based vector reindexing\n let vectorQueue: Queue<VectorIndexJobPayload> | undefined\n try {\n vectorQueue = container.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')\n } catch {\n // Queue not available, queue-based vector reindex will be disabled\n }\n\n const searchIndexer = new SearchIndexer(searchService, moduleConfigs, {\n queryEngine,\n fulltextQueue,\n vectorQueue,\n })\n\n // Register in container\n container.register({\n searchService: asValue(searchService),\n searchStrategies: asValue(strategies),\n searchIndexer: asValue(searchIndexer),\n })\n}\n\n/**\n * Determine default strategy order based on available strategies.\n * Prefers fulltext > vector > tokens.\n */\nfunction determineDefaultStrategies(strategies: SearchStrategy[]): SearchStrategyId[] {\n const available = new Set(strategies.map((s) => s.id))\n const defaults: SearchStrategyId[] = []\n\n if (available.has('fulltext')) defaults.push('fulltext')\n if (available.has('vector')) defaults.push('vector')\n if (available.has('tokens')) defaults.push('tokens')\n\n return defaults.length > 0 ? defaults : ['tokens']\n}\n\n/**\n * Helper to add a custom strategy to an existing SearchService.\n *\n * @param container - DI container\n * @param strategy - Strategy to add\n */\nexport function addSearchStrategy(container: SearchContainer, strategy: SearchStrategy): void {\n const service = container.resolve<SearchService>('searchService')\n service.registerStrategy(strategy)\n\n const strategies = container.resolve<SearchStrategy[]>('searchStrategies')\n strategies.push(strategy)\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe;AAGxB,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC,SAAS,4BAAmD;AAC5D,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,qBAAqB;AAkB9B,SAAS,+BAA+B;AACxC,SAAS,mCAAmC;AAE5C,SAAS,+BAAAA,oCAAmC;AAE5C,MAAM,sBAAsB;AAM5B,SAAS,yBAA8C;AACrD,SAAO;AACT;AAOA,SAAS,+BAAwC;AAC/C,QAAM,OAAO,QAAQ,IAAI,mCAAmC,IAAI,YAAY;AAC5E,SAAO,QAAQ,OAAO,QAAQ,UAAU,QAAQ,SAAS,QAAQ;AACnE;AAEA,SAAS,yCAAgG;AACvG,SAAO,YAAY;AACjB,UAAM,IAAI,MAAM,iGAAiG;AAAA,EACnH;AACF;AAsCO,SAAS,qBACd,WACA,SACM;AACN,QAAM,aAA+B,CAAC;AAGtC,MAAI,CAAC,SAAS,YAAY;AACxB,QAAI;AACF,YAAM,KAAK,UAAU,QAAa,IAAI;AACtC,YAAM,KAAK,GAAG,UAAU;AACxB,iBAAW,KAAK,IAAI,oBAAoB,EAAE,CAAC;AAAA,IAC7C,QAAQ;AAAA,IAER;AAAA,EACF;AAKA,MAAI,CAAC,SAAS,YAAY;AACxB,QAAI;AACF,YAAM,mBAAmB,UAAU,QAA0B,wBAAwB;AACrF,YAAM,UAAU,UAAU,QAAwB,eAAe;AACjE,YAAM,gBAAgB,UAAU,CAAC;AAEjC,UAAI,oBAAoB,eAAe;AACrC,mBAAW,KAAK,IAAI,qBAAqB,kBAAkB,aAAa,CAAC;AAAA,MAC3E;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,kBAAkB,oBAAI,IAAkC;AAC9D,aAAW,
|
|
4
|
+
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { Kysely } from 'kysely'\nimport type { FullTextSearchDriver } from './fulltext/types'\nimport { SearchService } from './service'\nimport { TokenSearchStrategy } from './strategies/token.strategy'\nimport { VectorSearchStrategy, type EmbeddingService } from './strategies/vector.strategy'\nimport { FullTextSearchStrategy } from './strategies/fulltext.strategy'\nimport { createFulltextDriver } from './fulltext/drivers'\nimport { SearchIndexer } from './indexer/search-indexer'\nimport type {\n SearchStrategy,\n ResultMergeConfig,\n SearchModuleConfig,\n SearchFieldPolicy,\n SearchEntityConfig,\n PresenterEnricherFn,\n} from './types'\nimport type { VectorDriver } from './vector/types'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport type { Queue } from '@open-mercato/queue'\nimport type { FulltextIndexJobPayload } from './queue/fulltext-indexing'\nimport type { VectorIndexJobPayload } from './queue/vector-indexing'\nimport type { EncryptionMapEntry } from './lib/field-policy'\nimport type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { createPresenterEnricher } from './lib/presenter-enricher'\nimport { createEncryptionMapResolver } from './lib/encryption-map-resolver'\n\nexport { createEncryptionMapResolver } from './lib/encryption-map-resolver'\n\nconst FULLTEXT_DRIVER_KEY = '__omSearchFulltextDriver__'\n\ntype SearchModuleGlobals = {\n [FULLTEXT_DRIVER_KEY]?: FullTextSearchDriver\n}\n\nfunction getSearchModuleGlobals(): SearchModuleGlobals {\n return globalThis as unknown as SearchModuleGlobals\n}\n\n/**\n * Check if encrypted fields should be excluded from search indexing.\n * Controlled by SEARCH_EXCLUDE_ENCRYPTED_FIELDS environment variable.\n * Default: false (index all fields including decrypted data)\n */\nfunction shouldExcludeEncryptedFields(): boolean {\n const raw = (process.env.SEARCH_EXCLUDE_ENCRYPTED_FIELDS ?? '').toLowerCase()\n return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on'\n}\n\nfunction createUnavailableEncryptionMapResolver(): (entityId: EntityId) => Promise<EncryptionMapEntry[]> {\n return async () => {\n throw new Error('[internal] Encryption map lookup is unavailable; refusing to index potentially encrypted fields')\n }\n}\n\n/**\n * Container interface - minimal subset needed for registration.\n */\nexport interface SearchContainer {\n resolve<T = unknown>(name: string): T\n register(registrations: Record<string, unknown>): void\n}\n\n/**\n * Configuration options for search module registration.\n */\nexport type SearchModuleOptions = {\n /** Override default strategies to use */\n defaultStrategies?: SearchStrategyId[]\n /** Override merge configuration */\n mergeConfig?: ResultMergeConfig\n /** Skip token strategy registration */\n skipTokens?: boolean\n /** Skip vector strategy registration */\n skipVector?: boolean\n /** Skip fulltext strategy registration */\n skipFulltext?: boolean\n /** Module configurations (from generated/search.generated.ts) */\n moduleConfigs?: SearchModuleConfig[]\n}\n\n/**\n * Register the search module in the DI container.\n *\n * This creates and registers:\n * - SearchService instance\n * - All configured search strategies\n *\n * @param container - Awilix container\n * @param options - Optional configuration overrides\n */\nexport function registerSearchModule(\n container: SearchContainer,\n options?: SearchModuleOptions,\n): void {\n const moduleConfigs = (options?.moduleConfigs ?? []).map((moduleConfig) => ({\n ...moduleConfig,\n entities: moduleConfig.entities.map((entityConfig) => {\n const buildSource = entityConfig.buildSource\n return buildSource\n ? { ...entityConfig, buildSource: (ctx: Parameters<typeof buildSource>[0]) => buildSource.call(entityConfig, { ...ctx, container }) }\n : entityConfig\n }),\n }))\n const strategies: SearchStrategy[] = []\n\n // Token strategy (always available unless explicitly skipped)\n if (!options?.skipTokens) {\n try {\n const em = container.resolve<any>('em')\n const db = em.getKysely() as Kysely<any>\n strategies.push(new TokenSearchStrategy(db))\n } catch {\n // Kysely not available via em, skipping TokenSearchStrategy\n }\n }\n\n // Vector strategy (requires embedding service and driver)\n // Note: We register even if not currently available - availability is checked at search time\n // via isAvailable(). The embedding config may be loaded later from the database.\n if (!options?.skipVector) {\n try {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n const drivers = container.resolve<VectorDriver[]>('vectorDrivers')\n const primaryDriver = drivers?.[0]\n\n if (embeddingService && primaryDriver) {\n strategies.push(new VectorSearchStrategy(embeddingService, primaryDriver))\n }\n } catch {\n // Vector module not available, skipping VectorSearchStrategy\n }\n }\n\n // Build entity config map for field policy resolution\n const entityConfigMap = new Map<EntityId, SearchEntityConfig>()\n for (const moduleConfig of moduleConfigs) {\n for (const entityConfig of moduleConfig.entities) {\n if (entityConfig.enabled !== false) {\n entityConfigMap.set(entityConfig.entityId as EntityId, entityConfig)\n }\n }\n }\n\n // Fulltext strategy (requires driver configuration, e.g., MEILISEARCH_HOST)\n if (!options?.skipFulltext) {\n const excludeEncrypted = shouldExcludeEncryptedFields()\n const singletonCacheEnabled = process.env.SEARCH_DISABLE_SINGLETON_CACHE !== '1'\n const g = getSearchModuleGlobals()\n\n // The fulltext driver is safe to memoize only when SEARCH_EXCLUDE_ENCRYPTED_FIELDS is\n // OFF \u2014 in that case the driver holds no request-scoped Kysely reference. When the flag\n // is ON the encryptionMapResolver captures a per-request db handle and must stay\n // per-request to avoid cross-request data leaks.\n const canMemoize = singletonCacheEnabled && !excludeEncrypted\n\n let fulltextDriver: FullTextSearchDriver | null = canMemoize\n ? (g[FULLTEXT_DRIVER_KEY] ?? null)\n : null\n\n if (!fulltextDriver) {\n let encryptionMapResolver: ((entityId: EntityId) => Promise<EncryptionMapEntry[]>) | undefined\n if (excludeEncrypted) {\n try {\n const em = container.resolve<any>('em')\n const db = em.getKysely() as Kysely<any>\n encryptionMapResolver = createEncryptionMapResolver(db)\n } catch {\n encryptionMapResolver = createUnavailableEncryptionMapResolver()\n }\n }\n\n fulltextDriver = createFulltextDriver({\n fieldPolicyResolver: (entityId: EntityId): SearchFieldPolicy | undefined => {\n const config = entityConfigMap.get(entityId)\n return config?.fieldPolicy\n },\n encryptionMapResolver,\n })\n\n if (canMemoize && fulltextDriver) {\n g[FULLTEXT_DRIVER_KEY] = fulltextDriver\n }\n }\n\n if (fulltextDriver) {\n strategies.push(new FullTextSearchStrategy(fulltextDriver))\n }\n }\n\n // Determine default strategies based on what's available\n const defaultStrategies = options?.defaultStrategies ?? determineDefaultStrategies(strategies)\n\n // Try to resolve queryEngine for reindex support and presenter enrichment\n let queryEngine: QueryEngine | undefined\n try {\n queryEngine = container.resolve<QueryEngine>('queryEngine')\n } catch {\n // QueryEngine not available, reindex will be disabled\n }\n\n // Resolve encryption service for decrypting presenter data\n let encryptionService: TenantDataEncryptionService | null = null\n try {\n encryptionService = container.resolve<TenantDataEncryptionService>('tenantEncryptionService')\n } catch {\n // Encryption service not available, presenters won't be decrypted\n }\n\n // Create presenter enricher for database-based presenter resolution\n let presenterEnricher: PresenterEnricherFn | undefined\n try {\n const em = container.resolve<any>('em')\n const db = em.getKysely() as Kysely<any>\n presenterEnricher = createPresenterEnricher(db, entityConfigMap, queryEngine, encryptionService)\n } catch {\n // Kysely not available, presenter enrichment disabled\n }\n\n // Create search service\n const searchService = new SearchService({\n strategies,\n defaultStrategies,\n fallbackStrategy: 'tokens',\n mergeConfig: options?.mergeConfig ?? {\n duplicateHandling: 'highest_score',\n strategyWeights: {\n fulltext: 1.2,\n vector: 1.0,\n tokens: 0.8,\n },\n },\n presenterEnricher,\n })\n\n // Try to resolve fulltextIndexQueue for queue-based reindexing\n let fulltextQueue: Queue<FulltextIndexJobPayload> | undefined\n try {\n fulltextQueue = container.resolve<Queue<FulltextIndexJobPayload>>('fulltextIndexQueue')\n } catch {\n // Queue not available, queue-based fulltext reindex will be disabled\n }\n\n // Try to resolve vectorIndexQueue for queue-based vector reindexing\n let vectorQueue: Queue<VectorIndexJobPayload> | undefined\n try {\n vectorQueue = container.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')\n } catch {\n // Queue not available, queue-based vector reindex will be disabled\n }\n\n const searchIndexer = new SearchIndexer(searchService, moduleConfigs, {\n queryEngine,\n fulltextQueue,\n vectorQueue,\n })\n\n // Register in container\n container.register({\n searchService: asValue(searchService),\n searchStrategies: asValue(strategies),\n searchIndexer: asValue(searchIndexer),\n })\n}\n\n/**\n * Determine default strategy order based on available strategies.\n * Prefers fulltext > vector > tokens.\n */\nfunction determineDefaultStrategies(strategies: SearchStrategy[]): SearchStrategyId[] {\n const available = new Set(strategies.map((s) => s.id))\n const defaults: SearchStrategyId[] = []\n\n if (available.has('fulltext')) defaults.push('fulltext')\n if (available.has('vector')) defaults.push('vector')\n if (available.has('tokens')) defaults.push('tokens')\n\n return defaults.length > 0 ? defaults : ['tokens']\n}\n\n/**\n * Helper to add a custom strategy to an existing SearchService.\n *\n * @param container - DI container\n * @param strategy - Strategy to add\n */\nexport function addSearchStrategy(container: SearchContainer, strategy: SearchStrategy): void {\n const service = container.resolve<SearchService>('searchService')\n service.registerStrategy(strategy)\n\n const strategies = container.resolve<SearchStrategy[]>('searchStrategies')\n strategies.push(strategy)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["createEncryptionMapResolver"]
|
|
7
7
|
}
|
|
@@ -51,9 +51,15 @@ class SearchIndexer {
|
|
|
51
51
|
* Index a record in the search service.
|
|
52
52
|
*/
|
|
53
53
|
async indexRecord(params) {
|
|
54
|
+
const indexableRecord = await this.buildIndexableRecord(params);
|
|
55
|
+
if (indexableRecord) {
|
|
56
|
+
await this.searchService.index(indexableRecord);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async buildIndexableRecord(params) {
|
|
54
60
|
const config = this.entityConfigMap.get(params.entityId);
|
|
55
61
|
if (!config || config.enabled === false) {
|
|
56
|
-
return;
|
|
62
|
+
return null;
|
|
57
63
|
}
|
|
58
64
|
const buildContext = {
|
|
59
65
|
record: params.record,
|
|
@@ -62,7 +68,7 @@ class SearchIndexer {
|
|
|
62
68
|
tenantId: params.tenantId,
|
|
63
69
|
queryEngine: this.noReindexQueryEngine
|
|
64
70
|
};
|
|
65
|
-
let text;
|
|
71
|
+
let text = config.buildSource ? [] : void 0;
|
|
66
72
|
let presenter;
|
|
67
73
|
let url;
|
|
68
74
|
let links;
|
|
@@ -71,7 +77,7 @@ class SearchIndexer {
|
|
|
71
77
|
try {
|
|
72
78
|
const source = await config.buildSource(buildContext);
|
|
73
79
|
if (source) {
|
|
74
|
-
text = source.text;
|
|
80
|
+
text = source.text ?? [];
|
|
75
81
|
if (source.presenter) presenter = source.presenter;
|
|
76
82
|
if (source.links) links = source.links;
|
|
77
83
|
if (source.checksumSource !== void 0) checksumSource = source.checksumSource;
|
|
@@ -120,7 +126,7 @@ class SearchIndexer {
|
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
|
-
|
|
129
|
+
return {
|
|
124
130
|
entityId: params.entityId,
|
|
125
131
|
recordId: params.recordId,
|
|
126
132
|
tenantId: params.tenantId,
|
|
@@ -132,7 +138,6 @@ class SearchIndexer {
|
|
|
132
138
|
text,
|
|
133
139
|
checksumSource
|
|
134
140
|
};
|
|
135
|
-
await this.searchService.index(indexableRecord);
|
|
136
141
|
}
|
|
137
142
|
/**
|
|
138
143
|
* Index a record by ID (loads the record from database first).
|
|
@@ -381,50 +386,10 @@ class SearchIndexer {
|
|
|
381
386
|
async bulkIndexRecords(params) {
|
|
382
387
|
const indexableRecords = [];
|
|
383
388
|
for (const param of params) {
|
|
384
|
-
const
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
const buildContext = {
|
|
389
|
-
record: param.record,
|
|
390
|
-
customFields: param.customFields ?? {},
|
|
391
|
-
organizationId: param.organizationId,
|
|
392
|
-
tenantId: param.tenantId
|
|
393
|
-
};
|
|
394
|
-
let presenter;
|
|
395
|
-
if (config.formatResult) {
|
|
396
|
-
try {
|
|
397
|
-
const result = await config.formatResult(buildContext);
|
|
398
|
-
if (result) presenter = result;
|
|
399
|
-
} catch {
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
let url;
|
|
403
|
-
if (config.resolveUrl) {
|
|
404
|
-
try {
|
|
405
|
-
const result = await config.resolveUrl(buildContext);
|
|
406
|
-
if (result) url = result;
|
|
407
|
-
} catch {
|
|
408
|
-
}
|
|
389
|
+
const indexableRecord = await this.buildIndexableRecord(param);
|
|
390
|
+
if (indexableRecord) {
|
|
391
|
+
indexableRecords.push(indexableRecord);
|
|
409
392
|
}
|
|
410
|
-
let links;
|
|
411
|
-
if (config.resolveLinks) {
|
|
412
|
-
try {
|
|
413
|
-
const result = await config.resolveLinks(buildContext);
|
|
414
|
-
if (result) links = result;
|
|
415
|
-
} catch {
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
indexableRecords.push({
|
|
419
|
-
entityId: param.entityId,
|
|
420
|
-
recordId: param.recordId,
|
|
421
|
-
tenantId: param.tenantId,
|
|
422
|
-
organizationId: param.organizationId,
|
|
423
|
-
fields: param.record,
|
|
424
|
-
presenter,
|
|
425
|
-
url,
|
|
426
|
-
links
|
|
427
|
-
});
|
|
428
393
|
}
|
|
429
394
|
if (indexableRecords.length > 0) {
|
|
430
395
|
try {
|
|
@@ -865,7 +830,7 @@ class SearchIndexer {
|
|
|
865
830
|
tenantId,
|
|
866
831
|
queryEngine: this.noReindexQueryEngine
|
|
867
832
|
};
|
|
868
|
-
let text;
|
|
833
|
+
let text = config.buildSource ? [] : void 0;
|
|
869
834
|
let presenter;
|
|
870
835
|
let url;
|
|
871
836
|
let links;
|
|
@@ -874,7 +839,7 @@ class SearchIndexer {
|
|
|
874
839
|
try {
|
|
875
840
|
const source = await config.buildSource(buildContext);
|
|
876
841
|
if (source) {
|
|
877
|
-
text = source.text;
|
|
842
|
+
text = source.text ?? [];
|
|
878
843
|
if (source.presenter) presenter = source.presenter;
|
|
879
844
|
if (source.links) links = source.links;
|
|
880
845
|
if (source.checksumSource !== void 0) checksumSource = source.checksumSource;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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 indexing a batch of records by id in a single bulk write.\n */\nexport type IndexRecordsByIdParams = {\n items: Array<{ entityId: EntityId; recordId: string }>\n tenantId: string\n organizationId?: string | null\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 organizationId?: string | null\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 * Returns a wrapped QueryEngine that forces skipAutoReindex: true on every query.\n * Used to prevent feedback loops when search indexing callbacks (buildSource, formatResult)\n * call queryEngine.query() to hydrate related entities.\n */\n private get noReindexQueryEngine(): QueryEngine | undefined {\n if (!this.queryEngine) return undefined\n const wrapped: QueryEngine = {\n query: (entity, opts) => this.queryEngine!.query(entity, { ...opts, skipAutoReindex: true }),\n }\n return wrapped\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.noReindexQueryEngine,\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 skipAutoReindex: true,\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 * Index a batch of records by id in a single bulk write.\n * Unlike calling indexRecordById() in a loop, this loads each record fresh\n * (same as indexRecordById) but flushes the whole batch through a single\n * searchService.bulkIndex() call. Strategies that implement bulkIndex then\n * collapse the batch into one write; strategies without it still write per\n * record, at the bounded concurrency SearchService applies.\n */\n async indexRecordsById(params: IndexRecordsByIdParams): Promise<{ indexed: number; skipped: number }> {\n const { items, tenantId, organizationId } = params\n if (!this.queryEngine || items.length === 0) {\n return { indexed: 0, skipped: items.length }\n }\n\n const recordIdsByEntity = new Map<EntityId, string[]>()\n for (const item of items) {\n const list = recordIdsByEntity.get(item.entityId) ?? []\n list.push(item.recordId)\n recordIdsByEntity.set(item.entityId, list)\n }\n\n const allRecords: IndexableRecord[] = []\n let skipped = 0\n\n for (const [entityId, recordIds] of recordIdsByEntity) {\n const config = this.entityConfigMap.get(entityId)\n if (!config || config.enabled === false) {\n skipped += recordIds.length\n continue\n }\n\n for (const recordId of recordIds) {\n try {\n const result = await this.queryEngine.query(entityId, {\n tenantId,\n organizationId: organizationId ?? undefined,\n filters: { id: recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n skipAutoReindex: true,\n })\n\n const record = result.items[0] as Record<string, unknown> | undefined\n if (!record) {\n skipped++\n continue\n }\n\n const { records: built, dropped } = await this.buildIndexableRecords(\n entityId,\n tenantId,\n organizationId ?? null,\n [record],\n config,\n )\n skipped += dropped\n allRecords.push(...built)\n } catch (error) {\n skipped++\n searchError('SearchIndexer', 'Failed to load record for batch indexing', {\n entityId,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n }\n\n if (allRecords.length > 0) {\n await this.searchService.bulkIndex(allRecords)\n }\n\n return { indexed: allRecords.length, skipped }\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, params.organizationId)\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, params.organizationId)\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 skipAutoReindex: true,\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 // Bulk index records via SearchService (sends to all strategies)\n if (records.length > 0) {\n try {\n await this.searchService.bulkIndex(records)\n result.recordsIndexed += records.length\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to bulk index records', {\n entityId: params.entityId,\n recordCount: records.length,\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 try {\n await this.searchService.bulkIndex(indexableRecords)\n } catch (error) {\n throw new Error(\n `Failed to bulk index ${indexableRecords.length} records: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\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 skipAutoReindex: true,\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, params.organizationId)\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 skipAutoReindex: true,\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.noReindexQueryEngine,\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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",
|
|
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 indexing a batch of records by id in a single bulk write.\n */\nexport type IndexRecordsByIdParams = {\n items: Array<{ entityId: EntityId; recordId: string }>\n tenantId: string\n organizationId?: string | null\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 organizationId?: string | null\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 * Returns a wrapped QueryEngine that forces skipAutoReindex: true on every query.\n * Used to prevent feedback loops when search indexing callbacks (buildSource, formatResult)\n * call queryEngine.query() to hydrate related entities.\n */\n private get noReindexQueryEngine(): QueryEngine | undefined {\n if (!this.queryEngine) return undefined\n const wrapped: QueryEngine = {\n query: (entity, opts) => this.queryEngine!.query(entity, { ...opts, skipAutoReindex: true }),\n }\n return wrapped\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 indexableRecord = await this.buildIndexableRecord(params)\n if (indexableRecord) {\n await this.searchService.index(indexableRecord)\n }\n }\n\n private async buildIndexableRecord(params: IndexRecordParams): Promise<IndexableRecord | null> {\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return null\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.noReindexQueryEngine,\n }\n\n // Try buildSource first (provides text, presenter, links, checksumSource)\n let text: string | string[] | undefined = config.buildSource ? [] : 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 return {\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\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 skipAutoReindex: true,\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 * Index a batch of records by id in a single bulk write.\n * Unlike calling indexRecordById() in a loop, this loads each record fresh\n * (same as indexRecordById) but flushes the whole batch through a single\n * searchService.bulkIndex() call. Strategies that implement bulkIndex then\n * collapse the batch into one write; strategies without it still write per\n * record, at the bounded concurrency SearchService applies.\n */\n async indexRecordsById(params: IndexRecordsByIdParams): Promise<{ indexed: number; skipped: number }> {\n const { items, tenantId, organizationId } = params\n if (!this.queryEngine || items.length === 0) {\n return { indexed: 0, skipped: items.length }\n }\n\n const recordIdsByEntity = new Map<EntityId, string[]>()\n for (const item of items) {\n const list = recordIdsByEntity.get(item.entityId) ?? []\n list.push(item.recordId)\n recordIdsByEntity.set(item.entityId, list)\n }\n\n const allRecords: IndexableRecord[] = []\n let skipped = 0\n\n for (const [entityId, recordIds] of recordIdsByEntity) {\n const config = this.entityConfigMap.get(entityId)\n if (!config || config.enabled === false) {\n skipped += recordIds.length\n continue\n }\n\n for (const recordId of recordIds) {\n try {\n const result = await this.queryEngine.query(entityId, {\n tenantId,\n organizationId: organizationId ?? undefined,\n filters: { id: recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n skipAutoReindex: true,\n })\n\n const record = result.items[0] as Record<string, unknown> | undefined\n if (!record) {\n skipped++\n continue\n }\n\n const { records: built, dropped } = await this.buildIndexableRecords(\n entityId,\n tenantId,\n organizationId ?? null,\n [record],\n config,\n )\n skipped += dropped\n allRecords.push(...built)\n } catch (error) {\n skipped++\n searchError('SearchIndexer', 'Failed to load record for batch indexing', {\n entityId,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n }\n\n if (allRecords.length > 0) {\n await this.searchService.bulkIndex(allRecords)\n }\n\n return { indexed: allRecords.length, skipped }\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, params.organizationId)\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, params.organizationId)\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 skipAutoReindex: true,\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 // Bulk index records via SearchService (sends to all strategies)\n if (records.length > 0) {\n try {\n await this.searchService.bulkIndex(records)\n result.recordsIndexed += records.length\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to bulk index records', {\n entityId: params.entityId,\n recordCount: records.length,\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 indexableRecord = await this.buildIndexableRecord(param)\n if (indexableRecord) {\n indexableRecords.push(indexableRecord)\n }\n }\n\n if (indexableRecords.length > 0) {\n try {\n await this.searchService.bulkIndex(indexableRecords)\n } catch (error) {\n throw new Error(\n `Failed to bulk index ${indexableRecords.length} records: ${\n error instanceof Error ? error.message : error\n }`\n )\n }\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 skipAutoReindex: true,\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, params.organizationId)\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 skipAutoReindex: true,\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.noReindexQueryEngine,\n }\n\n // Try buildSource first (provides text, presenter, links, checksumSource)\n let text: string | string[] | undefined = config.buildSource ? [] : 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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@open-mercato/shared/lib/custom-fields/kinds";
|
|
6
6
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
7
7
|
import { extractFallbackPresenter } from "./fallback-presenter.js";
|
|
8
|
+
import { extractSearchableFields } from "./field-policy.js";
|
|
8
9
|
import { needsSearchResultEnrichment } from "./search-result-enrichment.js";
|
|
9
10
|
const BATCH_SIZE = 500;
|
|
10
11
|
const enricherLogger = createLogger("search").child({ component: "presenter-enricher" });
|
|
@@ -189,7 +190,7 @@ async function computePresenterAndLinks(doc, entityId, recordId, config, tenantI
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
if (!presenter) {
|
|
192
|
-
presenter = extractFallbackPresenter(doc, entityId, recordId);
|
|
193
|
+
presenter = extractFallbackPresenter(extractSearchableFields(doc, { fieldPolicy: config?.fieldPolicy }), entityId, recordId);
|
|
193
194
|
}
|
|
194
195
|
if (config?.resolveUrl) {
|
|
195
196
|
try {
|