@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,201 @@
|
|
|
1
|
+
import type { Knex } from 'knex'
|
|
2
|
+
import {
|
|
3
|
+
prepareJob,
|
|
4
|
+
updateJobProgress,
|
|
5
|
+
finalizeJob,
|
|
6
|
+
type JobScope,
|
|
7
|
+
} from '@open-mercato/core/modules/query_index/lib/jobs'
|
|
8
|
+
|
|
9
|
+
export const REINDEX_LOCK_KEY = 'reindex_lock'
|
|
10
|
+
|
|
11
|
+
export type ReindexLockType = 'fulltext' | 'vector'
|
|
12
|
+
|
|
13
|
+
// Entity type mapping for search reindex jobs
|
|
14
|
+
const LOCK_ENTITY_TYPES: Record<ReindexLockType, string> = {
|
|
15
|
+
fulltext: 'search:reindex:fulltext',
|
|
16
|
+
vector: 'search:reindex:vector',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Heartbeat staleness threshold (30 seconds)
|
|
20
|
+
const HEARTBEAT_STALE_MS = 30 * 1000
|
|
21
|
+
|
|
22
|
+
export type ReindexLockStatus = {
|
|
23
|
+
type: ReindexLockType
|
|
24
|
+
action: string
|
|
25
|
+
startedAt: string
|
|
26
|
+
tenantId: string
|
|
27
|
+
organizationId?: string | null
|
|
28
|
+
processedCount?: number | null
|
|
29
|
+
totalCount?: number | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildScope(
|
|
33
|
+
type: ReindexLockType,
|
|
34
|
+
tenantId: string,
|
|
35
|
+
organizationId?: string | null,
|
|
36
|
+
): JobScope {
|
|
37
|
+
return {
|
|
38
|
+
entityType: LOCK_ENTITY_TYPES[type],
|
|
39
|
+
tenantId,
|
|
40
|
+
organizationId: organizationId ?? null,
|
|
41
|
+
partitionIndex: null,
|
|
42
|
+
partitionCount: null,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a reindex operation is currently in progress for a specific type.
|
|
48
|
+
* Returns the lock status if active, null if no lock or lock is stale.
|
|
49
|
+
*
|
|
50
|
+
* Automatically cleans up stale locks (heartbeat older than 60 seconds).
|
|
51
|
+
*/
|
|
52
|
+
export async function getReindexLockStatus(
|
|
53
|
+
knex: Knex,
|
|
54
|
+
tenantId: string,
|
|
55
|
+
options?: { type?: ReindexLockType },
|
|
56
|
+
): Promise<ReindexLockStatus | null> {
|
|
57
|
+
const typesToCheck: ReindexLockType[] = options?.type
|
|
58
|
+
? [options.type]
|
|
59
|
+
: ['fulltext', 'vector']
|
|
60
|
+
|
|
61
|
+
for (const lockType of typesToCheck) {
|
|
62
|
+
const entityType = LOCK_ENTITY_TYPES[lockType]
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const job = await knex('entity_index_jobs')
|
|
66
|
+
.where('entity_type', entityType)
|
|
67
|
+
.whereRaw('tenant_id is not distinct from ?', [tenantId])
|
|
68
|
+
.whereNull('finished_at')
|
|
69
|
+
.first()
|
|
70
|
+
|
|
71
|
+
if (!job) continue
|
|
72
|
+
|
|
73
|
+
// Check heartbeat staleness
|
|
74
|
+
const heartbeatAt = job.heartbeat_at
|
|
75
|
+
? new Date(job.heartbeat_at).getTime()
|
|
76
|
+
: 0
|
|
77
|
+
const elapsed = Date.now() - heartbeatAt
|
|
78
|
+
|
|
79
|
+
if (elapsed > HEARTBEAT_STALE_MS) {
|
|
80
|
+
// Auto-cleanup stale lock
|
|
81
|
+
await knex('entity_index_jobs')
|
|
82
|
+
.where('id', job.id)
|
|
83
|
+
.update({ finished_at: knex.fn.now() })
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// started_at comes as string from knex, convert if needed
|
|
88
|
+
const startedAtStr = job.started_at
|
|
89
|
+
? (typeof job.started_at === 'string' ? job.started_at : new Date(job.started_at).toISOString())
|
|
90
|
+
: new Date().toISOString()
|
|
91
|
+
|
|
92
|
+
const result = {
|
|
93
|
+
type: lockType,
|
|
94
|
+
action: job.status || 'reindexing',
|
|
95
|
+
startedAt: startedAtStr,
|
|
96
|
+
tenantId,
|
|
97
|
+
organizationId: job.organization_id,
|
|
98
|
+
processedCount: job.processed_count,
|
|
99
|
+
totalCount: job.total_count,
|
|
100
|
+
}
|
|
101
|
+
return result
|
|
102
|
+
} catch {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Acquire a reindex lock for a specific type. Returns whether lock was acquired.
|
|
112
|
+
* Fulltext and vector locks are independent - they don't block each other.
|
|
113
|
+
*/
|
|
114
|
+
export async function acquireReindexLock(
|
|
115
|
+
knex: Knex,
|
|
116
|
+
options: {
|
|
117
|
+
type: ReindexLockType
|
|
118
|
+
action: string
|
|
119
|
+
tenantId: string
|
|
120
|
+
organizationId?: string | null
|
|
121
|
+
totalCount?: number | null
|
|
122
|
+
},
|
|
123
|
+
): Promise<{ acquired: boolean; jobId?: string }> {
|
|
124
|
+
// Check existing active lock
|
|
125
|
+
const existing = await getReindexLockStatus(knex, options.tenantId, {
|
|
126
|
+
type: options.type,
|
|
127
|
+
})
|
|
128
|
+
if (existing) {
|
|
129
|
+
return { acquired: false }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const scope = buildScope(
|
|
134
|
+
options.type,
|
|
135
|
+
options.tenantId,
|
|
136
|
+
options.organizationId,
|
|
137
|
+
)
|
|
138
|
+
const jobId = await prepareJob(knex, scope, 'reindexing', {
|
|
139
|
+
totalCount: options.totalCount,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return { acquired: true, jobId: jobId ?? undefined }
|
|
143
|
+
} catch {
|
|
144
|
+
return { acquired: false }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Release the reindex lock for a specific type.
|
|
150
|
+
*/
|
|
151
|
+
export async function clearReindexLock(
|
|
152
|
+
knex: Knex,
|
|
153
|
+
tenantId: string,
|
|
154
|
+
type: ReindexLockType,
|
|
155
|
+
organizationId?: string | null,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
try {
|
|
158
|
+
const scope = buildScope(type, tenantId, organizationId)
|
|
159
|
+
await finalizeJob(knex, scope)
|
|
160
|
+
} catch {
|
|
161
|
+
// Ignore errors when clearing lock
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Update the reindex progress and refresh the heartbeat.
|
|
167
|
+
* Call this periodically during batch processing to prevent stale lock detection.
|
|
168
|
+
*
|
|
169
|
+
* If no active lock exists (e.g., it expired after queue restart), this will
|
|
170
|
+
* recreate the lock so the reindex button stays disabled while processing.
|
|
171
|
+
*/
|
|
172
|
+
export async function updateReindexProgress(
|
|
173
|
+
knex: Knex,
|
|
174
|
+
tenantId: string,
|
|
175
|
+
type: ReindexLockType,
|
|
176
|
+
processedDelta: number,
|
|
177
|
+
organizationId?: string | null,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
try {
|
|
180
|
+
const scope = buildScope(type, tenantId, organizationId)
|
|
181
|
+
const entityType = LOCK_ENTITY_TYPES[type]
|
|
182
|
+
|
|
183
|
+
// Try to update existing active job first
|
|
184
|
+
const updated = await knex('entity_index_jobs')
|
|
185
|
+
.where('entity_type', entityType)
|
|
186
|
+
.whereRaw('tenant_id is not distinct from ?', [tenantId])
|
|
187
|
+
.whereRaw('organization_id is not distinct from ?', [organizationId ?? null])
|
|
188
|
+
.whereNull('finished_at')
|
|
189
|
+
.update({
|
|
190
|
+
processed_count: knex.raw('coalesce(processed_count, 0) + ?', [Math.max(0, processedDelta)]),
|
|
191
|
+
heartbeat_at: knex.fn.now(),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// If no active lock exists, recreate it
|
|
195
|
+
if (updated === 0) {
|
|
196
|
+
await prepareJob(knex, scope, 'reindexing')
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// Ignore errors when updating progress
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'
|
|
2
|
+
import type { Queue } from '@open-mercato/queue'
|
|
3
|
+
import type { FulltextIndexJobPayload } from '../../../queue/fulltext-indexing'
|
|
4
|
+
import { resolveAutoIndexingEnabled } from '../lib/auto-indexing'
|
|
5
|
+
import { searchDebugWarn, searchError } from '../../../lib/debug'
|
|
6
|
+
|
|
7
|
+
export const metadata = { event: 'search.index_record', persistent: false }
|
|
8
|
+
|
|
9
|
+
type Payload = {
|
|
10
|
+
entityId?: string
|
|
11
|
+
recordId?: string
|
|
12
|
+
organizationId?: string | null
|
|
13
|
+
tenantId?: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type HandlerContext = { resolve: <T = unknown>(name: string) => T }
|
|
17
|
+
|
|
18
|
+
export default async function handle(payload: Payload, ctx: HandlerContext) {
|
|
19
|
+
const entityType = String(payload?.entityId ?? '')
|
|
20
|
+
const recordId = String(payload?.recordId ?? '')
|
|
21
|
+
|
|
22
|
+
if (!entityType || !recordId) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let organizationId = payload?.organizationId ?? null
|
|
27
|
+
let tenantId = payload?.tenantId ?? null
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
let em: any | null = null
|
|
31
|
+
try {
|
|
32
|
+
em = ctx.resolve('em')
|
|
33
|
+
} catch {
|
|
34
|
+
em = null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve missing scope from DB if needed (same pattern as vector_upsert.ts)
|
|
38
|
+
if ((organizationId == null || tenantId == null) && em) {
|
|
39
|
+
try {
|
|
40
|
+
const knex = em.getConnection().getKnex()
|
|
41
|
+
const table = resolveEntityTableName(em, entityType)
|
|
42
|
+
const row = await knex(table).select(['organization_id', 'tenant_id']).where({ id: recordId }).first()
|
|
43
|
+
if (organizationId == null) organizationId = row?.organization_id ?? organizationId
|
|
44
|
+
if (tenantId == null) tenantId = row?.tenant_id ?? tenantId
|
|
45
|
+
} catch {
|
|
46
|
+
// Ignore lookup errors
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!tenantId) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })
|
|
55
|
+
if (!autoIndexingEnabled) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let queue: Queue<FulltextIndexJobPayload>
|
|
60
|
+
try {
|
|
61
|
+
queue = ctx.resolve<Queue<FulltextIndexJobPayload>>('fulltextIndexQueue')
|
|
62
|
+
} catch {
|
|
63
|
+
searchDebugWarn('search.fulltext', 'fulltextIndexQueue not available, skipping fulltext indexing')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await queue.enqueue({
|
|
69
|
+
jobType: 'index',
|
|
70
|
+
entityType,
|
|
71
|
+
recordId,
|
|
72
|
+
tenantId: String(tenantId),
|
|
73
|
+
organizationId: organizationId ? String(organizationId) : null,
|
|
74
|
+
})
|
|
75
|
+
} catch (error) {
|
|
76
|
+
searchError('search.fulltext', 'Failed to enqueue fulltext index job', {
|
|
77
|
+
entityType,
|
|
78
|
+
recordId,
|
|
79
|
+
error: error instanceof Error ? error.message : error,
|
|
80
|
+
})
|
|
81
|
+
throw error
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'
|
|
2
|
+
import type { Queue } from '@open-mercato/queue'
|
|
3
|
+
import type { VectorIndexJobPayload } from '../../../queue/vector-indexing'
|
|
4
|
+
import { resolveAutoIndexingEnabled } from '../lib/auto-indexing'
|
|
5
|
+
import { searchDebugWarn, searchError } from '../../../lib/debug'
|
|
6
|
+
|
|
7
|
+
export const metadata = { event: 'query_index.delete_one', persistent: false }
|
|
8
|
+
|
|
9
|
+
type Payload = {
|
|
10
|
+
entityType?: string
|
|
11
|
+
recordId?: string
|
|
12
|
+
organizationId?: string | null
|
|
13
|
+
tenantId?: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type HandlerContext = { resolve: <T = unknown>(name: string) => T }
|
|
17
|
+
|
|
18
|
+
export default async function handle(payload: Payload, ctx: HandlerContext) {
|
|
19
|
+
const entityType = String(payload?.entityType ?? '')
|
|
20
|
+
const recordId = String(payload?.recordId ?? '')
|
|
21
|
+
if (!entityType || !recordId) return
|
|
22
|
+
|
|
23
|
+
let organizationId = payload?.organizationId ?? null
|
|
24
|
+
let tenantId = payload?.tenantId ?? null
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
let em: any | null = null
|
|
28
|
+
try {
|
|
29
|
+
em = ctx.resolve('em')
|
|
30
|
+
} catch {
|
|
31
|
+
em = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if ((organizationId == null || tenantId == null) && em) {
|
|
35
|
+
try {
|
|
36
|
+
const knex = em.getConnection().getKnex()
|
|
37
|
+
const table = resolveEntityTableName(em, entityType)
|
|
38
|
+
const row = await knex(table).select(['organization_id', 'tenant_id']).where({ id: recordId }).first()
|
|
39
|
+
if (organizationId == null) organizationId = row?.organization_id ?? organizationId
|
|
40
|
+
if (tenantId == null) tenantId = row?.tenant_id ?? tenantId
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore lookup errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!tenantId) return
|
|
47
|
+
|
|
48
|
+
const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })
|
|
49
|
+
if (!autoIndexingEnabled) return
|
|
50
|
+
|
|
51
|
+
let queue: Queue<VectorIndexJobPayload>
|
|
52
|
+
try {
|
|
53
|
+
queue = ctx.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')
|
|
54
|
+
} catch {
|
|
55
|
+
searchDebugWarn('search.vector', 'vectorIndexQueue not available, skipping vector delete')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await queue.enqueue({
|
|
61
|
+
jobType: 'delete',
|
|
62
|
+
entityType,
|
|
63
|
+
recordId,
|
|
64
|
+
tenantId: String(tenantId),
|
|
65
|
+
organizationId: organizationId ? String(organizationId) : null,
|
|
66
|
+
})
|
|
67
|
+
} catch (error) {
|
|
68
|
+
searchError('search.vector', 'Failed to enqueue vector delete job', {
|
|
69
|
+
entityType,
|
|
70
|
+
recordId,
|
|
71
|
+
error: error instanceof Error ? error.message : error,
|
|
72
|
+
})
|
|
73
|
+
throw error // Propagate to caller so failure is visible
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
|
|
2
|
+
import { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'
|
|
3
|
+
import type { SearchIndexer } from '../../../indexer/search-indexer'
|
|
4
|
+
import type { EmbeddingService } from '../../../vector'
|
|
5
|
+
import { writeCoverageCounts } from '@open-mercato/core/modules/query_index/lib/coverage'
|
|
6
|
+
import { resolveEmbeddingConfig } from '../lib/embedding-config'
|
|
7
|
+
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
8
|
+
import { searchDebugWarn } from '../../../lib/debug'
|
|
9
|
+
|
|
10
|
+
export const metadata = { event: 'query_index.vectorize_purge', persistent: false }
|
|
11
|
+
|
|
12
|
+
type Payload = {
|
|
13
|
+
entityType?: string
|
|
14
|
+
tenantId?: string | null
|
|
15
|
+
organizationId?: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
type HandlerContext = { resolve: <T = any>(name: string) => T }
|
|
20
|
+
|
|
21
|
+
export default async function handle(payload: Payload, ctx: HandlerContext) {
|
|
22
|
+
const entityType = String(payload?.entityType ?? '')
|
|
23
|
+
if (!entityType) return
|
|
24
|
+
const tenantIdRaw = payload?.tenantId
|
|
25
|
+
if (tenantIdRaw == null || tenantIdRaw === '') {
|
|
26
|
+
searchDebugWarn('search.vector', 'Skipping vector purge for reindex without tenant scope', { entityType })
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const tenantId = String(tenantIdRaw)
|
|
30
|
+
const organizationId = payload?.organizationId == null ? null : String(payload.organizationId)
|
|
31
|
+
|
|
32
|
+
let searchIndexer: SearchIndexer
|
|
33
|
+
try {
|
|
34
|
+
searchIndexer = ctx.resolve<SearchIndexer>('searchIndexer')
|
|
35
|
+
} catch {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Load saved embedding config for consistency (dimension info may be needed for table recreation)
|
|
40
|
+
try {
|
|
41
|
+
const embeddingConfig = await resolveEmbeddingConfig(ctx, { defaultValue: null })
|
|
42
|
+
if (embeddingConfig) {
|
|
43
|
+
const embeddingService = ctx.resolve<EmbeddingService>('vectorEmbeddingService')
|
|
44
|
+
embeddingService.updateConfig(embeddingConfig)
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Purge operations don't require embedding, ignore config errors
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
let em: any = null
|
|
52
|
+
try {
|
|
53
|
+
em = ctx.resolve('em')
|
|
54
|
+
} catch {
|
|
55
|
+
em = null
|
|
56
|
+
}
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
let eventBus: { emitEvent(event: string, payload: any, options?: any): Promise<void> } | null = null
|
|
59
|
+
try {
|
|
60
|
+
eventBus = ctx.resolve('eventBus')
|
|
61
|
+
} catch {
|
|
62
|
+
eventBus = null
|
|
63
|
+
}
|
|
64
|
+
const scopes = new Set<string>()
|
|
65
|
+
const registerScope = (org: string | null) => {
|
|
66
|
+
const key = org ?? '__null__'
|
|
67
|
+
if (!scopes.has(key)) scopes.add(key)
|
|
68
|
+
}
|
|
69
|
+
registerScope(null)
|
|
70
|
+
if (organizationId != null) registerScope(organizationId)
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await searchIndexer.purgeEntity({
|
|
74
|
+
entityId: entityType as EntityId,
|
|
75
|
+
tenantId,
|
|
76
|
+
})
|
|
77
|
+
if (em) {
|
|
78
|
+
try {
|
|
79
|
+
for (const scope of scopes) {
|
|
80
|
+
const orgValue = scope === '__null__' ? null : scope
|
|
81
|
+
await writeCoverageCounts(
|
|
82
|
+
em,
|
|
83
|
+
{
|
|
84
|
+
entityType,
|
|
85
|
+
tenantId,
|
|
86
|
+
organizationId: orgValue,
|
|
87
|
+
withDeleted: false,
|
|
88
|
+
},
|
|
89
|
+
{ vectorCount: 0 },
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
} catch (coverageError) {
|
|
93
|
+
searchDebugWarn('search.vector', 'Failed to reset vector coverage after purge', {
|
|
94
|
+
error: coverageError instanceof Error ? coverageError.message : coverageError,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (eventBus) {
|
|
99
|
+
await Promise.all(
|
|
100
|
+
Array.from(scopes).map((scope) => {
|
|
101
|
+
const orgValue = scope === '__null__' ? null : scope
|
|
102
|
+
return eventBus!
|
|
103
|
+
.emitEvent(
|
|
104
|
+
'query_index.coverage.refresh',
|
|
105
|
+
{
|
|
106
|
+
entityType,
|
|
107
|
+
tenantId,
|
|
108
|
+
organizationId: orgValue,
|
|
109
|
+
delayMs: 0,
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
.catch(() => undefined)
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
await recordIndexerLog(
|
|
117
|
+
{ em: em ?? undefined },
|
|
118
|
+
{
|
|
119
|
+
source: 'vector',
|
|
120
|
+
handler: 'event:query_index.vectorize_purge',
|
|
121
|
+
message: `Vector purge completed for ${entityType}`,
|
|
122
|
+
entityType,
|
|
123
|
+
tenantId,
|
|
124
|
+
organizationId,
|
|
125
|
+
details: payload,
|
|
126
|
+
},
|
|
127
|
+
).catch(() => undefined)
|
|
128
|
+
} catch (error) {
|
|
129
|
+
searchDebugWarn('search.vector', 'Failed to purge vector index scope', {
|
|
130
|
+
entityType,
|
|
131
|
+
tenantId,
|
|
132
|
+
organizationId,
|
|
133
|
+
error: error instanceof Error ? error.message : error,
|
|
134
|
+
})
|
|
135
|
+
await recordIndexerLog(
|
|
136
|
+
{ em: em ?? undefined },
|
|
137
|
+
{
|
|
138
|
+
source: 'vector',
|
|
139
|
+
handler: 'event:query_index.vectorize_purge',
|
|
140
|
+
level: 'warn',
|
|
141
|
+
message: `Vector purge failed for ${entityType}`,
|
|
142
|
+
entityType,
|
|
143
|
+
tenantId,
|
|
144
|
+
organizationId,
|
|
145
|
+
details: { error: error instanceof Error ? error.message : String(error), payload },
|
|
146
|
+
},
|
|
147
|
+
).catch(() => undefined)
|
|
148
|
+
await recordIndexerError(
|
|
149
|
+
{ em: em ?? undefined },
|
|
150
|
+
{
|
|
151
|
+
source: 'vector',
|
|
152
|
+
handler: 'event:query_index.vectorize_purge',
|
|
153
|
+
error,
|
|
154
|
+
entityType,
|
|
155
|
+
tenantId,
|
|
156
|
+
organizationId,
|
|
157
|
+
payload,
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'
|
|
2
|
+
import type { Queue } from '@open-mercato/queue'
|
|
3
|
+
import type { VectorIndexJobPayload } from '../../../queue/vector-indexing'
|
|
4
|
+
import { resolveAutoIndexingEnabled } from '../lib/auto-indexing'
|
|
5
|
+
import { searchDebugWarn, searchError } from '../../../lib/debug'
|
|
6
|
+
|
|
7
|
+
export const metadata = { event: 'query_index.vectorize_one', persistent: false }
|
|
8
|
+
|
|
9
|
+
type Payload = {
|
|
10
|
+
entityType?: string
|
|
11
|
+
recordId?: string
|
|
12
|
+
organizationId?: string | null
|
|
13
|
+
tenantId?: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type HandlerContext = { resolve: <T = unknown>(name: string) => T }
|
|
17
|
+
|
|
18
|
+
export default async function handle(payload: Payload, ctx: HandlerContext) {
|
|
19
|
+
const entityType = String(payload?.entityType ?? '')
|
|
20
|
+
const recordId = String(payload?.recordId ?? '')
|
|
21
|
+
if (!entityType || !recordId) return
|
|
22
|
+
|
|
23
|
+
let organizationId = payload?.organizationId ?? null
|
|
24
|
+
let tenantId = payload?.tenantId ?? null
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
let em: any | null = null
|
|
28
|
+
try {
|
|
29
|
+
em = ctx.resolve('em')
|
|
30
|
+
} catch {
|
|
31
|
+
em = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if ((organizationId == null || tenantId == null) && em) {
|
|
35
|
+
try {
|
|
36
|
+
const knex = em.getConnection().getKnex()
|
|
37
|
+
const table = resolveEntityTableName(em, entityType)
|
|
38
|
+
const row = await knex(table).select(['organization_id', 'tenant_id']).where({ id: recordId }).first()
|
|
39
|
+
if (organizationId == null) organizationId = row?.organization_id ?? organizationId
|
|
40
|
+
if (tenantId == null) tenantId = row?.tenant_id ?? tenantId
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore lookup errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!tenantId) return
|
|
47
|
+
|
|
48
|
+
const autoIndexingEnabled = await resolveAutoIndexingEnabled(ctx, { defaultValue: true })
|
|
49
|
+
if (!autoIndexingEnabled) return
|
|
50
|
+
|
|
51
|
+
let queue: Queue<VectorIndexJobPayload>
|
|
52
|
+
try {
|
|
53
|
+
queue = ctx.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')
|
|
54
|
+
} catch {
|
|
55
|
+
searchDebugWarn('search.vector', 'vectorIndexQueue not available, skipping vector indexing')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await queue.enqueue({
|
|
61
|
+
jobType: 'index',
|
|
62
|
+
entityType,
|
|
63
|
+
recordId,
|
|
64
|
+
tenantId: String(tenantId),
|
|
65
|
+
organizationId: organizationId ? String(organizationId) : null,
|
|
66
|
+
})
|
|
67
|
+
} catch (error) {
|
|
68
|
+
searchError('search.vector', 'Failed to enqueue vector index job', {
|
|
69
|
+
entityType,
|
|
70
|
+
recordId,
|
|
71
|
+
error: error instanceof Error ? error.message : error,
|
|
72
|
+
})
|
|
73
|
+
throw error // Propagate to caller so failure is visible
|
|
74
|
+
}
|
|
75
|
+
}
|