@open-mercato/search 0.4.11-develop.2207.23c94908f0 → 0.4.11-develop.2212.7e6680a1e8

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.
@@ -361,7 +361,13 @@ class SearchIndexer {
361
361
  });
362
362
  }
363
363
  if (indexableRecords.length > 0) {
364
- await this.searchService.bulkIndex(indexableRecords);
364
+ try {
365
+ await this.searchService.bulkIndex(indexableRecords);
366
+ } catch (error) {
367
+ throw new Error(
368
+ `Failed to bulk index ${indexableRecords.length} records: ${error instanceof Error ? error.message : error}`
369
+ );
370
+ }
365
371
  }
366
372
  }
367
373
  /**
@@ -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}\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)\n }\n\n /**\n * Reindex an entity via all configured strategies (including vector).\n * This is the general reindex method that works with all search strategies.\n */\n async reindexEntity(params: {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n if (!this.queryEngine) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Query engine not available' }],\n }\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Entity not configured for search' }],\n }\n }\n\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 1,\n recordsIndexed: 0,\n errors: [],\n }\n\n // Optionally purge first\n if (params.purgeFirst) {\n try {\n await this.searchService.purge(params.entityId, params.tenantId)\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to purge entity before reindex', {\n entityId: params.entityId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Paginate through all records\n let page = 1\n const pageSize = 200\n let hasMore = true\n\n while (hasMore && page <= MAX_PAGES) {\n try {\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n includeCustomFields: true,\n page: { page, pageSize },\n 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 // Index each record via SearchService (sends to all strategies)\n for (const record of records) {\n try {\n await this.searchService.index(record)\n result.recordsIndexed++\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to index record', {\n entityId: params.entityId,\n recordId: record.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n page++\n hasMore = items.length === pageSize\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n break\n }\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities via all configured strategies.\n */\n async reindexAll(params: {\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [],\n }\n\n const enabledEntities = this.listEnabledEntities()\n\n for (const entityId of enabledEntities) {\n const entityResult = await this.reindexEntity({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n purgeFirst: params.purgeFirst,\n })\n\n result.entitiesProcessed++\n result.recordsIndexed += entityResult.recordsIndexed\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Bulk index multiple records.\n */\n async bulkIndexRecords(params: IndexRecordParams[]): Promise<void> {\n const indexableRecords: IndexableRecord[] = []\n\n for (const param of params) {\n const config = this.entityConfigMap.get(param.entityId)\n if (!config || config.enabled === false) {\n continue\n }\n\n const buildContext: SearchBuildContext = {\n record: param.record,\n customFields: param.customFields ?? {},\n organizationId: param.organizationId,\n tenantId: param.tenantId,\n }\n\n let presenter: SearchResultPresenter | undefined\n if (config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch {\n // Skip presenter on error\n }\n }\n\n let url: string | undefined\n if (config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch {\n // Skip URL on error\n }\n }\n\n let links: SearchResultLink[] | undefined\n if (config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch {\n // Skip links on error\n }\n }\n\n indexableRecords.push({\n entityId: param.entityId,\n recordId: param.recordId,\n tenantId: param.tenantId,\n organizationId: param.organizationId,\n fields: param.record,\n presenter,\n url,\n links,\n })\n }\n\n if (indexableRecords.length > 0) {\n await this.searchService.bulkIndex(indexableRecords)\n }\n }\n\n /**\n * List all enabled entity IDs from the module configurations.\n */\n listEnabledEntities(): EntityId[] {\n return Array.from(this.entityConfigMap.keys())\n }\n\n /**\n * Get the fulltext strategy from the search service.\n */\n private getFulltextStrategy(): FullTextSearchStrategy | undefined {\n const strategy = this.searchService.getStrategy('fulltext')\n if (!strategy) return undefined\n return strategy as FullTextSearchStrategy\n }\n\n /**\n * Reindex a single entity type to fulltext search.\n * This fetches all records from the database and re-indexes them to fulltext only.\n *\n * When `useQueue` is true, batches are enqueued for background processing by workers.\n * When `useQueue` is false (default), batches are indexed directly (blocking).\n */\n async reindexEntityToFulltext(params: ReindexEntityParams): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n recordsDropped: 0,\n jobsEnqueued: 0,\n errors: [],\n }\n\n const fulltext = this.getFulltextStrategy()\n if (!fulltext) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Fulltext strategy not available' })\n return result\n }\n\n // If useQueue is requested but no queue is available, return error\n if (params.useQueue && !this.fulltextQueue) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Fulltext queue not configured for queue-based reindexing' })\n return result\n }\n\n if (!this.queryEngine) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'QueryEngine not available for reindexing' })\n return result\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config) {\n result.success = false\n result.errors.push({ entityId: params.entityId, error: 'Entity not configured for search' })\n return result\n }\n\n try {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'starting',\n processed: 0,\n })\n\n // Recreate index if requested (default: true)\n if (params.recreateIndex !== false) {\n await fulltext.recreateIndex(params.tenantId)\n }\n\n // Fetch and index records with pagination\n const pageSize = 200\n let page = 1\n let totalProcessed = 0\n let jobsEnqueued = 0\n\n for (;;) {\n params.onProgress?.({\n entityId: params.entityId,\n phase: 'fetching',\n processed: totalProcessed,\n })\n\n try {\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n page: { page, pageSize },\n 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)\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 deleting a record from the search index.\n */\nexport type DeleteRecordParams = {\n entityId: EntityId\n recordId: string\n tenantId: string\n}\n\n/**\n * Parameters for purging all records of an entity type.\n */\nexport type PurgeEntityParams = {\n entityId: EntityId\n tenantId: string\n}\n\n/**\n * Parameters for reindexing an entity to fulltext search.\n */\nexport type ReindexEntityParams = {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n /** Whether to recreate the index first (default: true) */\n recreateIndex?: boolean\n /** Callback for progress tracking */\n onProgress?: (progress: ReindexProgress) => void\n /** Whether to use queue for batch processing (default: false) */\n useQueue?: boolean\n}\n\n/**\n * Parameters for reindexing all entities to fulltext search.\n */\nexport type ReindexAllParams = {\n tenantId: string\n organizationId?: string | null\n /** Whether to recreate the index first (default: true) */\n recreateIndex?: boolean\n /** Callback for progress tracking */\n onProgress?: (progress: ReindexProgress) => void\n /** Whether to use queue for batch processing (default: false) */\n useQueue?: boolean\n}\n\n/**\n * Progress information during reindex.\n */\nexport type ReindexProgress = {\n entityId: EntityId\n phase: 'starting' | 'fetching' | 'indexing' | 'complete'\n processed: number\n total?: number\n}\n\n/**\n * Result of a reindex operation.\n */\nexport type ReindexResult = {\n success: boolean\n entitiesProcessed: number\n recordsIndexed: number\n /** Number of records dropped due to missing id or other validation failures */\n recordsDropped?: number\n /** Number of jobs enqueued (when useQueue is true) */\n jobsEnqueued?: number\n errors: Array<{ entityId: EntityId; error: string }>\n}\n\n/**\n * Optional dependencies for SearchIndexer.\n */\nexport type SearchIndexerOptions = {\n queryEngine?: QueryEngine\n /** Queue for fulltext batch indexing */\n fulltextQueue?: Queue<FulltextIndexJobPayload>\n /** Queue for vector batch indexing */\n vectorQueue?: Queue<VectorIndexJobPayload>\n}\n\n/**\n * SearchIndexer orchestrates indexing operations by resolving entity configs\n * and building IndexableRecords for the SearchService.\n */\nexport class SearchIndexer {\n private readonly entityConfigMap: Map<EntityId, SearchEntityConfig>\n private readonly queryEngine?: QueryEngine\n private readonly fulltextQueue?: Queue<FulltextIndexJobPayload>\n private readonly vectorQueue?: Queue<VectorIndexJobPayload>\n\n constructor(\n private readonly searchService: SearchService,\n private readonly moduleConfigs: SearchModuleConfig[],\n options?: SearchIndexerOptions,\n ) {\n this.entityConfigMap = new Map()\n this.queryEngine = options?.queryEngine\n this.fulltextQueue = options?.fulltextQueue\n this.vectorQueue = options?.vectorQueue\n for (const moduleConfig of moduleConfigs) {\n for (const entityConfig of moduleConfig.entities) {\n if (entityConfig.enabled !== false) {\n this.entityConfigMap.set(entityConfig.entityId as EntityId, entityConfig)\n }\n }\n }\n }\n\n /**\n * 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)\n }\n\n /**\n * Reindex an entity via all configured strategies (including vector).\n * This is the general reindex method that works with all search strategies.\n */\n async reindexEntity(params: {\n entityId: EntityId\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n if (!this.queryEngine) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Query engine not available' }],\n }\n }\n\n const config = this.entityConfigMap.get(params.entityId)\n if (!config || config.enabled === false) {\n return {\n success: false,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [{ entityId: params.entityId, error: 'Entity not configured for search' }],\n }\n }\n\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 1,\n recordsIndexed: 0,\n errors: [],\n }\n\n // Optionally purge first\n if (params.purgeFirst) {\n try {\n await this.searchService.purge(params.entityId, params.tenantId)\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to purge entity before reindex', {\n entityId: params.entityId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n // Paginate through all records\n let page = 1\n const pageSize = 200\n let hasMore = true\n\n while (hasMore && page <= MAX_PAGES) {\n try {\n const queryResult = await this.queryEngine.query(params.entityId, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? undefined,\n includeCustomFields: true,\n page: { page, pageSize },\n 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 // Index each record via SearchService (sends to all strategies)\n for (const record of records) {\n try {\n await this.searchService.index(record)\n result.recordsIndexed++\n } catch (error) {\n searchDebugWarn('SearchIndexer', 'Failed to index record', {\n entityId: params.entityId,\n recordId: record.recordId,\n error: error instanceof Error ? error.message : error,\n })\n }\n }\n\n page++\n hasMore = items.length === pageSize\n } catch (error) {\n result.success = false\n result.errors.push({\n entityId: params.entityId,\n error: error instanceof Error ? error.message : String(error),\n })\n break\n }\n }\n\n return result\n }\n\n /**\n * Reindex all enabled entities via all configured strategies.\n */\n async reindexAll(params: {\n tenantId: string\n organizationId?: string | null\n purgeFirst?: boolean\n }): Promise<ReindexResult> {\n const result: ReindexResult = {\n success: true,\n entitiesProcessed: 0,\n recordsIndexed: 0,\n errors: [],\n }\n\n const enabledEntities = this.listEnabledEntities()\n\n for (const entityId of enabledEntities) {\n const entityResult = await this.reindexEntity({\n entityId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n purgeFirst: params.purgeFirst,\n })\n\n result.entitiesProcessed++\n result.recordsIndexed += entityResult.recordsIndexed\n result.errors.push(...entityResult.errors)\n\n if (!entityResult.success) {\n result.success = false\n }\n }\n\n return result\n }\n\n /**\n * Bulk index multiple records.\n */\n async bulkIndexRecords(params: IndexRecordParams[]): Promise<void> {\n const indexableRecords: IndexableRecord[] = []\n\n for (const param of params) {\n const config = this.entityConfigMap.get(param.entityId)\n if (!config || config.enabled === false) {\n continue\n }\n\n const buildContext: SearchBuildContext = {\n record: param.record,\n customFields: param.customFields ?? {},\n organizationId: param.organizationId,\n tenantId: param.tenantId,\n }\n\n let presenter: SearchResultPresenter | undefined\n if (config.formatResult) {\n try {\n const result = await config.formatResult(buildContext)\n if (result) presenter = result\n } catch {\n // Skip presenter on error\n }\n }\n\n let url: string | undefined\n if (config.resolveUrl) {\n try {\n const result = await config.resolveUrl(buildContext)\n if (result) url = result\n } catch {\n // Skip URL on error\n }\n }\n\n let links: SearchResultLink[] | undefined\n if (config.resolveLinks) {\n try {\n const result = await config.resolveLinks(buildContext)\n if (result) links = result\n } catch {\n // Skip links on error\n }\n }\n\n indexableRecords.push({\n entityId: param.entityId,\n recordId: param.recordId,\n tenantId: param.tenantId,\n organizationId: param.organizationId,\n fields: param.record,\n presenter,\n url,\n links,\n })\n }\n\n if (indexableRecords.length > 0) {\n 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)\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": "AAeA,SAAS,aAAa,iBAAiB,mBAAmB;AAM1D,MAAM,YAAY;AAmGX,MAAM,cAAc;AAAA,EAMzB,YACmB,eACA,eACjB,SACA;AAHiB;AACA;AAGjB,SAAK,kBAAkB,oBAAI,IAAI;AAC/B,SAAK,cAAc,SAAS;AAC5B,SAAK,gBAAgB,SAAS;AAC9B,SAAK,cAAc,SAAS;AAC5B,eAAW,gBAAgB,eAAe;AACxC,iBAAW,gBAAgB,aAAa,UAAU;AAChD,YAAI,aAAa,YAAY,OAAO;AAClC,eAAK,gBAAgB,IAAI,aAAa,UAAsB,YAAY;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAY,uBAAgD;AAC1D,QAAI,CAAC,KAAK,YAAa,QAAO;AAC9B,UAAM,UAAuB;AAAA,MAC3B,OAAO,CAAC,QAAQ,SAAS,KAAK,YAAa,MAAM,QAAQ,EAAE,GAAG,MAAM,iBAAiB,KAAK,CAAC;AAAA,IAC7F;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,UAAoD;AAClE,WAAO,KAAK,gBAAgB,IAAI,QAAQ;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,sBAA4C;AAC1C,WAAO,MAAM,KAAK,KAAK,gBAAgB,OAAO,CAAC;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,UAA6B;AAC3C,UAAM,SAAS,KAAK,gBAAgB,IAAI,QAAQ;AAChD,WAAO,QAAQ,YAAY;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,QAA0C;AAC1D,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,UAAU,OAAO,YAAY,OAAO;AACvC;AAAA,IACF;AAEA,UAAM,eAAmC;AAAA,MACvC,QAAQ,OAAO;AAAA,MACf,cAAc,OAAO,gBAAgB,CAAC;AAAA,MACtC,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,aAAa,KAAK;AAAA,IACpB;AAGA,QAAI;AACJ,QAAI;AACJ,QAAI;AACJ,QAAI;AACJ,QAAI;AAEJ,QAAI,OAAO,aAAa;AACtB,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,YAAY,YAAY;AACpD,YAAI,QAAQ;AACV,iBAAO,OAAO;AACd,cAAI,OAAO,UAAW,aAAY,OAAO;AACzC,cAAI,OAAO,MAAO,SAAQ,OAAO;AACjC,cAAI,OAAO,mBAAmB,OAAW,kBAAiB,OAAO;AAAA,QACnE;AAAA,MACF,SAAS,OAAO;AACd,wBAAgB,iBAAiB,sBAAsB;AAAA,UACrD,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,CAAC,aAAa,OAAO,cAAc;AACrC,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,YAAI,OAAQ,aAAY;AAAA,MAC1B,SAAS,OAAO;AACd,wBAAgB,iBAAiB,uBAAuB;AAAA,UACtD,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,CAAC,OAAO,OAAO,YAAY;AAC7B,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,WAAW,YAAY;AACnD,YAAI,OAAQ,OAAM;AAAA,MACpB,SAAS,OAAO;AACd,wBAAgB,iBAAiB,qBAAqB;AAAA,UACpD,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,CAAC,SAAS,OAAO,cAAc;AACjC,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,YAAI,OAAQ,SAAQ;AAAA,MACtB,SAAS,OAAO;AACd,wBAAgB,iBAAiB,uBAAuB;AAAA,UACtD,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,UAAM,kBAAmC;AAAA,MACvC,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,QAAQ,OAAO;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,KAAK,cAAc,MAAM,eAAe;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,gBAAgB,QAK0C;AAC9D,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO,EAAE,QAAQ,WAAW,QAAQ,4BAA4B;AAAA,IAClE;AAEA,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,UAAU,OAAO,YAAY,OAAO;AACvC,aAAO,EAAE,QAAQ,WAAW,QAAQ,wBAAwB;AAAA,IAC9D;AAGA,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,YAAY,MAAM,OAAO,UAAU;AAAA,QAC3D,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO,kBAAkB;AAAA,QACzC,SAAS,EAAE,IAAI,OAAO,SAAS;AAAA,QAC/B,qBAAqB;AAAA,QACrB,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE;AAAA,QAC7B,iBAAiB;AAAA,MACnB,CAAC;AAED,YAAM,SAAS,OAAO,MAAM,CAAC;AAC7B,UAAI,CAAC,QAAQ;AACX,eAAO,EAAE,QAAQ,WAAW,QAAQ,mBAAmB;AAAA,MACzD;AAGA,YAAM,eAAwC,CAAC;AAC/C,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,YAAI,IAAI,WAAW,KAAK,KAAK,IAAI,WAAW,KAAK,GAAG;AAClD,uBAAa,IAAI,MAAM,CAAC,CAAC,IAAI;AAAA,QAC/B;AAAA,MACF;AAEA,YAAM,KAAK,YAAY;AAAA,QACrB,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB;AAAA,QACA;AAAA,MACF,CAAC;AAED,aAAO,EAAE,QAAQ,UAAU;AAAA,IAC7B,SAAS,OAAO;AACd,kBAAY,iBAAiB,sCAAsC;AAAA,QACjE,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAClD,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,QAA2C;AAC5D,UAAM,KAAK,cAAc,OAAO,OAAO,UAAU,OAAO,UAAU,OAAO,QAAQ;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,QAA0C;AAC1D,UAAM,KAAK,cAAc,MAAM,OAAO,UAAU,OAAO,QAAQ;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,QAKO;AACzB,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,mBAAmB;AAAA,QACnB,gBAAgB;AAAA,QAChB,QAAQ,CAAC,EAAE,UAAU,OAAO,UAAU,OAAO,6BAA6B,CAAC;AAAA,MAC7E;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,UAAU,OAAO,YAAY,OAAO;AACvC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,mBAAmB;AAAA,QACnB,gBAAgB;AAAA,QAChB,QAAQ,CAAC,EAAE,UAAU,OAAO,UAAU,OAAO,mCAAmC,CAAC;AAAA,MACnF;AAAA,IACF;AAEA,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,QAAQ,CAAC;AAAA,IACX;AAGA,QAAI,OAAO,YAAY;AACrB,UAAI;AACF,cAAM,KAAK,cAAc,MAAM,OAAO,UAAU,OAAO,QAAQ;AAAA,MACjE,SAAS,OAAO;AACd,wBAAgB,iBAAiB,yCAAyC;AAAA,UACxE,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAClD,CAAC;AAAA,MACH;AAAA,IACF;AAGA,QAAI,OAAO;AACX,UAAM,WAAW;AACjB,QAAI,UAAU;AAEd,WAAO,WAAW,QAAQ,WAAW;AACnC,UAAI;AACF,cAAM,cAAc,MAAM,KAAK,YAAY,MAAM,OAAO,UAAU;AAAA,UAChE,UAAU,OAAO;AAAA,UACjB,gBAAgB,OAAO,kBAAkB;AAAA,UACzC,qBAAqB;AAAA,UACrB,MAAM,EAAE,MAAM,SAAS;AAAA,UACvB,iBAAiB;AAAA,QACnB,CAAC;AAED,cAAM,QAAQ,YAAY;AAC1B,YAAI,MAAM,WAAW,GAAG;AACtB,oBAAU;AACV;AAAA,QACF;AAGA,cAAM,EAAE,QAAQ,IAAI,MAAM,KAAK;AAAA,UAC7B,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO,kBAAkB;AAAA,UACzB;AAAA,UACA;AAAA,QACF;AAGA,mBAAW,UAAU,SAAS;AAC5B,cAAI;AACF,kBAAM,KAAK,cAAc,MAAM,MAAM;AACrC,mBAAO;AAAA,UACT,SAAS,OAAO;AACd,4BAAgB,iBAAiB,0BAA0B;AAAA,cACzD,UAAU,OAAO;AAAA,cACjB,UAAU,OAAO;AAAA,cACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,YAClD,CAAC;AAAA,UACH;AAAA,QACF;AAEA;AACA,kBAAU,MAAM,WAAW;AAAA,MAC7B,SAAS,OAAO;AACd,eAAO,UAAU;AACjB,eAAO,OAAO,KAAK;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC9D,CAAC;AACD;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,QAIU;AACzB,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,QAAQ,CAAC;AAAA,IACX;AAEA,UAAM,kBAAkB,KAAK,oBAAoB;AAEjD,eAAW,YAAY,iBAAiB;AACtC,YAAM,eAAe,MAAM,KAAK,cAAc;AAAA,QAC5C;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,YAAY,OAAO;AAAA,MACrB,CAAC;AAED,aAAO;AACP,aAAO,kBAAkB,aAAa;AACtC,aAAO,OAAO,KAAK,GAAG,aAAa,MAAM;AAEzC,UAAI,CAAC,aAAa,SAAS;AACzB,eAAO,UAAU;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAiB,QAA4C;AACjE,UAAM,mBAAsC,CAAC;AAE7C,eAAW,SAAS,QAAQ;AAC1B,YAAM,SAAS,KAAK,gBAAgB,IAAI,MAAM,QAAQ;AACtD,UAAI,CAAC,UAAU,OAAO,YAAY,OAAO;AACvC;AAAA,MACF;AAEA,YAAM,eAAmC;AAAA,QACvC,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM,gBAAgB,CAAC;AAAA,QACrC,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB;AAEA,UAAI;AACJ,UAAI,OAAO,cAAc;AACvB,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,cAAI,OAAQ,aAAY;AAAA,QAC1B,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,UAAI;AACJ,UAAI,OAAO,YAAY;AACrB,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,WAAW,YAAY;AACnD,cAAI,OAAQ,OAAM;AAAA,QACpB,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,UAAI;AACJ,UAAI,OAAO,cAAc;AACvB,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,cAAI,OAAQ,SAAQ;AAAA,QACtB,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,uBAAiB,KAAK;AAAA,QACpB,UAAU,MAAM;AAAA,QAChB,UAAU,MAAM;AAAA,QAChB,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,QAAI,iBAAiB,SAAS,GAAG;AAC/B,UAAI;AACF,cAAM,KAAK,cAAc,UAAU,gBAAgB;AAAA,MACrD,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,wBAAwB,iBAAiB,MAAM,aAC7C,iBAAiB,QAAQ,MAAM,UAAU,KAC3C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,sBAAkC;AAChC,WAAO,MAAM,KAAK,KAAK,gBAAgB,KAAK,CAAC;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA,EAKQ,sBAA0D;AAChE,UAAM,WAAW,KAAK,cAAc,YAAY,UAAU;AAC1D,QAAI,CAAC,SAAU,QAAO;AACtB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,wBAAwB,QAAqD;AACjF,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,cAAc;AAAA,MACd,QAAQ,CAAC;AAAA,IACX;AAEA,UAAM,WAAW,KAAK,oBAAoB;AAC1C,QAAI,CAAC,UAAU;AACb,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,kCAAkC,CAAC;AAC1F,aAAO;AAAA,IACT;AAGA,QAAI,OAAO,YAAY,CAAC,KAAK,eAAe;AAC1C,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,2DAA2D,CAAC;AACnH,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,2CAA2C,CAAC;AACnG,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,QAAQ;AACX,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,mCAAmC,CAAC;AAC3F,aAAO;AAAA,IACT;AAEA,QAAI;AACF,aAAO,aAAa;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB,OAAO;AAAA,QACP,WAAW;AAAA,MACb,CAAC;AAGD,UAAI,OAAO,kBAAkB,OAAO;AAClC,cAAM,SAAS,cAAc,OAAO,QAAQ;AAAA,MAC9C;AAGA,YAAM,WAAW;AACjB,UAAI,OAAO;AACX,UAAI,iBAAiB;AACrB,UAAI,eAAe;AAEnB,iBAAS;AACP,eAAO,aAAa;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB,OAAO;AAAA,UACP,WAAW;AAAA,QACb,CAAC;AAED,YAAI;AACF,gBAAM,cAAc,MAAM,KAAK,YAAY,MAAM,OAAO,UAAU;AAAA,YAChE,UAAU,OAAO;AAAA,YACjB,gBAAgB,OAAO,kBAAkB;AAAA,YACzC,MAAM,EAAE,MAAM,SAAS;AAAA,YACvB,iBAAiB;AAAA,UACnB,CAAC;AAED,cAAI,CAAC,YAAY,MAAM,QAAQ;AAC7B;AAAA,UACF;AAEF,iBAAO,aAAa;AAAA,YAClB,UAAU,OAAO;AAAA,YACjB,OAAO;AAAA,YACP,WAAW;AAAA,YACX,OAAO,YAAY;AAAA,UACrB,CAAC;AAGC,gBAAM,EAAE,SAAS,kBAAkB,QAAQ,IAAI,MAAM,KAAK;AAAA,YACxD,OAAO;AAAA,YACP,OAAO;AAAA,YACP,OAAO,kBAAkB;AAAA,YACzB,YAAY;AAAA,YACZ;AAAA,UACF;AACA,iBAAO,kBAAkB,OAAO,kBAAkB,KAAK;AAGvD,cAAI,iBAAiB,SAAS,GAAG;AAC/B,gBAAI,OAAO,YAAY,KAAK,eAAe;AAGzC,oBAAM,KAAK,cAAc,QAAQ;AAAA,gBAC/B,SAAS;AAAA,gBACT,UAAU,OAAO;AAAA,gBACjB,gBAAgB,OAAO;AAAA,gBACvB,SAAS,iBAAiB,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,UAAU,EAAE,SAAS,EAAE;AAAA,cACvF,CAAC;AACD,8BAAgB;AAChB,gCAAkB,iBAAiB;AAAA,YACrC,OAAO;AAEL,kBAAI;AACF,sBAAM,SAAS,UAAU,gBAAgB;AACzC,kCAAkB,iBAAiB;AAAA,cACrC,SAAS,YAAY;AAEnB,sBAAM,WAAW,sBAAsB,QAAQ,WAAW,UAAU,OAAO,UAAU;AACrF,uBAAO,OAAO,KAAK;AAAA,kBACjB,UAAU,OAAO;AAAA,kBACjB,OAAO,SAAS,IAAI,YAAY,QAAQ;AAAA,gBAC1C,CAAC;AAAA,cACH;AAAA,YACF;AAAA,UACF;AAEA,cAAI,YAAY,MAAM,SAAS,UAAU;AACvC;AAAA,UACF;AACA,kBAAQ;AAGR,cAAI,OAAO,WAAW;AACpB;AAAA,UACF;AAAA,QACF,SAAS,YAAY;AACnB,gBAAM,WAAW,sBAAsB,QAAQ,WAAW,UAAU,OAAO,UAAU;AACrF,iBAAO,OAAO,KAAK;AAAA,YACjB,UAAU,OAAO;AAAA,YACjB,OAAO,iBAAiB,QAAQ;AAAA,UAClC,CAAC;AACD;AAAA,QACF;AAAA,MACF;AAEA,aAAO,oBAAoB;AAC3B,aAAO,iBAAiB;AACxB,aAAO,eAAe;AAEtB,aAAO,aAAa;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB,OAAO;AAAA,QACP,WAAW;AAAA,QACX,OAAO;AAAA,MACT,CAAC;AAAA,IACH,SAAS,OAAO;AACd,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC9D,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,qBAAqB,QAAkD;AAC3E,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,cAAc;AAAA,MACd,QAAQ,CAAC;AAAA,IACX;AAEA,UAAM,WAAW,KAAK,oBAAoB;AAC1C,QAAI,CAAC,UAAU;AACb,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAmB,OAAO,kCAAkC,CAAC;AAC5F,aAAO;AAAA,IACT;AAGA,QAAI,OAAO,kBAAkB,OAAO;AAClC,YAAM,SAAS,cAAc,OAAO,QAAQ;AAAA,IAC9C;AAEA,UAAM,WAAW,KAAK,oBAAoB;AAE1C,eAAW,YAAY,UAAU;AAC/B,YAAM,eAAe,MAAM,KAAK,wBAAwB;AAAA,QACtD;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,eAAe;AAAA;AAAA,QACf,YAAY,OAAO;AAAA,QACnB,UAAU,OAAO;AAAA,MACnB,CAAC;AAED,aAAO,qBAAqB,aAAa;AACzC,aAAO,kBAAkB,aAAa;AACtC,aAAO,kBAAkB,OAAO,kBAAkB,MAAM,aAAa,kBAAkB;AACvF,aAAO,gBAAgB,OAAO,gBAAgB,MAAM,aAAa,gBAAgB;AACjF,aAAO,OAAO,KAAK,GAAG,aAAa,MAAM;AAEzC,UAAI,CAAC,aAAa,SAAS;AACzB,eAAO,UAAU;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,sBAAsB,QAAgF;AAC1G,gBAAY,iBAAiB,gCAAgC;AAAA,MAC3D,UAAU,OAAO;AAAA,MACjB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,YAAY,OAAO;AAAA,IACrB,CAAC;AAED,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,cAAc;AAAA,MACd,QAAQ,CAAC;AAAA,IACX;AAGA,QAAI,OAAO,aAAa,SAAS,CAAC,KAAK,aAAa;AAClD,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,yDAAyD,CAAC;AACjH,aAAO;AAAA,IACT;AAEA,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,2CAA2C,CAAC;AACnG,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,KAAK,gBAAgB,IAAI,OAAO,QAAQ;AACvD,QAAI,CAAC,QAAQ;AACX,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,mCAAmC,CAAC;AAC3F,aAAO;AAAA,IACT;AAEA,QAAI;AACF,aAAO,aAAa;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB,OAAO;AAAA,QACP,WAAW;AAAA,MACb,CAAC;AAGD,UAAI,OAAO,YAAY;AACrB,YAAI;AACF,gBAAM,KAAK,cAAc,MAAM,OAAO,UAAU,OAAO,QAAQ;AAAA,QACjE,SAAS,OAAO;AACd,0BAAgB,iBAAiB,gDAAgD;AAAA,YAC/E,UAAU,OAAO;AAAA,YACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,UAClD,CAAC;AAAA,QACH;AAAA,MACF;AAGA,YAAM,WAAW;AACjB,UAAI,OAAO;AACX,UAAI,iBAAiB;AACrB,UAAI,eAAe;AAEnB,iBAAS;AACP,eAAO,aAAa;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB,OAAO;AAAA,UACP,WAAW;AAAA,QACb,CAAC;AAED,cAAM,cAAc,MAAM,KAAK,YAAY,MAAM,OAAO,UAAU;AAAA,UAChE,UAAU,OAAO;AAAA,UACjB,gBAAgB,OAAO,kBAAkB;AAAA,UACzC,MAAM,EAAE,MAAM,SAAS;AAAA,UACvB,iBAAiB;AAAA,QACnB,CAAC;AAED,YAAI,CAAC,YAAY,MAAM,OAAQ;AAE/B,eAAO,aAAa;AAAA,UAClB,UAAU,OAAO;AAAA,UACjB,OAAO;AAAA,UACP,WAAW;AAAA,UACX,OAAO,YAAY;AAAA,QACrB,CAAC;AAGD,cAAM,eAAoC,CAAC;AAC3C,mBAAW,QAAQ,YAAY,OAAO;AACpC,gBAAM,WAAW,OAAQ,KAAiC,MAAM,EAAE;AAClE,cAAI,CAAC,UAAU;AACb,mBAAO,kBAAkB,OAAO,kBAAkB,KAAK;AACvD;AAAA,UACF;AACA,uBAAa,KAAK;AAAA,YAChB,UAAU,OAAO;AAAA,YACjB;AAAA,UACF,CAAC;AAAA,QACH;AAGA,YAAI,aAAa,SAAS,GAAG;AAC3B,cAAI,OAAO,aAAa,SAAS,KAAK,aAAa;AACjD,kBAAM,KAAK,YAAY,QAAQ;AAAA,cAC7B,SAAS;AAAA,cACT,UAAU,OAAO;AAAA,cACjB,gBAAgB,OAAO,kBAAkB;AAAA,cACzC,SAAS;AAAA,YACX,CAAC;AACD,4BAAgB;AAChB,8BAAkB,aAAa;AAC/B,wBAAY,iBAAiB,sCAAsC;AAAA,cACjE,UAAU,OAAO;AAAA,cACjB,WAAW,aAAa;AAAA,cACxB;AAAA,cACA;AAAA,YACF,CAAC;AAAA,UACH,OAAO;AAEL,uBAAW,EAAE,UAAU,SAAS,KAAK,cAAc;AACjD,kBAAI;AACF,sBAAM,KAAK,gBAAgB;AAAA,kBACzB;AAAA,kBACA;AAAA,kBACA,UAAU,OAAO;AAAA,kBACjB,gBAAgB,OAAO;AAAA,gBACzB,CAAC;AACD;AAAA,cACF,SAAS,OAAO;AACd,gCAAgB,iBAAiB,oCAAoC;AAAA,kBACnE;AAAA,kBACA;AAAA,kBACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,gBAClD,CAAC;AAAA,cACH;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,YAAI,YAAY,MAAM,SAAS,SAAU;AACzC,gBAAQ;AAGR,YAAI,OAAO,WAAW;AACpB,0BAAgB,iBAAiB,gDAAgD;AAAA,YAC/E,UAAU,OAAO;AAAA,YACjB,UAAU;AAAA,YACV;AAAA,UACF,CAAC;AACD;AAAA,QACF;AAAA,MACF;AAEA,aAAO,oBAAoB;AAC3B,aAAO,iBAAiB;AACxB,aAAO,eAAe;AAEtB,aAAO,aAAa;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB,OAAO;AAAA,QACP,WAAW;AAAA,QACX,OAAO;AAAA,MACT,CAAC;AAAA,IACH,SAAS,OAAO;AACd,aAAO,UAAU;AACjB,aAAO,OAAO,KAAK;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC9D,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,mBAAmB,QAA6E;AACpG,UAAM,SAAwB;AAAA,MAC5B,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,cAAc;AAAA,MACd,QAAQ,CAAC;AAAA,IACX;AAEA,UAAM,WAAW,KAAK,oBAAoB;AAC1C,eAAW,YAAY,UAAU;AAC/B,YAAM,eAAe,MAAM,KAAK,sBAAsB;AAAA,QACpD;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,QACvB,YAAY,OAAO;AAAA,QACnB,UAAU,OAAO;AAAA,QACjB,YAAY,OAAO;AAAA,MACrB,CAAC;AAED,aAAO,qBAAqB,aAAa;AACzC,aAAO,kBAAkB,aAAa;AACtC,aAAO,kBAAkB,OAAO,kBAAkB,MAAM,aAAa,kBAAkB;AACvF,aAAO,gBAAgB,OAAO,gBAAgB,MAAM,aAAa,gBAAgB;AACjF,aAAO,OAAO,KAAK,GAAG,aAAa,MAAM;AAEzC,UAAI,CAAC,aAAa,SAAS;AACzB,eAAO,UAAU;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,sBACZ,UACA,UACA,gBACA,OACA,QAC0D;AAC1D,UAAM,UAA6B,CAAC;AACpC,QAAI,UAAU;AAGd,QAAI,MAAM,SAAS,GAAG;AACpB,kBAAY,iBAAiB,yBAAyB;AAAA,QACpD;AAAA,QACA,YAAY,OAAO,KAAK,MAAM,CAAC,CAAC;AAAA,QAChC,UAAU,MAAM,CAAC,EAAE;AAAA,QACnB,OAAO,QAAQ,MAAM,CAAC;AAAA,QACtB,WAAW,MAAM,CAAC,EAAE;AAAA,QACpB,UAAU,MAAM,CAAC,EAAE;AAAA,QACnB,eAAe,MAAM,CAAC,EAAE;AAAA,QACxB,YAAY,KAAK,UAAU,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG;AAAA,MACnD,CAAC;AAAA,IACH;AAEA,eAAW,QAAQ,OAAO;AACxB,YAAM,WAAW,OAAO,KAAK,MAAM,EAAE;AACrC,UAAI,CAAC,UAAU;AACb,wBAAgB,iBAAiB,4BAA4B,EAAE,UAAU,UAAU,OAAO,KAAK,IAAI,EAAE,CAAC;AACtG;AACA;AAAA,MACF;AAGA,YAAM,eAAwC,CAAC;AAC/C,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,YAAI,IAAI,WAAW,KAAK,KAAK,IAAI,WAAW,KAAK,GAAG;AAClD,gBAAM,QAAQ,IAAI,MAAM,CAAC;AACzB,uBAAa,KAAK,IAAI;AAAA,QACxB;AAAA,MACF;AAEA,YAAM,eAAmC;AAAA,QACvC,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA,aAAa,KAAK;AAAA,MACpB;AAGA,UAAI;AACJ,UAAI;AACJ,UAAI;AACJ,UAAI;AACJ,UAAI;AAEJ,UAAI,OAAO,aAAa;AACtB,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,YAAY,YAAY;AACpD,cAAI,QAAQ;AACV,mBAAO,OAAO;AACd,gBAAI,OAAO,UAAW,aAAY,OAAO;AACzC,gBAAI,OAAO,MAAO,SAAQ,OAAO;AACjC,gBAAI,OAAO,mBAAmB,OAAW,kBAAiB,OAAO;AAAA,UACnE;AAAA,QACF,SAAS,KAAK;AACZ,0BAAgB,iBAAiB,sBAAsB;AAAA,YACrD;AAAA,YACA;AAAA,YACA,OAAO,eAAe,QAAQ,IAAI,UAAU;AAAA,UAC9C,CAAC;AAAA,QACH;AAAA,MACF;AAGA,UAAI,CAAC,aAAa,OAAO,cAAc;AACrC,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,cAAI,OAAQ,aAAY;AAAA,QAC1B,QAAQ;AAAA,QAER;AAAA,MACF;AAGA,UAAI,CAAC,OAAO,OAAO,YAAY;AAC7B,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,WAAW,YAAY;AACnD,cAAI,OAAQ,OAAM;AAAA,QACpB,QAAQ;AAAA,QAER;AAAA,MACF;AAGA,UAAI,CAAC,SAAS,OAAO,cAAc;AACjC,YAAI;AACF,gBAAM,SAAS,MAAM,OAAO,aAAa,YAAY;AACrD,cAAI,OAAQ,SAAQ;AAAA,QACtB,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,cAAQ,KAAK;AAAA,QACX;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,gBAAY,iBAAiB,6BAA6B;AAAA,MACxD;AAAA,MACA,YAAY,MAAM;AAAA,MAClB,aAAa,QAAQ;AAAA,MACrB;AAAA,IACF,CAAC;AAED,WAAO,EAAE,SAAS,QAAQ;AAAA,EAC5B;AACF;",
6
6
  "names": []
