@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.
Files changed (237) hide show
  1. package/AGENTS.md +678 -0
  2. package/build.mjs +92 -0
  3. package/dist/di.js +157 -0
  4. package/dist/di.js.map +7 -0
  5. package/dist/fulltext/drivers/index.js +21 -0
  6. package/dist/fulltext/drivers/index.js.map +7 -0
  7. package/dist/fulltext/drivers/meilisearch/index.js +320 -0
  8. package/dist/fulltext/drivers/meilisearch/index.js.map +7 -0
  9. package/dist/fulltext/index.js +7 -0
  10. package/dist/fulltext/index.js.map +7 -0
  11. package/dist/fulltext/types.js +1 -0
  12. package/dist/fulltext/types.js.map +7 -0
  13. package/dist/index.js +12 -0
  14. package/dist/index.js.map +7 -0
  15. package/dist/indexer/index.js +8 -0
  16. package/dist/indexer/index.js.map +7 -0
  17. package/dist/indexer/search-indexer.js +848 -0
  18. package/dist/indexer/search-indexer.js.map +7 -0
  19. package/dist/indexer/subscribers/delete.js +41 -0
  20. package/dist/indexer/subscribers/delete.js.map +7 -0
  21. package/dist/lib/debug.js +34 -0
  22. package/dist/lib/debug.js.map +7 -0
  23. package/dist/lib/fallback-presenter.js +107 -0
  24. package/dist/lib/fallback-presenter.js.map +7 -0
  25. package/dist/lib/field-policy.js +75 -0
  26. package/dist/lib/field-policy.js.map +7 -0
  27. package/dist/lib/index.js +19 -0
  28. package/dist/lib/index.js.map +7 -0
  29. package/dist/lib/merger.js +93 -0
  30. package/dist/lib/merger.js.map +7 -0
  31. package/dist/lib/presenter-enricher.js +192 -0
  32. package/dist/lib/presenter-enricher.js.map +7 -0
  33. package/dist/modules/search/acl.js +14 -0
  34. package/dist/modules/search/acl.js.map +7 -0
  35. package/dist/modules/search/ai-tools.js +284 -0
  36. package/dist/modules/search/ai-tools.js.map +7 -0
  37. package/dist/modules/search/api/embeddings/reindex/cancel/route.js +65 -0
  38. package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +7 -0
  39. package/dist/modules/search/api/embeddings/reindex/route.js +165 -0
  40. package/dist/modules/search/api/embeddings/reindex/route.js.map +7 -0
  41. package/dist/modules/search/api/embeddings/route.js +246 -0
  42. package/dist/modules/search/api/embeddings/route.js.map +7 -0
  43. package/dist/modules/search/api/index/route.js +245 -0
  44. package/dist/modules/search/api/index/route.js.map +7 -0
  45. package/dist/modules/search/api/reindex/cancel/route.js +65 -0
  46. package/dist/modules/search/api/reindex/cancel/route.js.map +7 -0
  47. package/dist/modules/search/api/reindex/route.js +332 -0
  48. package/dist/modules/search/api/reindex/route.js.map +7 -0
  49. package/dist/modules/search/api/search/global/route.js +100 -0
  50. package/dist/modules/search/api/search/global/route.js.map +7 -0
  51. package/dist/modules/search/api/search/route.js +101 -0
  52. package/dist/modules/search/api/search/route.js.map +7 -0
  53. package/dist/modules/search/api/settings/fulltext/route.js +55 -0
  54. package/dist/modules/search/api/settings/fulltext/route.js.map +7 -0
  55. package/dist/modules/search/api/settings/global-search/route.js +80 -0
  56. package/dist/modules/search/api/settings/global-search/route.js.map +7 -0
  57. package/dist/modules/search/api/settings/route.js +118 -0
  58. package/dist/modules/search/api/settings/route.js.map +7 -0
  59. package/dist/modules/search/api/settings/vector-store/route.js +77 -0
  60. package/dist/modules/search/api/settings/vector-store/route.js.map +7 -0
  61. package/dist/modules/search/backend/config/search/page.js +10 -0
  62. package/dist/modules/search/backend/config/search/page.js.map +7 -0
  63. package/dist/modules/search/backend/config/search/page.meta.js +24 -0
  64. package/dist/modules/search/backend/config/search/page.meta.js.map +7 -0
  65. package/dist/modules/search/cli.js +698 -0
  66. package/dist/modules/search/cli.js.map +7 -0
  67. package/dist/modules/search/di.js +32 -0
  68. package/dist/modules/search/di.js.map +7 -0
  69. package/dist/modules/search/frontend/components/GlobalSearchDialog.js +357 -0
  70. package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +7 -0
  71. package/dist/modules/search/frontend/components/HybridSearchTable.js +343 -0
  72. package/dist/modules/search/frontend/components/HybridSearchTable.js.map +7 -0
  73. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +303 -0
  74. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +7 -0
  75. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +360 -0
  76. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +7 -0
  77. package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js +101 -0
  78. package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js.map +7 -0
  79. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +608 -0
  80. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +7 -0
  81. package/dist/modules/search/frontend/index.js +9 -0
  82. package/dist/modules/search/frontend/index.js.map +7 -0
  83. package/dist/modules/search/frontend/utils.js +41 -0
  84. package/dist/modules/search/frontend/utils.js.map +7 -0
  85. package/dist/modules/search/i18n/de.json +61 -0
  86. package/dist/modules/search/i18n/en.json +72 -0
  87. package/dist/modules/search/i18n/es.json +61 -0
  88. package/dist/modules/search/i18n/pl.json +61 -0
  89. package/dist/modules/search/index.js +11 -0
  90. package/dist/modules/search/index.js.map +7 -0
  91. package/dist/modules/search/lib/auto-indexing.js +29 -0
  92. package/dist/modules/search/lib/auto-indexing.js.map +7 -0
  93. package/dist/modules/search/lib/embedding-config.js +131 -0
  94. package/dist/modules/search/lib/embedding-config.js.map +7 -0
  95. package/dist/modules/search/lib/global-search-config.js +45 -0
  96. package/dist/modules/search/lib/global-search-config.js.map +7 -0
  97. package/dist/modules/search/lib/reindex-lock.js +99 -0
  98. package/dist/modules/search/lib/reindex-lock.js.map +7 -0
  99. package/dist/modules/search/subscribers/fulltext_upsert.js +64 -0
  100. package/dist/modules/search/subscribers/fulltext_upsert.js.map +7 -0
  101. package/dist/modules/search/subscribers/vector_delete.js +58 -0
  102. package/dist/modules/search/subscribers/vector_delete.js.map +7 -0
  103. package/dist/modules/search/subscribers/vector_purge.js +142 -0
  104. package/dist/modules/search/subscribers/vector_purge.js.map +7 -0
  105. package/dist/modules/search/subscribers/vector_upsert.js +58 -0
  106. package/dist/modules/search/subscribers/vector_upsert.js.map +7 -0
  107. package/dist/modules/search/workers/fulltext-index.worker.js +240 -0
  108. package/dist/modules/search/workers/fulltext-index.worker.js.map +7 -0
  109. package/dist/modules/search/workers/vector-index.worker.js +234 -0
  110. package/dist/modules/search/workers/vector-index.worker.js.map +7 -0
  111. package/dist/queue/fulltext-indexing.js +15 -0
  112. package/dist/queue/fulltext-indexing.js.map +7 -0
  113. package/dist/queue/index.js +3 -0
  114. package/dist/queue/index.js.map +7 -0
  115. package/dist/queue/vector-indexing.js +15 -0
  116. package/dist/queue/vector-indexing.js.map +7 -0
  117. package/dist/service.js +286 -0
  118. package/dist/service.js.map +7 -0
  119. package/dist/strategies/fulltext.strategy.js +116 -0
  120. package/dist/strategies/fulltext.strategy.js.map +7 -0
  121. package/dist/strategies/index.js +12 -0
  122. package/dist/strategies/index.js.map +7 -0
  123. package/dist/strategies/token.strategy.js +80 -0
  124. package/dist/strategies/token.strategy.js.map +7 -0
  125. package/dist/strategies/vector.strategy.js +137 -0
  126. package/dist/strategies/vector.strategy.js.map +7 -0
  127. package/dist/types.js +1 -0
  128. package/dist/types.js.map +7 -0
  129. package/dist/vector/drivers/chromadb/index.js +44 -0
  130. package/dist/vector/drivers/chromadb/index.js.map +7 -0
  131. package/dist/vector/drivers/index.js +9 -0
  132. package/dist/vector/drivers/index.js.map +7 -0
  133. package/dist/vector/drivers/pgvector/index.js +509 -0
  134. package/dist/vector/drivers/pgvector/index.js.map +7 -0
  135. package/dist/vector/drivers/qdrant/index.js +44 -0
  136. package/dist/vector/drivers/qdrant/index.js.map +7 -0
  137. package/dist/vector/index.js +4 -0
  138. package/dist/vector/index.js.map +7 -0
  139. package/dist/vector/lib/vector-logs.js +33 -0
  140. package/dist/vector/lib/vector-logs.js.map +7 -0
  141. package/dist/vector/services/checksum.js +20 -0
  142. package/dist/vector/services/checksum.js.map +7 -0
  143. package/dist/vector/services/embedding.js +222 -0
  144. package/dist/vector/services/embedding.js.map +7 -0
  145. package/dist/vector/services/index.js +4 -0
  146. package/dist/vector/services/index.js.map +7 -0
  147. package/dist/vector/services/vector-index.service.js +960 -0
  148. package/dist/vector/services/vector-index.service.js.map +7 -0
  149. package/dist/vector/types/pg.d.js +1 -0
  150. package/dist/vector/types/pg.d.js.map +7 -0
  151. package/dist/vector/types.js +75 -0
  152. package/dist/vector/types.js.map +7 -0
  153. package/jest.config.cjs +19 -0
  154. package/package.json +142 -0
  155. package/src/__tests__/queue.test.ts +148 -0
  156. package/src/__tests__/service.test.ts +345 -0
  157. package/src/__tests__/workers.test.ts +319 -0
  158. package/src/di.ts +291 -0
  159. package/src/fulltext/drivers/index.ts +41 -0
  160. package/src/fulltext/drivers/meilisearch/index.ts +410 -0
  161. package/src/fulltext/index.ts +13 -0
  162. package/src/fulltext/types.ts +115 -0
  163. package/src/index.ts +36 -0
  164. package/src/indexer/index.ts +13 -0
  165. package/src/indexer/search-indexer.ts +1141 -0
  166. package/src/indexer/subscribers/delete.ts +49 -0
  167. package/src/lib/debug.ts +46 -0
  168. package/src/lib/fallback-presenter.ts +106 -0
  169. package/src/lib/field-policy.ts +169 -0
  170. package/src/lib/index.ts +13 -0
  171. package/src/lib/merger.ts +159 -0
  172. package/src/lib/presenter-enricher.ts +323 -0
  173. package/src/modules/search/README.md +694 -0
  174. package/src/modules/search/acl.ts +10 -0
  175. package/src/modules/search/ai-tools.ts +467 -0
  176. package/src/modules/search/api/embeddings/reindex/cancel/route.ts +77 -0
  177. package/src/modules/search/api/embeddings/reindex/route.ts +197 -0
  178. package/src/modules/search/api/embeddings/route.ts +304 -0
  179. package/src/modules/search/api/index/route.ts +297 -0
  180. package/src/modules/search/api/reindex/cancel/route.ts +77 -0
  181. package/src/modules/search/api/reindex/route.ts +419 -0
  182. package/src/modules/search/api/search/global/route.ts +120 -0
  183. package/src/modules/search/api/search/route.ts +121 -0
  184. package/src/modules/search/api/settings/fulltext/route.ts +82 -0
  185. package/src/modules/search/api/settings/global-search/route.ts +91 -0
  186. package/src/modules/search/api/settings/route.ts +187 -0
  187. package/src/modules/search/api/settings/vector-store/route.ts +105 -0
  188. package/src/modules/search/backend/config/search/page.meta.ts +22 -0
  189. package/src/modules/search/backend/config/search/page.tsx +12 -0
  190. package/src/modules/search/cli.ts +818 -0
  191. package/src/modules/search/di.ts +50 -0
  192. package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +436 -0
  193. package/src/modules/search/frontend/components/HybridSearchTable.tsx +418 -0
  194. package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +476 -0
  195. package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +624 -0
  196. package/src/modules/search/frontend/components/sections/GlobalSearchSection.tsx +124 -0
  197. package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +943 -0
  198. package/src/modules/search/frontend/index.ts +3 -0
  199. package/src/modules/search/frontend/utils.ts +82 -0
  200. package/src/modules/search/i18n/de.json +61 -0
  201. package/src/modules/search/i18n/en.json +72 -0
  202. package/src/modules/search/i18n/es.json +61 -0
  203. package/src/modules/search/i18n/pl.json +61 -0
  204. package/src/modules/search/index.ts +9 -0
  205. package/src/modules/search/lib/auto-indexing.ts +35 -0
  206. package/src/modules/search/lib/embedding-config.ts +161 -0
  207. package/src/modules/search/lib/global-search-config.ts +69 -0
  208. package/src/modules/search/lib/reindex-lock.ts +201 -0
  209. package/src/modules/search/subscribers/fulltext_upsert.ts +83 -0
  210. package/src/modules/search/subscribers/vector_delete.ts +75 -0
  211. package/src/modules/search/subscribers/vector_purge.ts +161 -0
  212. package/src/modules/search/subscribers/vector_upsert.ts +75 -0
  213. package/src/modules/search/workers/fulltext-index.worker.ts +318 -0
  214. package/src/modules/search/workers/vector-index.worker.ts +292 -0
  215. package/src/queue/fulltext-indexing.ts +87 -0
  216. package/src/queue/index.ts +2 -0
  217. package/src/queue/vector-indexing.ts +66 -0
  218. package/src/service.ts +397 -0
  219. package/src/strategies/fulltext.strategy.ts +155 -0
  220. package/src/strategies/index.ts +17 -0
  221. package/src/strategies/token.strategy.ts +153 -0
  222. package/src/strategies/vector.strategy.ts +234 -0
  223. package/src/types.ts +38 -0
  224. package/src/vector/drivers/chromadb/index.ts +49 -0
  225. package/src/vector/drivers/index.ts +4 -0
  226. package/src/vector/drivers/pgvector/index.ts +627 -0
  227. package/src/vector/drivers/qdrant/index.ts +49 -0
  228. package/src/vector/index.ts +3 -0
  229. package/src/vector/lib/vector-logs.ts +46 -0
  230. package/src/vector/services/checksum.ts +18 -0
  231. package/src/vector/services/embedding.ts +275 -0
  232. package/src/vector/services/index.ts +3 -0
  233. package/src/vector/services/vector-index.service.ts +1234 -0
  234. package/src/vector/types/pg.d.ts +1 -0
  235. package/src/vector/types.ts +220 -0
  236. package/tsconfig.json +9 -0
  237. 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
+ }