@open-mercato/search 0.6.8-develop.7096.1.97319f09f6 → 0.6.8-develop.7100.1.fbf66fca35

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.
@@ -183,6 +183,72 @@ class SearchIndexer {
183
183
  throw error;
184
184
  }
185
185
  }
186
+ /**
187
+ * Index a batch of records by id in a single bulk write.
188
+ * Unlike calling indexRecordById() in a loop, this loads each record fresh
189
+ * (same as indexRecordById) but flushes the whole batch through a single
190
+ * searchService.bulkIndex() call. Strategies that implement bulkIndex then
191
+ * collapse the batch into one write; strategies without it still write per
192
+ * record, at the bounded concurrency SearchService applies.
193
+ */
194
+ async indexRecordsById(params) {
195
+ const { items, tenantId, organizationId } = params;
196
+ if (!this.queryEngine || items.length === 0) {
197
+ return { indexed: 0, skipped: items.length };
198
+ }
199
+ const recordIdsByEntity = /* @__PURE__ */ new Map();
200
+ for (const item of items) {
201
+ const list = recordIdsByEntity.get(item.entityId) ?? [];
202
+ list.push(item.recordId);
203
+ recordIdsByEntity.set(item.entityId, list);
204
+ }
205
+ const allRecords = [];
206
+ let skipped = 0;
207
+ for (const [entityId, recordIds] of recordIdsByEntity) {
208
+ const config = this.entityConfigMap.get(entityId);
209
+ if (!config || config.enabled === false) {
210
+ skipped += recordIds.length;
211
+ continue;
212
+ }
213
+ for (const recordId of recordIds) {
214
+ try {
215
+ const result = await this.queryEngine.query(entityId, {
216
+ tenantId,
217
+ organizationId: organizationId ?? void 0,
218
+ filters: { id: recordId },
219
+ includeCustomFields: true,
220
+ page: { page: 1, pageSize: 1 },
221
+ skipAutoReindex: true
222
+ });
223
+ const record = result.items[0];
224
+ if (!record) {
225
+ skipped++;
226
+ continue;
227
+ }
228
+ const { records: built, dropped } = await this.buildIndexableRecords(
229
+ entityId,
230
+ tenantId,
231
+ organizationId ?? null,
232
+ [record],
233
+ config
234
+ );
235
+ skipped += dropped;
236
+ allRecords.push(...built);
237
+ } catch (error) {
238
+ skipped++;
239
+ searchError("SearchIndexer", "Failed to load record for batch indexing", {
240
+ entityId,
241
+ recordId,
242
+ error: error instanceof Error ? error.message : error
243
+ });
244
+ }
245
+ }
246
+ }
247
+ if (allRecords.length > 0) {
248
+ await this.searchService.bulkIndex(allRecords);
249
+ }
250
+ return { indexed: allRecords.length, skipped };
251
+ }
186
252
  /**
187
253
  * Delete a record from the search index.
188
254
  */
@@ -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 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 * 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 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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",
6
6
  "names": []
7
7
  }
