@open-mercato/search 0.4.2-canary-c02407ff85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +678 -0
- package/build.mjs +92 -0
- package/dist/di.js +157 -0
- package/dist/di.js.map +7 -0
- package/dist/fulltext/drivers/index.js +21 -0
- package/dist/fulltext/drivers/index.js.map +7 -0
- package/dist/fulltext/drivers/meilisearch/index.js +320 -0
- package/dist/fulltext/drivers/meilisearch/index.js.map +7 -0
- package/dist/fulltext/index.js +7 -0
- package/dist/fulltext/index.js.map +7 -0
- package/dist/fulltext/types.js +1 -0
- package/dist/fulltext/types.js.map +7 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +7 -0
- package/dist/indexer/index.js +8 -0
- package/dist/indexer/index.js.map +7 -0
- package/dist/indexer/search-indexer.js +848 -0
- package/dist/indexer/search-indexer.js.map +7 -0
- package/dist/indexer/subscribers/delete.js +41 -0
- package/dist/indexer/subscribers/delete.js.map +7 -0
- package/dist/lib/debug.js +34 -0
- package/dist/lib/debug.js.map +7 -0
- package/dist/lib/fallback-presenter.js +107 -0
- package/dist/lib/fallback-presenter.js.map +7 -0
- package/dist/lib/field-policy.js +75 -0
- package/dist/lib/field-policy.js.map +7 -0
- package/dist/lib/index.js +19 -0
- package/dist/lib/index.js.map +7 -0
- package/dist/lib/merger.js +93 -0
- package/dist/lib/merger.js.map +7 -0
- package/dist/lib/presenter-enricher.js +192 -0
- package/dist/lib/presenter-enricher.js.map +7 -0
- package/dist/modules/search/acl.js +14 -0
- package/dist/modules/search/acl.js.map +7 -0
- package/dist/modules/search/ai-tools.js +284 -0
- package/dist/modules/search/ai-tools.js.map +7 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js +65 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +7 -0
- package/dist/modules/search/api/embeddings/reindex/route.js +165 -0
- package/dist/modules/search/api/embeddings/reindex/route.js.map +7 -0
- package/dist/modules/search/api/embeddings/route.js +246 -0
- package/dist/modules/search/api/embeddings/route.js.map +7 -0
- package/dist/modules/search/api/index/route.js +245 -0
- package/dist/modules/search/api/index/route.js.map +7 -0
- package/dist/modules/search/api/reindex/cancel/route.js +65 -0
- package/dist/modules/search/api/reindex/cancel/route.js.map +7 -0
- package/dist/modules/search/api/reindex/route.js +332 -0
- package/dist/modules/search/api/reindex/route.js.map +7 -0
- package/dist/modules/search/api/search/global/route.js +100 -0
- package/dist/modules/search/api/search/global/route.js.map +7 -0
- package/dist/modules/search/api/search/route.js +101 -0
- package/dist/modules/search/api/search/route.js.map +7 -0
- package/dist/modules/search/api/settings/fulltext/route.js +55 -0
- package/dist/modules/search/api/settings/fulltext/route.js.map +7 -0
- package/dist/modules/search/api/settings/global-search/route.js +80 -0
- package/dist/modules/search/api/settings/global-search/route.js.map +7 -0
- package/dist/modules/search/api/settings/route.js +118 -0
- package/dist/modules/search/api/settings/route.js.map +7 -0
- package/dist/modules/search/api/settings/vector-store/route.js +77 -0
- package/dist/modules/search/api/settings/vector-store/route.js.map +7 -0
- package/dist/modules/search/backend/config/search/page.js +10 -0
- package/dist/modules/search/backend/config/search/page.js.map +7 -0
- package/dist/modules/search/backend/config/search/page.meta.js +24 -0
- package/dist/modules/search/backend/config/search/page.meta.js.map +7 -0
- package/dist/modules/search/cli.js +698 -0
- package/dist/modules/search/cli.js.map +7 -0
- package/dist/modules/search/di.js +32 -0
- package/dist/modules/search/di.js.map +7 -0
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js +357 -0
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +7 -0
- package/dist/modules/search/frontend/components/HybridSearchTable.js +343 -0
- package/dist/modules/search/frontend/components/HybridSearchTable.js.map +7 -0
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +303 -0
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +360 -0
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js +101 -0
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +608 -0
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/index.js +9 -0
- package/dist/modules/search/frontend/index.js.map +7 -0
- package/dist/modules/search/frontend/utils.js +41 -0
- package/dist/modules/search/frontend/utils.js.map +7 -0
- package/dist/modules/search/i18n/de.json +61 -0
- package/dist/modules/search/i18n/en.json +72 -0
- package/dist/modules/search/i18n/es.json +61 -0
- package/dist/modules/search/i18n/pl.json +61 -0
- package/dist/modules/search/index.js +11 -0
- package/dist/modules/search/index.js.map +7 -0
- package/dist/modules/search/lib/auto-indexing.js +29 -0
- package/dist/modules/search/lib/auto-indexing.js.map +7 -0
- package/dist/modules/search/lib/embedding-config.js +131 -0
- package/dist/modules/search/lib/embedding-config.js.map +7 -0
- package/dist/modules/search/lib/global-search-config.js +45 -0
- package/dist/modules/search/lib/global-search-config.js.map +7 -0
- package/dist/modules/search/lib/reindex-lock.js +99 -0
- package/dist/modules/search/lib/reindex-lock.js.map +7 -0
- package/dist/modules/search/subscribers/fulltext_upsert.js +64 -0
- package/dist/modules/search/subscribers/fulltext_upsert.js.map +7 -0
- package/dist/modules/search/subscribers/vector_delete.js +58 -0
- package/dist/modules/search/subscribers/vector_delete.js.map +7 -0
- package/dist/modules/search/subscribers/vector_purge.js +142 -0
- package/dist/modules/search/subscribers/vector_purge.js.map +7 -0
- package/dist/modules/search/subscribers/vector_upsert.js +58 -0
- package/dist/modules/search/subscribers/vector_upsert.js.map +7 -0
- package/dist/modules/search/workers/fulltext-index.worker.js +240 -0
- package/dist/modules/search/workers/fulltext-index.worker.js.map +7 -0
- package/dist/modules/search/workers/vector-index.worker.js +234 -0
- package/dist/modules/search/workers/vector-index.worker.js.map +7 -0
- package/dist/queue/fulltext-indexing.js +15 -0
- package/dist/queue/fulltext-indexing.js.map +7 -0
- package/dist/queue/index.js +3 -0
- package/dist/queue/index.js.map +7 -0
- package/dist/queue/vector-indexing.js +15 -0
- package/dist/queue/vector-indexing.js.map +7 -0
- package/dist/service.js +286 -0
- package/dist/service.js.map +7 -0
- package/dist/strategies/fulltext.strategy.js +116 -0
- package/dist/strategies/fulltext.strategy.js.map +7 -0
- package/dist/strategies/index.js +12 -0
- package/dist/strategies/index.js.map +7 -0
- package/dist/strategies/token.strategy.js +80 -0
- package/dist/strategies/token.strategy.js.map +7 -0
- package/dist/strategies/vector.strategy.js +137 -0
- package/dist/strategies/vector.strategy.js.map +7 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +7 -0
- package/dist/vector/drivers/chromadb/index.js +44 -0
- package/dist/vector/drivers/chromadb/index.js.map +7 -0
- package/dist/vector/drivers/index.js +9 -0
- package/dist/vector/drivers/index.js.map +7 -0
- package/dist/vector/drivers/pgvector/index.js +509 -0
- package/dist/vector/drivers/pgvector/index.js.map +7 -0
- package/dist/vector/drivers/qdrant/index.js +44 -0
- package/dist/vector/drivers/qdrant/index.js.map +7 -0
- package/dist/vector/index.js +4 -0
- package/dist/vector/index.js.map +7 -0
- package/dist/vector/lib/vector-logs.js +33 -0
- package/dist/vector/lib/vector-logs.js.map +7 -0
- package/dist/vector/services/checksum.js +20 -0
- package/dist/vector/services/checksum.js.map +7 -0
- package/dist/vector/services/embedding.js +222 -0
- package/dist/vector/services/embedding.js.map +7 -0
- package/dist/vector/services/index.js +4 -0
- package/dist/vector/services/index.js.map +7 -0
- package/dist/vector/services/vector-index.service.js +960 -0
- package/dist/vector/services/vector-index.service.js.map +7 -0
- package/dist/vector/types/pg.d.js +1 -0
- package/dist/vector/types/pg.d.js.map +7 -0
- package/dist/vector/types.js +75 -0
- package/dist/vector/types.js.map +7 -0
- package/jest.config.cjs +19 -0
- package/package.json +142 -0
- package/src/__tests__/queue.test.ts +148 -0
- package/src/__tests__/service.test.ts +345 -0
- package/src/__tests__/workers.test.ts +319 -0
- package/src/di.ts +291 -0
- package/src/fulltext/drivers/index.ts +41 -0
- package/src/fulltext/drivers/meilisearch/index.ts +410 -0
- package/src/fulltext/index.ts +13 -0
- package/src/fulltext/types.ts +115 -0
- package/src/index.ts +36 -0
- package/src/indexer/index.ts +13 -0
- package/src/indexer/search-indexer.ts +1141 -0
- package/src/indexer/subscribers/delete.ts +49 -0
- package/src/lib/debug.ts +46 -0
- package/src/lib/fallback-presenter.ts +106 -0
- package/src/lib/field-policy.ts +169 -0
- package/src/lib/index.ts +13 -0
- package/src/lib/merger.ts +159 -0
- package/src/lib/presenter-enricher.ts +323 -0
- package/src/modules/search/README.md +694 -0
- package/src/modules/search/acl.ts +10 -0
- package/src/modules/search/ai-tools.ts +467 -0
- package/src/modules/search/api/embeddings/reindex/cancel/route.ts +77 -0
- package/src/modules/search/api/embeddings/reindex/route.ts +197 -0
- package/src/modules/search/api/embeddings/route.ts +304 -0
- package/src/modules/search/api/index/route.ts +297 -0
- package/src/modules/search/api/reindex/cancel/route.ts +77 -0
- package/src/modules/search/api/reindex/route.ts +419 -0
- package/src/modules/search/api/search/global/route.ts +120 -0
- package/src/modules/search/api/search/route.ts +121 -0
- package/src/modules/search/api/settings/fulltext/route.ts +82 -0
- package/src/modules/search/api/settings/global-search/route.ts +91 -0
- package/src/modules/search/api/settings/route.ts +187 -0
- package/src/modules/search/api/settings/vector-store/route.ts +105 -0
- package/src/modules/search/backend/config/search/page.meta.ts +22 -0
- package/src/modules/search/backend/config/search/page.tsx +12 -0
- package/src/modules/search/cli.ts +818 -0
- package/src/modules/search/di.ts +50 -0
- package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +436 -0
- package/src/modules/search/frontend/components/HybridSearchTable.tsx +418 -0
- package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +476 -0
- package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +624 -0
- package/src/modules/search/frontend/components/sections/GlobalSearchSection.tsx +124 -0
- package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +943 -0
- package/src/modules/search/frontend/index.ts +3 -0
- package/src/modules/search/frontend/utils.ts +82 -0
- package/src/modules/search/i18n/de.json +61 -0
- package/src/modules/search/i18n/en.json +72 -0
- package/src/modules/search/i18n/es.json +61 -0
- package/src/modules/search/i18n/pl.json +61 -0
- package/src/modules/search/index.ts +9 -0
- package/src/modules/search/lib/auto-indexing.ts +35 -0
- package/src/modules/search/lib/embedding-config.ts +161 -0
- package/src/modules/search/lib/global-search-config.ts +69 -0
- package/src/modules/search/lib/reindex-lock.ts +201 -0
- package/src/modules/search/subscribers/fulltext_upsert.ts +83 -0
- package/src/modules/search/subscribers/vector_delete.ts +75 -0
- package/src/modules/search/subscribers/vector_purge.ts +161 -0
- package/src/modules/search/subscribers/vector_upsert.ts +75 -0
- package/src/modules/search/workers/fulltext-index.worker.ts +318 -0
- package/src/modules/search/workers/vector-index.worker.ts +292 -0
- package/src/queue/fulltext-indexing.ts +87 -0
- package/src/queue/index.ts +2 -0
- package/src/queue/vector-indexing.ts +66 -0
- package/src/service.ts +397 -0
- package/src/strategies/fulltext.strategy.ts +155 -0
- package/src/strategies/index.ts +17 -0
- package/src/strategies/token.strategy.ts +153 -0
- package/src/strategies/vector.strategy.ts +234 -0
- package/src/types.ts +38 -0
- package/src/vector/drivers/chromadb/index.ts +49 -0
- package/src/vector/drivers/index.ts +4 -0
- package/src/vector/drivers/pgvector/index.ts +627 -0
- package/src/vector/drivers/qdrant/index.ts +49 -0
- package/src/vector/index.ts +3 -0
- package/src/vector/lib/vector-logs.ts +46 -0
- package/src/vector/services/checksum.ts +18 -0
- package/src/vector/services/embedding.ts +275 -0
- package/src/vector/services/index.ts +3 -0
- package/src/vector/services/vector-index.service.ts +1234 -0
- package/src/vector/types/pg.d.ts +1 -0
- package/src/vector/types.ts +220 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'
|
|
2
|
+
import { FULLTEXT_INDEXING_QUEUE_NAME, type FulltextIndexJobPayload } from '../../../queue/fulltext-indexing'
|
|
3
|
+
import type { FullTextSearchStrategy } from '../../../strategies/fulltext.strategy'
|
|
4
|
+
import type { SearchIndexer } from '../../../indexer/search-indexer'
|
|
5
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
6
|
+
import type { Knex } from 'knex'
|
|
7
|
+
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
8
|
+
import { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'
|
|
9
|
+
import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
|
|
10
|
+
import { searchDebug, searchDebugWarn, searchError } from '../../../lib/debug'
|
|
11
|
+
import { updateReindexProgress } from '../lib/reindex-lock'
|
|
12
|
+
|
|
13
|
+
// Worker metadata for auto-discovery
|
|
14
|
+
const DEFAULT_CONCURRENCY = 2
|
|
15
|
+
const envConcurrency = process.env.WORKERS_FULLTEXT_INDEXING_CONCURRENCY
|
|
16
|
+
|
|
17
|
+
export const metadata: WorkerMeta = {
|
|
18
|
+
queue: FULLTEXT_INDEXING_QUEUE_NAME,
|
|
19
|
+
concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type HandlerContext = { resolve: <T = unknown>(name: string) => T }
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Process a fulltext indexing job.
|
|
26
|
+
*
|
|
27
|
+
* This handler processes single record indexing, batch indexing, deletion, and purge
|
|
28
|
+
* operations for the fulltext search strategy.
|
|
29
|
+
*
|
|
30
|
+
* All indexing operations (single and batch) use searchIndexer.indexRecordById() to load
|
|
31
|
+
* fresh data, ensuring consistency with the vector worker pattern.
|
|
32
|
+
*
|
|
33
|
+
* @param job - The queued job containing payload
|
|
34
|
+
* @param jobCtx - Queue job context with job ID and attempt info
|
|
35
|
+
* @param ctx - DI container context for resolving services
|
|
36
|
+
*/
|
|
37
|
+
export async function handleFulltextIndexJob(
|
|
38
|
+
job: QueuedJob<FulltextIndexJobPayload>,
|
|
39
|
+
jobCtx: JobContext,
|
|
40
|
+
ctx: HandlerContext,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const { jobType, tenantId } = job.payload
|
|
43
|
+
|
|
44
|
+
if (!tenantId) {
|
|
45
|
+
searchDebugWarn('fulltext-index.worker', 'Skipping job with missing tenantId', {
|
|
46
|
+
jobId: jobCtx.jobId,
|
|
47
|
+
jobType,
|
|
48
|
+
})
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Resolve EntityManager for logging and knex for database queries
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
let em: any | null = null
|
|
55
|
+
let knex: Knex | null = null
|
|
56
|
+
try {
|
|
57
|
+
em = ctx.resolve('em') as EntityManager
|
|
58
|
+
knex = (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()
|
|
59
|
+
} catch {
|
|
60
|
+
em = null
|
|
61
|
+
knex = null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Resolve searchIndexer for loading fresh data
|
|
65
|
+
let searchIndexer: SearchIndexer | undefined
|
|
66
|
+
try {
|
|
67
|
+
searchIndexer = ctx.resolve<SearchIndexer>('searchIndexer')
|
|
68
|
+
} catch {
|
|
69
|
+
searchDebugWarn('fulltext-index.worker', 'searchIndexer not available')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Resolve fulltext strategy
|
|
73
|
+
let fulltextStrategy: FullTextSearchStrategy | undefined
|
|
74
|
+
try {
|
|
75
|
+
const searchStrategies = ctx.resolve<unknown[]>('searchStrategies')
|
|
76
|
+
fulltextStrategy = searchStrategies?.find(
|
|
77
|
+
(s: unknown) => (s as { id?: string })?.id === 'fulltext',
|
|
78
|
+
) as FullTextSearchStrategy | undefined
|
|
79
|
+
} catch {
|
|
80
|
+
searchDebugWarn('fulltext-index.worker', 'searchStrategies not available')
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!fulltextStrategy) {
|
|
85
|
+
searchDebugWarn('fulltext-index.worker', 'Fulltext strategy not configured')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if fulltext is available
|
|
90
|
+
const isAvailable = await fulltextStrategy.isAvailable()
|
|
91
|
+
if (!isAvailable) {
|
|
92
|
+
throw new Error('Fulltext search is not available') // Will trigger retry
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// ========== SINGLE INDEX: Use searchIndexer.indexRecordById() for fresh data ==========
|
|
97
|
+
if (jobType === 'index') {
|
|
98
|
+
const { entityType, recordId, organizationId } = job.payload as {
|
|
99
|
+
entityType: string
|
|
100
|
+
recordId: string
|
|
101
|
+
organizationId?: string | null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!entityType || !recordId) {
|
|
105
|
+
searchDebugWarn('fulltext-index.worker', 'Skipping index with missing fields', {
|
|
106
|
+
jobId: jobCtx.jobId,
|
|
107
|
+
entityType,
|
|
108
|
+
recordId,
|
|
109
|
+
})
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!searchIndexer) {
|
|
114
|
+
throw new Error('searchIndexer not available for single-record index')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = await searchIndexer.indexRecordById({
|
|
118
|
+
entityId: entityType as EntityId,
|
|
119
|
+
recordId,
|
|
120
|
+
tenantId,
|
|
121
|
+
organizationId,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
searchDebug('fulltext-index.worker', 'Indexed single record to fulltext', {
|
|
125
|
+
jobId: jobCtx.jobId,
|
|
126
|
+
tenantId,
|
|
127
|
+
entityType,
|
|
128
|
+
recordId,
|
|
129
|
+
action: result.action,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await recordIndexerLog(
|
|
133
|
+
{ em: em ?? undefined },
|
|
134
|
+
{
|
|
135
|
+
source: 'fulltext',
|
|
136
|
+
handler: 'worker:fulltext:index',
|
|
137
|
+
message: `Indexed record to fulltext (${result.action})`,
|
|
138
|
+
entityType,
|
|
139
|
+
recordId,
|
|
140
|
+
tenantId,
|
|
141
|
+
details: { jobId: jobCtx.jobId },
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ========== BATCH-INDEX: Use searchIndexer.indexRecordById() for fresh data ==========
|
|
148
|
+
if (jobType === 'batch-index') {
|
|
149
|
+
const { records, organizationId } = job.payload
|
|
150
|
+
if (!records || records.length === 0) {
|
|
151
|
+
searchDebugWarn('fulltext-index.worker', 'Skipping batch-index with no records', {
|
|
152
|
+
jobId: jobCtx.jobId,
|
|
153
|
+
})
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!searchIndexer) {
|
|
158
|
+
throw new Error('searchIndexer not available for batch indexing')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Process each record using indexRecordById (same pattern as vector worker)
|
|
162
|
+
let successCount = 0
|
|
163
|
+
let failCount = 0
|
|
164
|
+
|
|
165
|
+
for (const { entityId, recordId } of records) {
|
|
166
|
+
try {
|
|
167
|
+
const result = await searchIndexer.indexRecordById({
|
|
168
|
+
entityId: entityId as EntityId,
|
|
169
|
+
recordId,
|
|
170
|
+
tenantId,
|
|
171
|
+
organizationId,
|
|
172
|
+
})
|
|
173
|
+
if (result.action === 'indexed') {
|
|
174
|
+
successCount++
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
failCount++
|
|
178
|
+
searchDebugWarn('fulltext-index.worker', 'Failed to index record in batch', {
|
|
179
|
+
entityId,
|
|
180
|
+
recordId,
|
|
181
|
+
error: error instanceof Error ? error.message : error,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Update heartbeat to signal worker is still processing
|
|
187
|
+
if (knex && successCount > 0) {
|
|
188
|
+
await updateReindexProgress(knex, tenantId, 'fulltext', successCount, organizationId ?? null)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
searchDebug('fulltext-index.worker', 'Batch indexed to fulltext', {
|
|
192
|
+
jobId: jobCtx.jobId,
|
|
193
|
+
tenantId,
|
|
194
|
+
requestedCount: records.length,
|
|
195
|
+
successCount,
|
|
196
|
+
failCount,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
await recordIndexerLog(
|
|
200
|
+
{ em: em ?? undefined },
|
|
201
|
+
{
|
|
202
|
+
source: 'fulltext',
|
|
203
|
+
handler: 'worker:fulltext:batch-index',
|
|
204
|
+
message: `Indexed ${successCount}/${records.length} records to fulltext`,
|
|
205
|
+
tenantId,
|
|
206
|
+
details: { jobId: jobCtx.jobId, requestedCount: records.length, successCount, failCount },
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ========== DELETE ==========
|
|
213
|
+
if (jobType === 'delete') {
|
|
214
|
+
const { entityId, recordId } = job.payload
|
|
215
|
+
if (!entityId || !recordId) {
|
|
216
|
+
searchDebugWarn('fulltext-index.worker', 'Skipping delete with missing fields', {
|
|
217
|
+
jobId: jobCtx.jobId,
|
|
218
|
+
entityId,
|
|
219
|
+
recordId,
|
|
220
|
+
})
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await fulltextStrategy.delete(entityId, recordId, tenantId)
|
|
225
|
+
|
|
226
|
+
searchDebug('fulltext-index.worker', 'Deleted from fulltext', {
|
|
227
|
+
jobId: jobCtx.jobId,
|
|
228
|
+
tenantId,
|
|
229
|
+
entityId,
|
|
230
|
+
recordId,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
await recordIndexerLog(
|
|
234
|
+
{ em: em ?? undefined },
|
|
235
|
+
{
|
|
236
|
+
source: 'fulltext',
|
|
237
|
+
handler: 'worker:fulltext:delete',
|
|
238
|
+
message: `Deleted record from fulltext`,
|
|
239
|
+
entityType: entityId,
|
|
240
|
+
recordId,
|
|
241
|
+
tenantId,
|
|
242
|
+
details: { jobId: jobCtx.jobId },
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ========== PURGE ==========
|
|
249
|
+
if (jobType === 'purge') {
|
|
250
|
+
const { entityId } = job.payload
|
|
251
|
+
if (!entityId) {
|
|
252
|
+
searchDebugWarn('fulltext-index.worker', 'Skipping purge with missing entityId', {
|
|
253
|
+
jobId: jobCtx.jobId,
|
|
254
|
+
})
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await fulltextStrategy.purge(entityId, tenantId)
|
|
259
|
+
|
|
260
|
+
searchDebug('fulltext-index.worker', 'Purged entity from fulltext', {
|
|
261
|
+
jobId: jobCtx.jobId,
|
|
262
|
+
tenantId,
|
|
263
|
+
entityId,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
await recordIndexerLog(
|
|
267
|
+
{ em: em ?? undefined },
|
|
268
|
+
{
|
|
269
|
+
source: 'fulltext',
|
|
270
|
+
handler: 'worker:fulltext:purge',
|
|
271
|
+
message: `Purged entity from fulltext`,
|
|
272
|
+
entityType: entityId,
|
|
273
|
+
tenantId,
|
|
274
|
+
details: { jobId: jobCtx.jobId },
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
searchError('fulltext-index.worker', `Failed to ${jobType}`, {
|
|
281
|
+
jobId: jobCtx.jobId,
|
|
282
|
+
tenantId,
|
|
283
|
+
error: error instanceof Error ? error.message : error,
|
|
284
|
+
attemptNumber: jobCtx.attemptNumber,
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const entityId = 'entityId' in job.payload ? job.payload.entityId :
|
|
288
|
+
'entityType' in job.payload ? (job.payload as { entityType?: string }).entityType : undefined
|
|
289
|
+
const recordId = 'recordId' in job.payload ? job.payload.recordId : undefined
|
|
290
|
+
|
|
291
|
+
await recordIndexerError(
|
|
292
|
+
{ em: em ?? undefined },
|
|
293
|
+
{
|
|
294
|
+
source: 'fulltext',
|
|
295
|
+
handler: `worker:fulltext:${jobType}`,
|
|
296
|
+
error,
|
|
297
|
+
entityType: entityId,
|
|
298
|
+
recordId,
|
|
299
|
+
tenantId,
|
|
300
|
+
payload: job.payload,
|
|
301
|
+
},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
// Re-throw to let the queue handle retry logic
|
|
305
|
+
throw error
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Default export for worker auto-discovery.
|
|
311
|
+
* Wraps handleFulltextIndexJob to match the expected handler signature.
|
|
312
|
+
*/
|
|
313
|
+
export default async function handle(
|
|
314
|
+
job: QueuedJob<FulltextIndexJobPayload>,
|
|
315
|
+
ctx: JobContext & HandlerContext
|
|
316
|
+
): Promise<void> {
|
|
317
|
+
return handleFulltextIndexJob(job, ctx, ctx)
|
|
318
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'
|
|
2
|
+
import { VECTOR_INDEXING_QUEUE_NAME, type VectorIndexJobPayload } from '../../../queue/vector-indexing'
|
|
3
|
+
import type { SearchIndexer } from '../../../indexer/search-indexer'
|
|
4
|
+
import type { EmbeddingService } from '../../../vector'
|
|
5
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
6
|
+
import type { Knex } from 'knex'
|
|
7
|
+
import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
|
|
8
|
+
import { applyCoverageAdjustments, createCoverageAdjustments } from '@open-mercato/core/modules/query_index/lib/coverage'
|
|
9
|
+
import { logVectorOperation } from '../../../vector/lib/vector-logs'
|
|
10
|
+
import { resolveAutoIndexingEnabled } from '../lib/auto-indexing'
|
|
11
|
+
import { resolveEmbeddingConfig } from '../lib/embedding-config'
|
|
12
|
+
import { searchDebugWarn } from '../../../lib/debug'
|
|
13
|
+
import { updateReindexProgress } from '../lib/reindex-lock'
|
|
14
|
+
|
|
15
|
+
// Worker metadata for auto-discovery
|
|
16
|
+
const DEFAULT_CONCURRENCY = 2
|
|
17
|
+
const envConcurrency = process.env.WORKERS_VECTOR_INDEXING_CONCURRENCY
|
|
18
|
+
|
|
19
|
+
export const metadata: WorkerMeta = {
|
|
20
|
+
queue: VECTOR_INDEXING_QUEUE_NAME,
|
|
21
|
+
concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type HandlerContext = { resolve: <T = unknown>(name: string) => T }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Process a vector index job.
|
|
28
|
+
*
|
|
29
|
+
* This handler is called by the queue worker to process indexing and deletion jobs.
|
|
30
|
+
* It uses SearchIndexer to load records and index them via SearchService.
|
|
31
|
+
*
|
|
32
|
+
* @param job - The queued job containing payload
|
|
33
|
+
* @param jobCtx - Queue job context with job ID and attempt info
|
|
34
|
+
* @param ctx - DI container context for resolving services
|
|
35
|
+
*/
|
|
36
|
+
export async function handleVectorIndexJob(
|
|
37
|
+
job: QueuedJob<VectorIndexJobPayload>,
|
|
38
|
+
jobCtx: JobContext,
|
|
39
|
+
ctx: HandlerContext,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const { jobType, entityType, recordId, tenantId, organizationId, records } = job.payload
|
|
42
|
+
|
|
43
|
+
// Handle batch-index jobs (from reindex operations)
|
|
44
|
+
if (jobType === 'batch-index') {
|
|
45
|
+
if (!records?.length || !tenantId) {
|
|
46
|
+
searchDebugWarn('vector-index.worker', 'Skipping batch-index job with missing required fields', {
|
|
47
|
+
jobId: jobCtx.jobId,
|
|
48
|
+
recordCount: records?.length ?? 0,
|
|
49
|
+
tenantId,
|
|
50
|
+
})
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let searchIndexer: SearchIndexer
|
|
55
|
+
try {
|
|
56
|
+
searchIndexer = ctx.resolve<SearchIndexer>('searchIndexer')
|
|
57
|
+
} catch {
|
|
58
|
+
searchDebugWarn('vector-index.worker', 'searchIndexer not available')
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Get knex for heartbeat updates
|
|
63
|
+
let knex: Knex | null = null
|
|
64
|
+
try {
|
|
65
|
+
const em = ctx.resolve('em') as EntityManager
|
|
66
|
+
knex = (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()
|
|
67
|
+
} catch {
|
|
68
|
+
knex = null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Load saved embedding config to use the correct provider/model
|
|
72
|
+
try {
|
|
73
|
+
const embeddingConfig = await resolveEmbeddingConfig(ctx, { defaultValue: null })
|
|
74
|
+
if (embeddingConfig) {
|
|
75
|
+
const embeddingService = ctx.resolve<EmbeddingService>('vectorEmbeddingService')
|
|
76
|
+
embeddingService.updateConfig(embeddingConfig)
|
|
77
|
+
}
|
|
78
|
+
} catch (configErr) {
|
|
79
|
+
searchDebugWarn('vector-index.worker', 'Failed to load embedding config for batch, using defaults', {
|
|
80
|
+
error: configErr instanceof Error ? configErr.message : configErr,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Process each record in the batch
|
|
85
|
+
let successCount = 0
|
|
86
|
+
let failCount = 0
|
|
87
|
+
for (const { entityId, recordId: recId } of records) {
|
|
88
|
+
try {
|
|
89
|
+
const result = await searchIndexer.indexRecordById({
|
|
90
|
+
entityId: entityId as Parameters<typeof searchIndexer.indexRecordById>[0]['entityId'],
|
|
91
|
+
recordId: recId,
|
|
92
|
+
tenantId,
|
|
93
|
+
organizationId,
|
|
94
|
+
})
|
|
95
|
+
if (result.action === 'indexed') {
|
|
96
|
+
successCount++
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
failCount++
|
|
100
|
+
searchDebugWarn('vector-index.worker', 'Failed to index record in batch', {
|
|
101
|
+
entityId,
|
|
102
|
+
recordId: recId,
|
|
103
|
+
error: error instanceof Error ? error.message : error,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Update heartbeat to signal worker is still processing
|
|
109
|
+
if (knex && successCount > 0) {
|
|
110
|
+
await updateReindexProgress(knex, tenantId, 'vector', successCount, organizationId ?? null)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
searchDebugWarn('vector-index.worker', 'Batch-index job completed', {
|
|
114
|
+
jobId: jobCtx.jobId,
|
|
115
|
+
totalRecords: records.length,
|
|
116
|
+
successCount,
|
|
117
|
+
failCount,
|
|
118
|
+
})
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle single record jobs (index/delete)
|
|
123
|
+
if (!entityType || !recordId || !tenantId) {
|
|
124
|
+
searchDebugWarn('vector-index.worker', 'Skipping job with missing required fields', {
|
|
125
|
+
jobId: jobCtx.jobId,
|
|
126
|
+
entityType,
|
|
127
|
+
recordId,
|
|
128
|
+
tenantId,
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })
|
|
134
|
+
if (!autoIndexingEnabled) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let searchIndexer: SearchIndexer
|
|
139
|
+
try {
|
|
140
|
+
searchIndexer = ctx.resolve<SearchIndexer>('searchIndexer')
|
|
141
|
+
} catch {
|
|
142
|
+
searchDebugWarn('vector-index.worker', 'searchIndexer not available')
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Load saved embedding config to use the correct provider/model
|
|
147
|
+
try {
|
|
148
|
+
const embeddingConfig = await resolveEmbeddingConfig(ctx, { defaultValue: null })
|
|
149
|
+
if (embeddingConfig) {
|
|
150
|
+
const embeddingService = ctx.resolve<EmbeddingService>('vectorEmbeddingService')
|
|
151
|
+
embeddingService.updateConfig(embeddingConfig)
|
|
152
|
+
}
|
|
153
|
+
} catch (configErr) {
|
|
154
|
+
// Delete operations don't require embedding, only warn for index operations
|
|
155
|
+
if (jobType === 'index') {
|
|
156
|
+
searchDebugWarn('vector-index.worker', 'Failed to load embedding config, using defaults', {
|
|
157
|
+
error: configErr instanceof Error ? configErr.message : configErr,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
|
+
let em: any | null = null
|
|
164
|
+
try {
|
|
165
|
+
em = ctx.resolve('em')
|
|
166
|
+
} catch {
|
|
167
|
+
em = null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let eventBus: { emitEvent(event: string, payload: unknown, options?: unknown): Promise<void> } | null = null
|
|
171
|
+
try {
|
|
172
|
+
eventBus = ctx.resolve('eventBus')
|
|
173
|
+
} catch {
|
|
174
|
+
eventBus = null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const handlerName = jobType === 'delete'
|
|
178
|
+
? 'worker:vector-indexing:delete'
|
|
179
|
+
: 'worker:vector-indexing:index'
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
let action: 'indexed' | 'deleted' | 'skipped' = 'skipped'
|
|
183
|
+
let delta = 0
|
|
184
|
+
|
|
185
|
+
if (jobType === 'delete') {
|
|
186
|
+
await searchIndexer.deleteRecord({
|
|
187
|
+
entityId: entityType,
|
|
188
|
+
recordId,
|
|
189
|
+
tenantId,
|
|
190
|
+
})
|
|
191
|
+
action = 'deleted'
|
|
192
|
+
delta = -1
|
|
193
|
+
} else {
|
|
194
|
+
const result = await searchIndexer.indexRecordById({
|
|
195
|
+
entityId: entityType,
|
|
196
|
+
recordId,
|
|
197
|
+
tenantId,
|
|
198
|
+
organizationId,
|
|
199
|
+
})
|
|
200
|
+
action = result.action
|
|
201
|
+
if (result.action === 'indexed') {
|
|
202
|
+
delta = 1
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (delta !== 0) {
|
|
207
|
+
let adjustmentsApplied = false
|
|
208
|
+
if (em) {
|
|
209
|
+
try {
|
|
210
|
+
const adjustments = createCoverageAdjustments({
|
|
211
|
+
entityType,
|
|
212
|
+
tenantId,
|
|
213
|
+
organizationId,
|
|
214
|
+
baseDelta: 0,
|
|
215
|
+
indexDelta: 0,
|
|
216
|
+
vectorDelta: delta,
|
|
217
|
+
})
|
|
218
|
+
if (adjustments.length) {
|
|
219
|
+
await applyCoverageAdjustments(em, adjustments)
|
|
220
|
+
adjustmentsApplied = true
|
|
221
|
+
}
|
|
222
|
+
} catch (coverageError) {
|
|
223
|
+
searchDebugWarn('vector-index.worker', 'Failed to adjust vector coverage', {
|
|
224
|
+
error: coverageError instanceof Error ? coverageError.message : coverageError,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!adjustmentsApplied && eventBus) {
|
|
230
|
+
try {
|
|
231
|
+
await eventBus.emitEvent('query_index.coverage.refresh', {
|
|
232
|
+
entityType,
|
|
233
|
+
tenantId,
|
|
234
|
+
organizationId,
|
|
235
|
+
withDeleted: false,
|
|
236
|
+
delayMs: 1000,
|
|
237
|
+
})
|
|
238
|
+
} catch (emitError) {
|
|
239
|
+
searchDebugWarn('vector-index.worker', 'Failed to enqueue coverage refresh', {
|
|
240
|
+
error: emitError instanceof Error ? emitError.message : emitError,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await logVectorOperation({
|
|
247
|
+
em,
|
|
248
|
+
handler: handlerName,
|
|
249
|
+
entityType,
|
|
250
|
+
recordId,
|
|
251
|
+
result: {
|
|
252
|
+
action,
|
|
253
|
+
tenantId,
|
|
254
|
+
organizationId: organizationId ?? null,
|
|
255
|
+
created: action === 'indexed',
|
|
256
|
+
existed: action === 'deleted',
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
} catch (error) {
|
|
260
|
+
searchDebugWarn('vector-index.worker', `Failed to ${jobType} vector index`, {
|
|
261
|
+
entityType,
|
|
262
|
+
recordId,
|
|
263
|
+
error: error instanceof Error ? error.message : error,
|
|
264
|
+
})
|
|
265
|
+
await recordIndexerError(
|
|
266
|
+
{ em: em ?? undefined },
|
|
267
|
+
{
|
|
268
|
+
source: 'vector',
|
|
269
|
+
handler: handlerName,
|
|
270
|
+
error,
|
|
271
|
+
entityType,
|
|
272
|
+
recordId,
|
|
273
|
+
tenantId,
|
|
274
|
+
organizationId,
|
|
275
|
+
payload: job.payload,
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
// Re-throw to let the queue handle retry logic
|
|
279
|
+
throw error
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Default export for worker auto-discovery.
|
|
285
|
+
* Wraps handleVectorIndexJob to match the expected handler signature.
|
|
286
|
+
*/
|
|
287
|
+
export default async function handle(
|
|
288
|
+
job: QueuedJob<VectorIndexJobPayload>,
|
|
289
|
+
ctx: JobContext & HandlerContext
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
return handleVectorIndexJob(job, ctx, ctx)
|
|
292
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createQueue, type Queue } from '@open-mercato/queue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Job types for fulltext indexing queue.
|
|
5
|
+
*/
|
|
6
|
+
export type FulltextIndexJobType = 'index' | 'batch-index' | 'delete' | 'purge'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Minimal record reference for batch indexing.
|
|
10
|
+
* Only contains identifiers - actual data is loaded fresh via searchIndexer.indexRecordById().
|
|
11
|
+
* This keeps queue payloads small and ensures fresh data is indexed.
|
|
12
|
+
*/
|
|
13
|
+
export type FulltextBatchRecord = {
|
|
14
|
+
entityId: string
|
|
15
|
+
recordId: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Payload for single record indexing jobs.
|
|
20
|
+
* Worker loads fresh data via searchIndexer.indexRecordById().
|
|
21
|
+
*/
|
|
22
|
+
export type FulltextIndexPayload = {
|
|
23
|
+
jobType: 'index'
|
|
24
|
+
tenantId: string
|
|
25
|
+
organizationId?: string | null
|
|
26
|
+
entityType: string
|
|
27
|
+
recordId: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Payload for batch indexing jobs.
|
|
32
|
+
* Worker loads fresh data via searchIndexer.indexRecordById() for each record.
|
|
33
|
+
*/
|
|
34
|
+
export type FulltextBatchIndexPayload = {
|
|
35
|
+
jobType: 'batch-index'
|
|
36
|
+
tenantId: string
|
|
37
|
+
organizationId?: string | null
|
|
38
|
+
records: FulltextBatchRecord[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Payload for delete jobs.
|
|
43
|
+
*/
|
|
44
|
+
export type FulltextDeletePayload = {
|
|
45
|
+
jobType: 'delete'
|
|
46
|
+
tenantId: string
|
|
47
|
+
entityId: string
|
|
48
|
+
recordId: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Payload for purge jobs (delete all records of an entity type).
|
|
53
|
+
*/
|
|
54
|
+
export type FulltextPurgePayload = {
|
|
55
|
+
jobType: 'purge'
|
|
56
|
+
tenantId: string
|
|
57
|
+
entityId: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Union type for all fulltext indexing job payloads.
|
|
62
|
+
*/
|
|
63
|
+
export type FulltextIndexJobPayload =
|
|
64
|
+
| FulltextIndexPayload
|
|
65
|
+
| FulltextBatchIndexPayload
|
|
66
|
+
| FulltextDeletePayload
|
|
67
|
+
| FulltextPurgePayload
|
|
68
|
+
|
|
69
|
+
export const FULLTEXT_INDEXING_QUEUE_NAME = 'fulltext-indexing'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a fulltext indexing queue.
|
|
73
|
+
*
|
|
74
|
+
* @param strategy - Queue strategy ('local' for development, 'async' for production with Redis)
|
|
75
|
+
* @param options - Optional connection configuration for async strategy
|
|
76
|
+
*/
|
|
77
|
+
export function createFulltextIndexingQueue(
|
|
78
|
+
strategy: 'local' | 'async' = 'local',
|
|
79
|
+
options?: { connection?: { url?: string; host?: string; port?: number } },
|
|
80
|
+
): Queue<FulltextIndexJobPayload> {
|
|
81
|
+
if (strategy === 'async') {
|
|
82
|
+
return createQueue<FulltextIndexJobPayload>(FULLTEXT_INDEXING_QUEUE_NAME, 'async', {
|
|
83
|
+
connection: options?.connection,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
return createQueue<FulltextIndexJobPayload>(FULLTEXT_INDEXING_QUEUE_NAME, 'local')
|
|
87
|
+
}
|