7
7
  }
package/dist/service.js CHANGED
@@ -142,17 +142,28 @@ class SearchService {
142
142
  return Promise.all(records.map((record) => this.executeStrategyIndex(strategy, record)));
143
143
  })
144
144
  );
145
+ const failures = [];
145
146
  for (let i = 0; i < results.length; i++) {
146
147
  const result = results[i];
147
148
  if (result.status === "rejected") {
148
149
  const strategy = strategies[i];
150
+ const errorMessage = result.reason instanceof Error ? result.reason.message : result.reason;
151
+ failures.push({
152
+ strategyId: strategy?.id || "unknown",
153
+ error: errorMessage
154
+ });
149
155
  searchError("SearchService", "Strategy bulkIndex failed", {
150
156
  strategyId: strategy?.id,
151
157
  recordCount: records.length,
152
- error: result.reason instanceof Error ? result.reason.message : result.reason
158
+ error: errorMessage
153
159
  });
154
160
  }
155
161
  }
162
+ if (failures.length > 0) {
163
+ throw new Error(
164
+ `Bulk indexing failed for ${failures.length} strategy(ies): ${failures.map((f) => `${f.strategyId} (${f.error})`).join(", ")}`
165
+ );
166
+ }
156
167
  }
157
168
  /**
158
169
  * Purge all records for an entity type.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/service.ts"],
4
- "sourcesContent": ["import type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n SearchServiceOptions,\n ResultMergeConfig,\n IndexableRecord,\n PresenterEnricherFn,\n} from './types'\nimport { mergeAndRankResults } from './lib/merger'\nimport { searchError } from './lib/debug'\nimport { needsSearchResultEnrichment } from './lib/search-result-enrichment'\n\n/**\n * Default merge configuration.\n */\nconst DEFAULT_MERGE_CONFIG: ResultMergeConfig = {\n duplicateHandling: 'highest_score',\n}\n\n/**\n * SearchService orchestrates multiple search strategies, executing searches in parallel\n * and merging results using the RRF algorithm.\n *\n * Features:\n * - Parallel strategy execution for optimal performance\n * - Graceful degradation when strategies fail\n * - Result merging with configurable weights\n * - Strategy availability checking\n *\n * @example\n * ```typescript\n * const service = new SearchService({\n * strategies: [tokenStrategy, vectorStrategy, fulltextStrategy],\n * defaultStrategies: ['fulltext', 'vector', 'tokens'],\n * mergeConfig: {\n * duplicateHandling: 'highest_score',\n * strategyWeights: { fulltext: 1.2, vector: 1.0, tokens: 0.8 },\n * },\n * })\n *\n * const results = await service.search('john doe', { tenantId: 'tenant-123' })\n * ```\n */\nexport class SearchService {\n private readonly strategies: Map<SearchStrategyId, SearchStrategy>\n private readonly defaultStrategies: SearchStrategyId[]\n private readonly fallbackStrategy: SearchStrategyId | undefined\n private readonly mergeConfig: ResultMergeConfig\n private readonly presenterEnricher?: PresenterEnricherFn\n\n constructor(options: SearchServiceOptions = {}) {\n this.strategies = new Map()\n for (const strategy of options.strategies ?? []) {\n this.strategies.set(strategy.id, strategy)\n }\n this.defaultStrategies = options.defaultStrategies ?? ['tokens']\n this.fallbackStrategy = options.fallbackStrategy\n this.mergeConfig = options.mergeConfig ?? DEFAULT_MERGE_CONFIG\n this.presenterEnricher = options.presenterEnricher\n }\n\n /**\n * Get all registered strategies.\n */\n getStrategies(): SearchStrategy[] {\n return Array.from(this.strategies.values())\n }\n\n /**\n * Execute a search query across configured strategies.\n *\n * @param query - Search query string\n * @param options - Search options with tenant, filters, etc.\n * @returns Merged and ranked search results\n */\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n const strategyIds = options.strategies ?? this.defaultStrategies\n const activeStrategies = await this.getAvailableStrategies(strategyIds)\n\n if (activeStrategies.length === 0) {\n // Try fallback strategy if defined\n if (this.fallbackStrategy) {\n const fallback = await this.getAvailableStrategies([this.fallbackStrategy])\n if (fallback.length > 0) {\n activeStrategies.push(...fallback)\n }\n }\n }\n\n if (activeStrategies.length === 0) {\n return []\n }\n\n // Execute searches in parallel with graceful degradation\n const results = await Promise.allSettled(\n activeStrategies.map((strategy) => this.executeStrategySearch(strategy, query, options)),\n )\n\n // Collect successful results, log failures\n const allResults: SearchResult[] = []\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'fulfilled') {\n allResults.push(...result.value)\n } else {\n const strategy = activeStrategies[i]\n searchError('SearchService', 'Strategy search failed', {\n strategyId: strategy?.id,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n\n // Merge and rank results\n const merged = mergeAndRankResults(allResults, this.mergeConfig)\n\n // Enrich results missing presenter or navigation metadata\n return this.enrichResultsWithPresenter(merged, options.tenantId, options.organizationId)\n }\n\n /**\n * Enrich results that are missing presenter data using the configured enricher.\n * This ensures token-only results get proper titles/subtitles for display.\n */\n private async enrichResultsWithPresenter(\n results: SearchResult[],\n tenantId: string,\n organizationId?: string | null,\n ): Promise<SearchResult[]> {\n // If no enricher configured, return as-is\n if (!this.presenterEnricher) return results\n\n const hasMissing = results.some(needsSearchResultEnrichment)\n if (!hasMissing) return results\n\n // Use the configured presenter enricher\n try {\n return await this.presenterEnricher(results, tenantId, organizationId)\n } catch {\n // Enrichment failed, return results as-is\n return results\n }\n }\n\n /**\n * Index a record across all available strategies.\n *\n * @param record - Record to index\n */\n async index(record: IndexableRecord): Promise<void> {\n const strategies = await this.getAvailableStrategies()\n\n if (strategies.length === 0) {\n return\n }\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => this.executeStrategyIndex(strategy, record)),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy index failed', {\n strategyId: strategy?.id,\n entityId: record.entityId,\n recordId: record.recordId,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Delete a record from all strategies.\n *\n * @param entityId - Entity type identifier\n * @param recordId - Record primary key\n * @param tenantId - Tenant for isolation\n */\n async delete(entityId: string, recordId: string, tenantId: string): Promise<void> {\n const strategies = await this.getAvailableStrategies()\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => this.executeStrategyDelete(strategy, entityId, recordId, tenantId)),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy delete failed', {\n strategyId: strategy?.id,\n entityId,\n recordId,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Bulk index multiple records.\n *\n * @param records - Records to index\n */\n async bulkIndex(records: IndexableRecord[]): Promise<void> {\n if (records.length === 0) return\n\n const strategies = await this.getAvailableStrategies()\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => {\n if (strategy.bulkIndex) {\n return strategy.bulkIndex(records)\n }\n // Fallback to individual indexing\n return Promise.all(records.map((record) => this.executeStrategyIndex(strategy, record)))\n }),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy bulkIndex failed', {\n strategyId: strategy?.id,\n recordCount: records.length,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Purge all records for an entity type.\n *\n * @param entityId - Entity type to purge\n * @param tenantId - Tenant for isolation\n */\n async purge(entityId: string, tenantId: string): Promise<void> {\n const strategies = await this.getAvailableStrategies()\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => {\n if (strategy.purge) {\n return strategy.purge(entityId, tenantId)\n }\n return Promise.resolve()\n }),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy purge failed', {\n strategyId: strategy?.id,\n entityId,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Register a new strategy at runtime.\n *\n * @param strategy - Strategy to register\n */\n registerStrategy(strategy: SearchStrategy): void {\n this.strategies.set(strategy.id, strategy)\n }\n\n /**\n * Unregister a strategy.\n *\n * @param strategyId - Strategy ID to remove\n */\n unregisterStrategy(strategyId: SearchStrategyId): void {\n this.strategies.delete(strategyId)\n }\n\n /**\n * Get all registered strategy IDs.\n */\n getRegisteredStrategies(): SearchStrategyId[] {\n return Array.from(this.strategies.keys())\n }\n\n /**\n * Get a specific strategy by ID.\n *\n * @param strategyId - Strategy ID to retrieve\n * @returns The strategy if registered, undefined otherwise\n */\n getStrategy(strategyId: SearchStrategyId): SearchStrategy | undefined {\n return this.strategies.get(strategyId)\n }\n\n /**\n * Get the default strategies list.\n */\n getDefaultStrategies(): SearchStrategyId[] {\n return [...this.defaultStrategies]\n }\n\n /**\n * Check if a specific strategy is available.\n *\n * @param strategyId - Strategy ID to check\n */\n async isStrategyAvailable(strategyId: SearchStrategyId): Promise<boolean> {\n const strategy = this.strategies.get(strategyId)\n if (!strategy) return false\n return strategy.isAvailable()\n }\n\n /**\n * Get available strategies from the requested list.\n * Filters out strategies that are not registered or not available.\n */\n private async getAvailableStrategies(ids?: SearchStrategyId[]): Promise<SearchStrategy[]> {\n const targetIds = ids ?? Array.from(this.strategies.keys())\n const available: SearchStrategy[] = []\n\n for (const id of targetIds) {\n const strategy = this.strategies.get(id)\n if (strategy) {\n try {\n const isAvailable = await strategy.isAvailable()\n if (isAvailable) {\n available.push(strategy)\n }\n } catch {\n // Strategy availability check failed, skip it\n }\n }\n }\n\n // Sort by priority (higher priority first)\n return available.sort((a, b) => b.priority - a.priority)\n }\n\n /**\n * Execute search on a single strategy with error handling.\n */\n private async executeStrategySearch(\n strategy: SearchStrategy,\n query: string,\n options: SearchOptions,\n ): Promise<SearchResult[]> {\n await strategy.ensureReady()\n return strategy.search(query, options)\n }\n\n /**\n * Execute index on a single strategy with error handling.\n */\n private async executeStrategyIndex(\n strategy: SearchStrategy,\n record: IndexableRecord,\n ): Promise<void> {\n await strategy.ensureReady()\n return strategy.index(record)\n }\n\n /**\n * Execute delete on a single strategy with error handling.\n */\n private async executeStrategyDelete(\n strategy: SearchStrategy,\n entityId: string,\n recordId: string,\n tenantId: string,\n ): Promise<void> {\n await strategy.ensureReady()\n return strategy.delete(entityId, recordId, tenantId)\n }\n}\n"],
5
- "mappings": "AAUA,SAAS,2BAA2B;AACpC,SAAS,mBAAmB;AAC5B,SAAS,mCAAmC;AAK5C,MAAM,uBAA0C;AAAA,EAC9C,mBAAmB;AACrB;AA0BO,MAAM,cAAc;AAAA,EAOzB,YAAY,UAAgC,CAAC,GAAG;AAC9C,SAAK,aAAa,oBAAI,IAAI;AAC1B,eAAW,YAAY,QAAQ,cAAc,CAAC,GAAG;AAC/C,WAAK,WAAW,IAAI,SAAS,IAAI,QAAQ;AAAA,IAC3C;AACA,SAAK,oBAAoB,QAAQ,qBAAqB,CAAC,QAAQ;AAC/D,SAAK,mBAAmB,QAAQ;AAChC,SAAK,cAAc,QAAQ,eAAe;AAC1C,SAAK,oBAAoB,QAAQ;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAkC;AAChC,WAAO,MAAM,KAAK,KAAK,WAAW,OAAO,CAAC;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAO,OAAe,SAAiD;AAC3E,UAAM,cAAc,QAAQ,cAAc,KAAK;AAC/C,UAAM,mBAAmB,MAAM,KAAK,uBAAuB,WAAW;AAEtE,QAAI,iBAAiB,WAAW,GAAG;AAEjC,UAAI,KAAK,kBAAkB;AACzB,cAAM,WAAW,MAAM,KAAK,uBAAuB,CAAC,KAAK,gBAAgB,CAAC;AAC1E,YAAI,SAAS,SAAS,GAAG;AACvB,2BAAiB,KAAK,GAAG,QAAQ;AAAA,QACnC;AAAA,MACF;AAAA,IACF;AAEA,QAAI,iBAAiB,WAAW,GAAG;AACjC,aAAO,CAAC;AAAA,IACV;AAGA,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,iBAAiB,IAAI,CAAC,aAAa,KAAK,sBAAsB,UAAU,OAAO,OAAO,CAAC;AAAA,IACzF;AAGA,UAAM,aAA6B,CAAC;AACpC,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,SAAS,QAAQ,CAAC;AACxB,UAAI,OAAO,WAAW,aAAa;AACjC,mBAAW,KAAK,GAAG,OAAO,KAAK;AAAA,MACjC,OAAO;AACL,cAAM,WAAW,iBAAiB,CAAC;AACnC,oBAAY,iBAAiB,0BAA0B;AAAA,UACrD,YAAY,UAAU;AAAA,UACtB,OAAO,OAAO,kBAAkB,QAAQ,OAAO,OAAO,UAAU,OAAO;AAAA,QACzE,CAAC;AAAA,MACH;AAAA,IACF;AAGA,UAAM,SAAS,oBAAoB,YAAY,KAAK,WAAW;AAG/D,WAAO,KAAK,2BAA2B,QAAQ,QAAQ,UAAU,QAAQ,cAAc;AAAA,EACzF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,2BACZ,SACA,UACA,gBACyB;AAEzB,QAAI,CAAC,KAAK,kBAAmB,QAAO;AAEpC,UAAM,aAAa,QAAQ,KAAK,2BAA2B;AAC3D,QAAI,CAAC,WAAY,QAAO;AAGxB,QAAI;AACF,aAAO,MAAM,KAAK,kBAAkB,SAAS,UAAU,cAAc;AAAA,IACvE,QAAQ;AAEN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,MAAM,QAAwC;AAClD,UAAM,aAAa,MAAM,KAAK,uBAAuB;AAErD,QAAI,WAAW,WAAW,GAAG;AAC3B;AAAA,IACF;AAEA,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,WAAW,IAAI,CAAC,aAAa,KAAK,qBAAqB,UAAU,MAAM,CAAC;AAAA,IAC1E;AAGA,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,SAAS,QAAQ,CAAC;AACxB,UAAI,OAAO,WAAW,YAAY;AAChC,cAAM,WAAW,WAAW,CAAC;AAC7B,oBAAY,iBAAiB,yBAAyB;AAAA,UACpD,YAAY,UAAU;AAAA,UACtB,UAAU,OAAO;AAAA,UACjB,UAAU,OAAO;AAAA,UACjB,OAAO,OAAO,kBAAkB,QAAQ,OAAO,OAAO,UAAU,OAAO;AAAA,QACzE,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAO,UAAkB,UAAkB,UAAiC;AAChF,UAAM,aAAa,MAAM,KAAK,uBAAuB;AAErD,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,WAAW,IAAI,CAAC,aAAa,KAAK,sBAAsB,UAAU,UAAU,UAAU,QAAQ,CAAC;AAAA,IACjG;AAGA,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,SAAS,QAAQ,CAAC;AACxB,UAAI,OAAO,WAAW,YAAY;AAChC,cAAM,WAAW,WAAW,CAAC;AAC7B,oBAAY,iBAAiB,0BAA0B;AAAA,UACrD,YAAY,UAAU;AAAA,UACtB;AAAA,UACA;AAAA,UACA,OAAO,OAAO,kBAAkB,QAAQ,OAAO,OAAO,UAAU,OAAO;AAAA,QACzE,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UAAU,SAA2C;AACzD,QAAI,QAAQ,WAAW,EAAG;AAE1B,UAAM,aAAa,MAAM,KAAK,uBAAuB;AAErD,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,WAAW,IAAI,CAAC,aAAa;AAC3B,YAAI,SAAS,WAAW;AACtB,iBAAO,SAAS,UAAU,OAAO;AAAA,QACnC;AAEA,eAAO,QAAQ,IAAI,QAAQ,IAAI,CAAC,WAAW,KAAK,qBAAqB,UAAU,MAAM,CAAC,CAAC;AAAA,MACzF,CAAC;AAAA,IACH;AAGA,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,SAAS,QAAQ,CAAC;AACxB,UAAI,OAAO,WAAW,YAAY;AAChC,cAAM,WAAW,WAAW,CAAC;AAC7B,oBAAY,iBAAiB,6BAA6B;AAAA,UACxD,YAAY,UAAU;AAAA,UACtB,aAAa,QAAQ;AAAA,UACrB,OAAO,OAAO,kBAAkB,QAAQ,OAAO,OAAO,UAAU,OAAO;AAAA,QACzE,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,MAAM,UAAkB,UAAiC;AAC7D,UAAM,aAAa,MAAM,KAAK,uBAAuB;AAErD,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,WAAW,IAAI,CAAC,aAAa;AAC3B,YAAI,SAAS,OAAO;AAClB,iBAAO,SAAS,MAAM,UAAU,QAAQ;AAAA,QAC1C;AACA,eAAO,QAAQ,QAAQ;AAAA,MACzB,CAAC;AAAA,IACH;AAGA,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,SAAS,QAAQ,CAAC;AACxB,UAAI,OAAO,WAAW,YAAY;AAChC,cAAM,WAAW,WAAW,CAAC;AAC7B,oBAAY,iBAAiB,yBAAyB;AAAA,UACpD,YAAY,UAAU;AAAA,UACtB;AAAA,UACA,OAAO,OAAO,kBAAkB,QAAQ,OAAO,OAAO,UAAU,OAAO;AAAA,QACzE,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,UAAgC;AAC/C,SAAK,WAAW,IAAI,SAAS,IAAI,QAAQ;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,mBAAmB,YAAoC;AACrD,SAAK,WAAW,OAAO,UAAU;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAKA,0BAA8C;AAC5C,WAAO,MAAM,KAAK,KAAK,WAAW,KAAK,CAAC;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,YAAY,YAA0D;AACpE,WAAO,KAAK,WAAW,IAAI,UAAU;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA,EAKA,uBAA2C;AACzC,WAAO,CAAC,GAAG,KAAK,iBAAiB;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,oBAAoB,YAAgD;AACxE,UAAM,WAAW,KAAK,WAAW,IAAI,UAAU;AAC/C,QAAI,CAAC,SAAU,QAAO;AACtB,WAAO,SAAS,YAAY;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,uBAAuB,KAAqD;AACxF,UAAM,YAAY,OAAO,MAAM,KAAK,KAAK,WAAW,KAAK,CAAC;AAC1D,UAAM,YAA8B,CAAC;AAErC,eAAW,MAAM,WAAW;AAC1B,YAAM,WAAW,KAAK,WAAW,IAAI,EAAE;AACvC,UAAI,UAAU;AACZ,YAAI;AACF,gBAAM,cAAc,MAAM,SAAS,YAAY;AAC/C,cAAI,aAAa;AACf,sBAAU,KAAK,QAAQ;AAAA,UACzB;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAGA,WAAO,UAAU,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,sBACZ,UACA,OACA,SACyB;AACzB,UAAM,SAAS,YAAY;AAC3B,WAAO,SAAS,OAAO,OAAO,OAAO;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,qBACZ,UACA,QACe;AACf,UAAM,SAAS,YAAY;AAC3B,WAAO,SAAS,MAAM,MAAM;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,sBACZ,UACA,UACA,UACA,UACe;AACf,UAAM,SAAS,YAAY;AAC3B,WAAO,SAAS,OAAO,UAAU,UAAU,QAAQ;AAAA,EACrD;AACF;",
4
+ "sourcesContent": ["import type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n SearchServiceOptions,\n ResultMergeConfig,\n IndexableRecord,\n PresenterEnricherFn,\n} from './types'\nimport { mergeAndRankResults } from './lib/merger'\nimport { searchError } from './lib/debug'\nimport { needsSearchResultEnrichment } from './lib/search-result-enrichment'\n\n/**\n * Default merge configuration.\n */\nconst DEFAULT_MERGE_CONFIG: ResultMergeConfig = {\n duplicateHandling: 'highest_score',\n}\n\n/**\n * SearchService orchestrates multiple search strategies, executing searches in parallel\n * and merging results using the RRF algorithm.\n *\n * Features:\n * - Parallel strategy execution for optimal performance\n * - Graceful degradation when strategies fail\n * - Result merging with configurable weights\n * - Strategy availability checking\n *\n * @example\n * ```typescript\n * const service = new SearchService({\n * strategies: [tokenStrategy, vectorStrategy, fulltextStrategy],\n * defaultStrategies: ['fulltext', 'vector', 'tokens'],\n * mergeConfig: {\n * duplicateHandling: 'highest_score',\n * strategyWeights: { fulltext: 1.2, vector: 1.0, tokens: 0.8 },\n * },\n * })\n *\n * const results = await service.search('john doe', { tenantId: 'tenant-123' })\n * ```\n */\nexport class SearchService {\n private readonly strategies: Map<SearchStrategyId, SearchStrategy>\n private readonly defaultStrategies: SearchStrategyId[]\n private readonly fallbackStrategy: SearchStrategyId | undefined\n private readonly mergeConfig: ResultMergeConfig\n private readonly presenterEnricher?: PresenterEnricherFn\n\n constructor(options: SearchServiceOptions = {}) {\n this.strategies = new Map()\n for (const strategy of options.strategies ?? []) {\n this.strategies.set(strategy.id, strategy)\n }\n this.defaultStrategies = options.defaultStrategies ?? ['tokens']\n this.fallbackStrategy = options.fallbackStrategy\n this.mergeConfig = options.mergeConfig ?? DEFAULT_MERGE_CONFIG\n this.presenterEnricher = options.presenterEnricher\n }\n\n /**\n * Get all registered strategies.\n */\n getStrategies(): SearchStrategy[] {\n return Array.from(this.strategies.values())\n }\n\n /**\n * Execute a search query across configured strategies.\n *\n * @param query - Search query string\n * @param options - Search options with tenant, filters, etc.\n * @returns Merged and ranked search results\n */\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n const strategyIds = options.strategies ?? this.defaultStrategies\n const activeStrategies = await this.getAvailableStrategies(strategyIds)\n\n if (activeStrategies.length === 0) {\n // Try fallback strategy if defined\n if (this.fallbackStrategy) {\n const fallback = await this.getAvailableStrategies([this.fallbackStrategy])\n if (fallback.length > 0) {\n activeStrategies.push(...fallback)\n }\n }\n }\n\n if (activeStrategies.length === 0) {\n return []\n }\n\n // Execute searches in parallel with graceful degradation\n const results = await Promise.allSettled(\n activeStrategies.map((strategy) => this.executeStrategySearch(strategy, query, options)),\n )\n\n // Collect successful results, log failures\n const allResults: SearchResult[] = []\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'fulfilled') {\n allResults.push(...result.value)\n } else {\n const strategy = activeStrategies[i]\n searchError('SearchService', 'Strategy search failed', {\n strategyId: strategy?.id,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n\n // Merge and rank results\n const merged = mergeAndRankResults(allResults, this.mergeConfig)\n\n // Enrich results missing presenter or navigation metadata\n return this.enrichResultsWithPresenter(merged, options.tenantId, options.organizationId)\n }\n\n /**\n * Enrich results that are missing presenter data using the configured enricher.\n * This ensures token-only results get proper titles/subtitles for display.\n */\n private async enrichResultsWithPresenter(\n results: SearchResult[],\n tenantId: string,\n organizationId?: string | null,\n ): Promise<SearchResult[]> {\n // If no enricher configured, return as-is\n if (!this.presenterEnricher) return results\n\n const hasMissing = results.some(needsSearchResultEnrichment)\n if (!hasMissing) return results\n\n // Use the configured presenter enricher\n try {\n return await this.presenterEnricher(results, tenantId, organizationId)\n } catch {\n // Enrichment failed, return results as-is\n return results\n }\n }\n\n /**\n * Index a record across all available strategies.\n *\n * @param record - Record to index\n */\n async index(record: IndexableRecord): Promise<void> {\n const strategies = await this.getAvailableStrategies()\n\n if (strategies.length === 0) {\n return\n }\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => this.executeStrategyIndex(strategy, record)),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy index failed', {\n strategyId: strategy?.id,\n entityId: record.entityId,\n recordId: record.recordId,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Delete a record from all strategies.\n *\n * @param entityId - Entity type identifier\n * @param recordId - Record primary key\n * @param tenantId - Tenant for isolation\n */\n async delete(entityId: string, recordId: string, tenantId: string): Promise<void> {\n const strategies = await this.getAvailableStrategies()\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => this.executeStrategyDelete(strategy, entityId, recordId, tenantId)),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy delete failed', {\n strategyId: strategy?.id,\n entityId,\n recordId,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Bulk index multiple records.\n *\n * @param records - Records to index\n */\n async bulkIndex(records: IndexableRecord[]): Promise<void> {\n if (records.length === 0) return\n\n const strategies = await this.getAvailableStrategies()\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => {\n if (strategy.bulkIndex) {\n return strategy.bulkIndex(records)\n }\n // Fallback to individual indexing\n return Promise.all(records.map((record) => this.executeStrategyIndex(strategy, record)))\n }),\n )\n\n // Collect failures and throw if any occurred\n const failures: Array<{ strategyId: string; error: string }> = []\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n const errorMessage = result.reason instanceof Error ? result.reason.message : result.reason\n failures.push({\n strategyId: strategy?.id || 'unknown',\n error: errorMessage,\n })\n searchError('SearchService', 'Strategy bulkIndex failed', {\n strategyId: strategy?.id,\n recordCount: records.length,\n error: errorMessage,\n })\n }\n }\n\n if (failures.length > 0) {\n throw new Error(\n `Bulk indexing failed for ${failures.length} strategy(ies): ${failures\n .map((f) => `${f.strategyId} (${f.error})`)\n .join(', ')}`\n )\n }\n }\n\n /**\n * Purge all records for an entity type.\n *\n * @param entityId - Entity type to purge\n * @param tenantId - Tenant for isolation\n */\n async purge(entityId: string, tenantId: string): Promise<void> {\n const strategies = await this.getAvailableStrategies()\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => {\n if (strategy.purge) {\n return strategy.purge(entityId, tenantId)\n }\n return Promise.resolve()\n }),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy purge failed', {\n strategyId: strategy?.id,\n entityId,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Register a new strategy at runtime.\n *\n * @param strategy - Strategy to register\n */\n registerStrategy(strategy: SearchStrategy): void {\n this.strategies.set(strategy.id, strategy)\n }\n\n /**\n * Unregister a strategy.\n *\n * @param strategyId - Strategy ID to remove\n */\n unregisterStrategy(strategyId: SearchStrategyId): void {\n this.strategies.delete(strategyId)\n }\n\n /**\n * Get all registered strategy IDs.\n */\n getRegisteredStrategies(): SearchStrategyId[] {\n return Array.from(this.strategies.keys())\n }\n\n /**\n * Get a specific strategy by ID.\n *\n * @param strategyId - Strategy ID to retrieve\n * @returns The strategy if registered, undefined otherwise\n */\n getStrategy(strategyId: SearchStrategyId): SearchStrategy | undefined {\n return this.strategies.get(strategyId)\n }\n\n /**\n * Get the default strategies list.\n */\n getDefaultStrategies(): SearchStrategyId[] {\n return [...this.defaultStrategies]\n }\n\n /**\n * Check if a specific strategy is available.\n *\n * @param strategyId - Strategy ID to check\n */\n async isStrategyAvailable(strategyId: SearchStrategyId): Promise<boolean> {\n const strategy = this.strategies.get(strategyId)\n if (!strategy) return false\n return strategy.isAvailable()\n }\n\n /**\n * Get available strategies from the requested list.\n * Filters out strategies that are not registered or not available.\n */\n private async getAvailableStrategies(ids?: SearchStrategyId[]): Promise<SearchStrategy[]> {\n const targetIds = ids ?? Array.from(this.strategies.keys())\n const available: SearchStrategy[] = []\n\n for (const id of targetIds) {\n const strategy = this.strategies.get(id)\n if (strategy) {\n try {\n const isAvailable = await strategy.isAvailable()\n if (isAvailable) {\n available.push(strategy)\n }\n } catch {\n // Strategy availability check failed, skip it\n }\n }\n }\n\n // Sort by priority (higher priority first)\n return available.sort((a, b) => b.priority - a.priority)\n }\n\n /**\n * Execute search on a single strategy with error handling.\n */\n private async executeStrategySearch(\n strategy: SearchStrategy,\n query: string,\n options: SearchOptions,\n ): Promise<SearchResult[]> {\n await strategy.ensureReady()\n return strategy.search(query, options)\n }\n\n /**\n * Execute index on a single strategy with error handling.\n */\n private async executeStrategyIndex(\n strategy: SearchStrategy,\n record: IndexableRecord,\n ): Promise<void> {\n await strategy.ensureReady()\n return strategy.index(record)\n }\n\n /**\n * Execute delete on a single strategy with error handling.\n */\n private async executeStrategyDelete(\n strategy: SearchStrategy,\n entityId: string,\n recordId: string,\n tenantId: string,\n ): Promise<void> {\n await strategy.ensureReady()\n return strategy.delete(entityId, recordId, tenantId)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/search",
3
- "version": "0.4.11-develop.2207.23c94908f0",
3
+ "version": "0.4.11-develop.2212.7e6680a1e8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -126,9 +126,9 @@
126
126
  "zod": "^4.0.0"
127
127
  },
128
128
  "peerDependencies": {
129
- "@open-mercato/core": "0.4.11-develop.2207.23c94908f0",
130
- "@open-mercato/queue": "0.4.11-develop.2207.23c94908f0",
131
- "@open-mercato/shared": "0.4.11-develop.2207.23c94908f0"
129
+ "@open-mercato/core": "0.4.11-develop.2212.7e6680a1e8",
130
+ "@open-mercato/queue": "0.4.11-develop.2212.7e6680a1e8",
131
+ "@open-mercato/shared": "0.4.11-develop.2212.7e6680a1e8"
132
132
  },
133
133
  "devDependencies": {
134
134
  "@types/jest": "^30.0.0",
@@ -350,6 +350,36 @@ describe('SearchService', () => {
350
350
  expect(strategy.bulkIndex).not.toHaveBeenCalled()
351
351
  expect(strategy.index).not.toHaveBeenCalled()
352
352
  })
353
+
354
+ it('should throw error when strategy bulkIndex fails', async () => {
355
+ const strategy = createMockStrategy({
356
+ id: 'failing-strategy',
357
+ bulkIndex: jest.fn().mockRejectedValue(new Error('Index strategy failed')),
358
+ })
359
+ const service = new SearchService({ strategies: [strategy] })
360
+ const records = [createMockRecord({ recordId: 'rec-1' })]
361
+
362
+ await expect(service.bulkIndex(records)).rejects.toThrow(
363
+ 'Bulk indexing failed for 1 strategy(ies): failing-strategy (Index strategy failed)'
364
+ )
365
+ })
366
+
367
+ it('should throw error when multiple strategies fail', async () => {
368
+ const strategy1 = createMockStrategy({
369
+ id: 'failing1',
370
+ bulkIndex: jest.fn().mockRejectedValue(new Error('First failure')),
371
+ })
372
+ const strategy2 = createMockStrategy({
373
+ id: 'failing2',
374
+ bulkIndex: jest.fn().mockRejectedValue(new Error('Second failure')),
375
+ })
376
+ const service = new SearchService({ strategies: [strategy1, strategy2] })
377
+ const records = [createMockRecord({ recordId: 'rec-1' })]
378
+
379
+ await expect(service.bulkIndex(records)).rejects.toThrow(
380
+ 'Bulk indexing failed for 2 strategy(ies): failing1 (First failure), failing2 (Second failure)'
381
+ )
382
+ })
353
383
  })
354
384
 
355
385
  describe('delete', () => {
@@ -561,7 +561,15 @@ export class SearchIndexer {
561
561
  }
562
562
 
563
563
  if (indexableRecords.length > 0) {
564
- await this.searchService.bulkIndex(indexableRecords)
564
+ try {
565
+ await this.searchService.bulkIndex(indexableRecords)
566
+ } catch (error) {
567
+ throw new Error(
568
+ `Failed to bulk index ${indexableRecords.length} records: ${
569
+ error instanceof Error ? error.message : error
570
+ }`
571
+ )
572
+ }
565
573
  }
566
574
  }
567
575
 
package/src/service.ts CHANGED
@@ -224,18 +224,32 @@ export class SearchService {
224
224
  }),
225
225
  )
226
226
 
227
- // Log any failures
227
+ // Collect failures and throw if any occurred
228
+ const failures: Array<{ strategyId: string; error: string }> = []
228
229
  for (let i = 0; i < results.length; i++) {
229
230
  const result = results[i]
230
231
  if (result.status === 'rejected') {
231
232
  const strategy = strategies[i]
233
+ const errorMessage = result.reason instanceof Error ? result.reason.message : result.reason
234
+ failures.push({
235
+ strategyId: strategy?.id || 'unknown',
236
+ error: errorMessage,
237
+ })
232
238
  searchError('SearchService', 'Strategy bulkIndex failed', {
233
239
  strategyId: strategy?.id,
234
240
  recordCount: records.length,
235
- error: result.reason instanceof Error ? result.reason.message : result.reason,
241
+ error: errorMessage,
236
242
  })
237
243
  }
238
244
  }
245
+
246
+ if (failures.length > 0) {
247
+ throw new Error(
248
+ `Bulk indexing failed for ${failures.length} strategy(ies): ${failures
249
+ .map((f) => `${f.strategyId} (${f.error})`)
250
+ .join(', ')}`
251
+ )
252
+ }
239
253
  }
240
254
 
241
255
  /**