@@ -145,28 +145,11 @@ async function handleFulltextIndexJob(job, jobCtx, ctx) {
145
145
  if (!searchIndexer) {
146
146
  throw new Error("searchIndexer not available for batch indexing");
147
147
  }
148
- let successCount = 0;
149
- let failCount = 0;
150
- for (const { entityId, recordId } of records) {
151
- try {
152
- const result = await searchIndexer.indexRecordById({
153
- entityId,
154
- recordId,
155
- tenantId,
156
- organizationId
157
- });
158
- if (result.action === "indexed") {
159
- successCount++;
160
- }
161
- } catch (error) {
162
- failCount++;
163
- searchDebugWarn("fulltext-index.worker", "Failed to index record in batch", {
164
- entityId,
165
- recordId,
166
- error: error instanceof Error ? error.message : error
167
- });
168
- }
169
- }
148
+ const { indexed: successCount, skipped: skippedCount } = await searchIndexer.indexRecordsById({
149
+ items: records.map(({ entityId, recordId }) => ({ entityId, recordId })),
150
+ tenantId,
151
+ organizationId
152
+ });
170
153
  await advanceFulltextReindexProgress({
171
154
  db,
172
155
  em,
@@ -180,7 +163,7 @@ async function handleFulltextIndexJob(job, jobCtx, ctx) {
180
163
  tenantId,
181
164
  requestedCount: records.length,
182
165
  successCount,
183
- failCount
166
+ skippedCount
184
167
  });
185
168
  await recordIndexerLog(
186
169
  { em: em ?? void 0 },
@@ -189,7 +172,7 @@ async function handleFulltextIndexJob(job, jobCtx, ctx) {
189
172
  handler: "worker:fulltext:batch-index",
190
173
  message: `Indexed ${successCount}/${records.length} records to fulltext`,
191
174
  tenantId,
192
- details: { jobId: jobCtx.jobId, requestedCount: records.length, successCount, failCount }
175
+ details: { jobId: jobCtx.jobId, requestedCount: records.length, successCount, skippedCount }
193
176
  }
194
177
  );
195
178
  return;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/search/workers/fulltext-index.worker.ts"],
4
- "sourcesContent": ["import type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'\nimport type { Kysely } from 'kysely'\nimport { FULLTEXT_INDEXING_QUEUE_NAME, type FulltextIndexJobPayload } from '../../../queue/fulltext-indexing'\nimport type { FullTextSearchStrategy } from '../../../strategies/fulltext.strategy'\nimport type { SearchIndexer } from '../../../indexer/search-indexer'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\nimport { searchDebug, searchDebugWarn, searchError } from '../../../lib/debug'\nimport { clearReindexLock, updateReindexProgress } from '../lib/reindex-lock'\nimport { hasActiveReindexProgress, incrementReindexProgress } from '../lib/reindex-progress'\n\n// Worker metadata for auto-discovery\nconst DEFAULT_CONCURRENCY = 2\nconst envConcurrency = process.env.WORKERS_FULLTEXT_INDEXING_CONCURRENCY\n\nexport const metadata: WorkerMeta = {\n queue: FULLTEXT_INDEXING_QUEUE_NAME,\n concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY,\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\nasync function advanceFulltextReindexProgress(params: {\n db: Kysely<any> | null\n em: EntityManager | null\n progressService: ProgressService | null\n tenantId: string\n organizationId?: string | null\n delta: number\n}): Promise<void> {\n if (!Number.isFinite(params.delta) || params.delta <= 0) return\n\n if (params.progressService && params.em) {\n const hasActiveProgress = await hasActiveReindexProgress({\n em: params.em,\n type: 'fulltext',\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n })\n\n if (!hasActiveProgress) {\n if (params.db) {\n await clearReindexLock(params.db, params.tenantId, 'fulltext', params.organizationId ?? null)\n }\n return\n }\n\n if (params.db) {\n await updateReindexProgress(params.db, params.tenantId, 'fulltext', params.delta, params.organizationId ?? null)\n }\n\n const completed = await incrementReindexProgress({\n em: params.em,\n progressService: params.progressService,\n type: 'fulltext',\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n delta: params.delta,\n })\n if (completed && params.db) {\n await clearReindexLock(params.db, params.tenantId, 'fulltext', params.organizationId ?? null)\n }\n return\n }\n\n if (params.db) {\n await updateReindexProgress(params.db, params.tenantId, 'fulltext', params.delta, params.organizationId ?? null)\n }\n}\n\n/**\n * Process a fulltext indexing job.\n *\n * This handler processes single record indexing, batch indexing, deletion, and purge\n * operations for the fulltext search strategy.\n *\n * All indexing operations (single and batch) use searchIndexer.indexRecordById() to load\n * fresh data, ensuring consistency with the vector worker pattern.\n *\n * @param job - The queued job containing payload\n * @param jobCtx - Queue job context with job ID and attempt info\n * @param ctx - DI container context for resolving services\n */\nexport async function handleFulltextIndexJob(\n job: QueuedJob<FulltextIndexJobPayload>,\n jobCtx: JobContext,\n ctx: HandlerContext,\n): Promise<void> {\n const { jobType, tenantId } = job.payload\n\n if (!tenantId) {\n searchDebugWarn('fulltext-index.worker', 'Skipping job with missing tenantId', {\n jobId: jobCtx.jobId,\n jobType,\n })\n return\n }\n\n // Resolve EntityManager for logging and Kysely for database queries\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any | null = null\n let db: Kysely<any> | null = null\n try {\n em = ctx.resolve('em') as EntityManager\n db = (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n } catch {\n em = null\n db = null\n }\n\n // Resolve searchIndexer for loading fresh data\n let searchIndexer: SearchIndexer | undefined\n try {\n searchIndexer = ctx.resolve<SearchIndexer>('searchIndexer')\n } catch {\n searchDebugWarn('fulltext-index.worker', 'searchIndexer not available')\n }\n\n // Resolve fulltext strategy\n let fulltextStrategy: FullTextSearchStrategy | undefined\n try {\n const searchStrategies = ctx.resolve<unknown[]>('searchStrategies')\n fulltextStrategy = searchStrategies?.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext',\n ) as FullTextSearchStrategy | undefined\n } catch {\n searchDebugWarn('fulltext-index.worker', 'searchStrategies not available')\n return\n }\n\n if (!fulltextStrategy) {\n searchDebugWarn('fulltext-index.worker', 'Fulltext strategy not configured')\n return\n }\n\n // Check if fulltext is available\n const isAvailable = await fulltextStrategy.isAvailable()\n if (!isAvailable) {\n throw new Error('Fulltext search is not available') // Will trigger retry\n }\n\n try {\n let progressService: ProgressService | null = null\n try {\n progressService = ctx.resolve<ProgressService>('progressService')\n } catch {\n progressService = null\n }\n\n // ========== SINGLE INDEX: Use searchIndexer.indexRecordById() for fresh data ==========\n if (jobType === 'index') {\n const { entityType, recordId, organizationId } = job.payload as {\n entityType: string\n recordId: string\n organizationId?: string | null\n }\n\n if (!entityType || !recordId) {\n searchDebugWarn('fulltext-index.worker', 'Skipping index with missing fields', {\n jobId: jobCtx.jobId,\n entityType,\n recordId,\n })\n return\n }\n\n if (!searchIndexer) {\n throw new Error('searchIndexer not available for single-record index')\n }\n\n const result = await searchIndexer.indexRecordById({\n entityId: entityType as EntityId,\n recordId,\n tenantId,\n organizationId,\n })\n\n searchDebug('fulltext-index.worker', 'Indexed single record to fulltext', {\n jobId: jobCtx.jobId,\n tenantId,\n entityType,\n recordId,\n action: result.action,\n })\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: 'worker:fulltext:index',\n message: `Indexed record to fulltext (${result.action})`,\n entityType,\n recordId,\n tenantId,\n details: { jobId: jobCtx.jobId },\n },\n )\n return\n }\n\n // ========== BATCH-INDEX: Use searchIndexer.indexRecordById() for fresh data ==========\n if (jobType === 'batch-index') {\n const { records, organizationId } = job.payload\n if (!records || records.length === 0) {\n searchDebugWarn('fulltext-index.worker', 'Skipping batch-index with no records', {\n jobId: jobCtx.jobId,\n })\n return\n }\n\n if (!searchIndexer) {\n throw new Error('searchIndexer not available for batch indexing')\n }\n\n // Process each record using indexRecordById (same pattern as vector worker)\n let successCount = 0\n let failCount = 0\n\n for (const { entityId, recordId } of records) {\n try {\n const result = await searchIndexer.indexRecordById({\n entityId: entityId as EntityId,\n recordId,\n tenantId,\n organizationId,\n })\n if (result.action === 'indexed') {\n successCount++\n }\n } catch (error) {\n failCount++\n searchDebugWarn('fulltext-index.worker', 'Failed to index record in batch', {\n entityId,\n recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n await advanceFulltextReindexProgress({\n db,\n em,\n progressService,\n tenantId,\n organizationId: organizationId ?? null,\n delta: records.length,\n })\n\n searchDebug('fulltext-index.worker', 'Batch indexed to fulltext', {\n jobId: jobCtx.jobId,\n tenantId,\n requestedCount: records.length,\n successCount,\n failCount,\n })\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: 'worker:fulltext:batch-index',\n message: `Indexed ${successCount}/${records.length} records to fulltext`,\n tenantId,\n details: { jobId: jobCtx.jobId, requestedCount: records.length, successCount, failCount },\n },\n )\n return\n }\n\n // ========== DELETE ==========\n if (jobType === 'delete') {\n const { entityId, recordId } = job.payload\n if (!entityId || !recordId) {\n searchDebugWarn('fulltext-index.worker', 'Skipping delete with missing fields', {\n jobId: jobCtx.jobId,\n entityId,\n recordId,\n })\n return\n }\n\n await fulltextStrategy.delete(entityId, recordId, tenantId)\n\n searchDebug('fulltext-index.worker', 'Deleted from fulltext', {\n jobId: jobCtx.jobId,\n tenantId,\n entityId,\n recordId,\n })\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: 'worker:fulltext:delete',\n message: `Deleted record from fulltext`,\n entityType: entityId,\n recordId,\n tenantId,\n details: { jobId: jobCtx.jobId },\n },\n )\n return\n }\n\n // ========== PURGE ==========\n if (jobType === 'purge') {\n const { entityId } = job.payload\n if (!entityId) {\n searchDebugWarn('fulltext-index.worker', 'Skipping purge with missing entityId', {\n jobId: jobCtx.jobId,\n })\n return\n }\n\n await fulltextStrategy.purge(entityId, tenantId)\n\n searchDebug('fulltext-index.worker', 'Purged entity from fulltext', {\n jobId: jobCtx.jobId,\n tenantId,\n entityId,\n })\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: 'worker:fulltext:purge',\n message: `Purged entity from fulltext`,\n entityType: entityId,\n tenantId,\n details: { jobId: jobCtx.jobId },\n },\n )\n return\n }\n } catch (error) {\n searchError('fulltext-index.worker', `Failed to ${jobType}`, {\n jobId: jobCtx.jobId,\n tenantId,\n error: error instanceof Error ? error.message : error,\n attemptNumber: jobCtx.attemptNumber,\n })\n\n const entityId = 'entityId' in job.payload ? job.payload.entityId :\n 'entityType' in job.payload ? (job.payload as { entityType?: string }).entityType : undefined\n const recordId = 'recordId' in job.payload ? job.payload.recordId : undefined\n\n await recordIndexerError(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: `worker:fulltext:${jobType}`,\n error,\n entityType: entityId,\n recordId,\n tenantId,\n payload: job.payload,\n },\n )\n\n // Re-throw to let the queue handle retry logic\n throw error\n }\n}\n\n/**\n * Default export for worker auto-discovery.\n * Wraps handleFulltextIndexJob to match the expected handler signature.\n */\nexport default async function handle(\n job: QueuedJob<FulltextIndexJobPayload>,\n ctx: JobContext & HandlerContext\n): Promise<void> {\n return handleFulltextIndexJob(job, ctx, ctx)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'\nimport type { Kysely } from 'kysely'\nimport { FULLTEXT_INDEXING_QUEUE_NAME, type FulltextIndexJobPayload } from '../../../queue/fulltext-indexing'\nimport type { FullTextSearchStrategy } from '../../../strategies/fulltext.strategy'\nimport type { SearchIndexer } from '../../../indexer/search-indexer'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\nimport { searchDebug, searchDebugWarn, searchError } from '../../../lib/debug'\nimport { clearReindexLock, updateReindexProgress } from '../lib/reindex-lock'\nimport { hasActiveReindexProgress, incrementReindexProgress } from '../lib/reindex-progress'\n\n// Worker metadata for auto-discovery\nconst DEFAULT_CONCURRENCY = 2\nconst envConcurrency = process.env.WORKERS_FULLTEXT_INDEXING_CONCURRENCY\n\nexport const metadata: WorkerMeta = {\n queue: FULLTEXT_INDEXING_QUEUE_NAME,\n concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY,\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\nasync function advanceFulltextReindexProgress(params: {\n db: Kysely<any> | null\n em: EntityManager | null\n progressService: ProgressService | null\n tenantId: string\n organizationId?: string | null\n delta: number\n}): Promise<void> {\n if (!Number.isFinite(params.delta) || params.delta <= 0) return\n\n if (params.progressService && params.em) {\n const hasActiveProgress = await hasActiveReindexProgress({\n em: params.em,\n type: 'fulltext',\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n })\n\n if (!hasActiveProgress) {\n if (params.db) {\n await clearReindexLock(params.db, params.tenantId, 'fulltext', params.organizationId ?? null)\n }\n return\n }\n\n if (params.db) {\n await updateReindexProgress(params.db, params.tenantId, 'fulltext', params.delta, params.organizationId ?? null)\n }\n\n const completed = await incrementReindexProgress({\n em: params.em,\n progressService: params.progressService,\n type: 'fulltext',\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n delta: params.delta,\n })\n if (completed && params.db) {\n await clearReindexLock(params.db, params.tenantId, 'fulltext', params.organizationId ?? null)\n }\n return\n }\n\n if (params.db) {\n await updateReindexProgress(params.db, params.tenantId, 'fulltext', params.delta, params.organizationId ?? null)\n }\n}\n\n/**\n * Process a fulltext indexing job.\n *\n * This handler processes single record indexing, batch indexing, deletion, and purge\n * operations for the fulltext search strategy.\n *\n * Single-record jobs load fresh data via searchIndexer.indexRecordById(). Batch jobs load\n * fresh data per record via searchIndexer.indexRecordsById(), which flushes the whole batch\n * through a single bulk write instead of one write per record.\n *\n * @param job - The queued job containing payload\n * @param jobCtx - Queue job context with job ID and attempt info\n * @param ctx - DI container context for resolving services\n */\nexport async function handleFulltextIndexJob(\n job: QueuedJob<FulltextIndexJobPayload>,\n jobCtx: JobContext,\n ctx: HandlerContext,\n): Promise<void> {\n const { jobType, tenantId } = job.payload\n\n if (!tenantId) {\n searchDebugWarn('fulltext-index.worker', 'Skipping job with missing tenantId', {\n jobId: jobCtx.jobId,\n jobType,\n })\n return\n }\n\n // Resolve EntityManager for logging and Kysely for database queries\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any | null = null\n let db: Kysely<any> | null = null\n try {\n em = ctx.resolve('em') as EntityManager\n db = (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n } catch {\n em = null\n db = null\n }\n\n // Resolve searchIndexer for loading fresh data\n let searchIndexer: SearchIndexer | undefined\n try {\n searchIndexer = ctx.resolve<SearchIndexer>('searchIndexer')\n } catch {\n searchDebugWarn('fulltext-index.worker', 'searchIndexer not available')\n }\n\n // Resolve fulltext strategy\n let fulltextStrategy: FullTextSearchStrategy | undefined\n try {\n const searchStrategies = ctx.resolve<unknown[]>('searchStrategies')\n fulltextStrategy = searchStrategies?.find(\n (s: unknown) => (s as { id?: string })?.id === 'fulltext',\n ) as FullTextSearchStrategy | undefined\n } catch {\n searchDebugWarn('fulltext-index.worker', 'searchStrategies not available')\n return\n }\n\n if (!fulltextStrategy) {\n searchDebugWarn('fulltext-index.worker', 'Fulltext strategy not configured')\n return\n }\n\n // Check if fulltext is available\n const isAvailable = await fulltextStrategy.isAvailable()\n if (!isAvailable) {\n throw new Error('Fulltext search is not available') // Will trigger retry\n }\n\n try {\n let progressService: ProgressService | null = null\n try {\n progressService = ctx.resolve<ProgressService>('progressService')\n } catch {\n progressService = null\n }\n\n // ========== SINGLE INDEX: Use searchIndexer.indexRecordById() for fresh data ==========\n if (jobType === 'index') {\n const { entityType, recordId, organizationId } = job.payload as {\n entityType: string\n recordId: string\n organizationId?: string | null\n }\n\n if (!entityType || !recordId) {\n searchDebugWarn('fulltext-index.worker', 'Skipping index with missing fields', {\n jobId: jobCtx.jobId,\n entityType,\n recordId,\n })\n return\n }\n\n if (!searchIndexer) {\n throw new Error('searchIndexer not available for single-record index')\n }\n\n const result = await searchIndexer.indexRecordById({\n entityId: entityType as EntityId,\n recordId,\n tenantId,\n organizationId,\n })\n\n searchDebug('fulltext-index.worker', 'Indexed single record to fulltext', {\n jobId: jobCtx.jobId,\n tenantId,\n entityType,\n recordId,\n action: result.action,\n })\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: 'worker:fulltext:index',\n message: `Indexed record to fulltext (${result.action})`,\n entityType,\n recordId,\n tenantId,\n details: { jobId: jobCtx.jobId },\n },\n )\n return\n }\n\n // ========== BATCH-INDEX: Load fresh data, write the whole batch in one call ==========\n if (jobType === 'batch-index') {\n const { records, organizationId } = job.payload\n if (!records || records.length === 0) {\n searchDebugWarn('fulltext-index.worker', 'Skipping batch-index with no records', {\n jobId: jobCtx.jobId,\n })\n return\n }\n\n if (!searchIndexer) {\n throw new Error('searchIndexer not available for batch indexing')\n }\n\n // Load and index the whole batch through a single bulk write instead of\n // one indexRecordById() call per record.\n const { indexed: successCount, skipped: skippedCount } = await searchIndexer.indexRecordsById({\n items: records.map(({ entityId, recordId }) => ({ entityId: entityId as EntityId, recordId })),\n tenantId,\n organizationId,\n })\n\n await advanceFulltextReindexProgress({\n db,\n em,\n progressService,\n tenantId,\n organizationId: organizationId ?? null,\n delta: records.length,\n })\n\n searchDebug('fulltext-index.worker', 'Batch indexed to fulltext', {\n jobId: jobCtx.jobId,\n tenantId,\n requestedCount: records.length,\n successCount,\n skippedCount,\n })\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: 'worker:fulltext:batch-index',\n message: `Indexed ${successCount}/${records.length} records to fulltext`,\n tenantId,\n details: { jobId: jobCtx.jobId, requestedCount: records.length, successCount, skippedCount },\n },\n )\n return\n }\n\n // ========== DELETE ==========\n if (jobType === 'delete') {\n const { entityId, recordId } = job.payload\n if (!entityId || !recordId) {\n searchDebugWarn('fulltext-index.worker', 'Skipping delete with missing fields', {\n jobId: jobCtx.jobId,\n entityId,\n recordId,\n })\n return\n }\n\n await fulltextStrategy.delete(entityId, recordId, tenantId)\n\n searchDebug('fulltext-index.worker', 'Deleted from fulltext', {\n jobId: jobCtx.jobId,\n tenantId,\n entityId,\n recordId,\n })\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: 'worker:fulltext:delete',\n message: `Deleted record from fulltext`,\n entityType: entityId,\n recordId,\n tenantId,\n details: { jobId: jobCtx.jobId },\n },\n )\n return\n }\n\n // ========== PURGE ==========\n if (jobType === 'purge') {\n const { entityId } = job.payload\n if (!entityId) {\n searchDebugWarn('fulltext-index.worker', 'Skipping purge with missing entityId', {\n jobId: jobCtx.jobId,\n })\n return\n }\n\n await fulltextStrategy.purge(entityId, tenantId)\n\n searchDebug('fulltext-index.worker', 'Purged entity from fulltext', {\n jobId: jobCtx.jobId,\n tenantId,\n entityId,\n })\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: 'worker:fulltext:purge',\n message: `Purged entity from fulltext`,\n entityType: entityId,\n tenantId,\n details: { jobId: jobCtx.jobId },\n },\n )\n return\n }\n } catch (error) {\n searchError('fulltext-index.worker', `Failed to ${jobType}`, {\n jobId: jobCtx.jobId,\n tenantId,\n error: error instanceof Error ? error.message : error,\n attemptNumber: jobCtx.attemptNumber,\n })\n\n const entityId = 'entityId' in job.payload ? job.payload.entityId :\n 'entityType' in job.payload ? (job.payload as { entityType?: string }).entityType : undefined\n const recordId = 'recordId' in job.payload ? job.payload.recordId : undefined\n\n await recordIndexerError(\n { em: em ?? undefined },\n {\n source: 'fulltext',\n handler: `worker:fulltext:${jobType}`,\n error,\n entityType: entityId,\n recordId,\n tenantId,\n payload: job.payload,\n },\n )\n\n // Re-throw to let the queue handle retry logic\n throw error\n }\n}\n\n/**\n * Default export for worker auto-discovery.\n * Wraps handleFulltextIndexJob to match the expected handler signature.\n */\nexport default async function handle(\n job: QueuedJob<FulltextIndexJobPayload>,\n ctx: JobContext & HandlerContext\n): Promise<void> {\n return handleFulltextIndexJob(job, ctx, ctx)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/service.js CHANGED
@@ -4,6 +4,19 @@ const DEFAULT_MERGE_CONFIG = {
4
4
  duplicateHandling: "highest_score"
5
5
  };
6
6
  const STRATEGY_AVAILABILITY_CACHE_TTL_MS = 2e3;
7
+ const BULK_INDEX_FALLBACK_CONCURRENCY = 4;
8
+ async function mapWithConcurrency(items, limit, worker) {
9
+ if (items.length === 0) return;
10
+ let nextIndex = 0;
11
+ const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
12
+ for (; ; ) {
13
+ const currentIndex = nextIndex++;
14
+ if (currentIndex >= items.length) return;
15
+ await worker(items[currentIndex]);
16
+ }
17
+ });
18
+ await Promise.all(runners);
19
+ }
7
20
  function normalizeOrganizationFilter(options) {
8
21
  const single = typeof options.organizationId === "string" ? options.organizationId.trim() : "";
9
22
  if (single) return [single];
@@ -145,7 +158,11 @@ class SearchService {
145
158
  if (strategy.bulkIndex) {
146
159
  return strategy.bulkIndex(records);
147
160
  }
148
- return Promise.all(records.map((record) => this.executeStrategyIndex(strategy, record)));
161
+ return mapWithConcurrency(
162
+ records,
163
+ BULK_INDEX_FALLBACK_CONCURRENCY,
164
+ (record) => this.executeStrategyIndex(strategy, record)
165
+ );
149
166
  })
150
167
  );
151
168
  const failures = [];