@open-mercato/search 0.4.6-develop-6953d75a91 → 0.4.6-develop-5cacfcc21a

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 (35) hide show
  1. package/dist/modules/search/__integration__/TC-SEARCH-001.spec.js +64 -0
  2. package/dist/modules/search/__integration__/TC-SEARCH-001.spec.js.map +7 -0
  3. package/dist/modules/search/api/embeddings/reindex/cancel/route.js +10 -0
  4. package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +2 -2
  5. package/dist/modules/search/api/embeddings/reindex/route.js +23 -0
  6. package/dist/modules/search/api/embeddings/reindex/route.js.map +2 -2
  7. package/dist/modules/search/api/reindex/cancel/route.js +10 -0
  8. package/dist/modules/search/api/reindex/cancel/route.js.map +2 -2
  9. package/dist/modules/search/api/reindex/route.js +39 -0
  10. package/dist/modules/search/api/reindex/route.js.map +2 -2
  11. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +13 -26
  12. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +2 -2
  13. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +10 -6
  14. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +2 -2
  15. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +10 -6
  16. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +2 -2
  17. package/dist/modules/search/lib/reindex-progress.js +151 -0
  18. package/dist/modules/search/lib/reindex-progress.js.map +7 -0
  19. package/dist/modules/search/workers/fulltext-index.worker.js +22 -2
  20. package/dist/modules/search/workers/fulltext-index.worker.js.map +2 -2
  21. package/dist/modules/search/workers/vector-index.worker.js +25 -3
  22. package/dist/modules/search/workers/vector-index.worker.js.map +2 -2
  23. package/package.json +4 -4
  24. package/src/modules/search/README.md +13 -0
  25. package/src/modules/search/__integration__/TC-SEARCH-001.spec.ts +80 -0
  26. package/src/modules/search/api/embeddings/reindex/cancel/route.ts +11 -0
  27. package/src/modules/search/api/embeddings/reindex/route.ts +27 -0
  28. package/src/modules/search/api/reindex/cancel/route.ts +11 -0
  29. package/src/modules/search/api/reindex/route.ts +44 -0
  30. package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +13 -33
  31. package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +12 -7
  32. package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +12 -7
  33. package/src/modules/search/lib/reindex-progress.ts +202 -0
  34. package/src/modules/search/workers/fulltext-index.worker.ts +24 -2
  35. package/src/modules/search/workers/vector-index.worker.ts +27 -3
@@ -4,6 +4,7 @@ import * as React from 'react'
4
4
  import { useT } from '@open-mercato/shared/lib/i18n/context'
5
5
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
6
6
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
+ import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
7
8
  import { Button } from '@open-mercato/ui/primitives/button'
8
9
  import { Label } from '@open-mercato/ui/primitives/label'
9
10
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
@@ -234,13 +235,17 @@ export function VectorSearchSection({
234
235
  fetchActivityLogs()
235
236
  }, [fetchActivityLogs])
236
237
 
237
- // Poll for activity when reindexing
238
- React.useEffect(() => {
239
- if (vectorReindexLock || vectorReindexing) {
240
- const interval = setInterval(fetchActivityLogs, 5000)
241
- return () => clearInterval(interval)
242
- }
243
- }, [vectorReindexLock, vectorReindexing, fetchActivityLogs])
238
+ useAppEvent('progress.job.updated', () => {
239
+ void fetchActivityLogs()
240
+ }, [fetchActivityLogs])
241
+
242
+ useAppEvent('progress.job.completed', () => {
243
+ void fetchActivityLogs()
244
+ }, [fetchActivityLogs])
245
+
246
+ useAppEvent('om:bridge:reconnected', () => {
247
+ void fetchActivityLogs()
248
+ }, [fetchActivityLogs])
244
249
 
245
250
  // Update auto-indexing
246
251
  const updateAutoIndexing = React.useCallback(async (nextValue: boolean) => {
@@ -0,0 +1,202 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { ProgressJob } from '@open-mercato/core/modules/progress/data/entities'
3
+ import type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'
4
+
5
+ export type ReindexProgressType = 'fulltext' | 'vector'
6
+
7
+ const REINDEX_JOB_CONFIG: Record<ReindexProgressType, { jobType: string; name: string }> = {
8
+ fulltext: {
9
+ jobType: 'search.reindex.fulltext',
10
+ name: 'Search fulltext reindex',
11
+ },
12
+ vector: {
13
+ jobType: 'search.reindex.vector',
14
+ name: 'Search vector reindex',
15
+ },
16
+ }
17
+
18
+ function buildScopeFilter(
19
+ type: ReindexProgressType,
20
+ tenantId: string,
21
+ organizationId?: string | null,
22
+ ) {
23
+ const config = REINDEX_JOB_CONFIG[type]
24
+ return {
25
+ jobType: config.jobType,
26
+ tenantId,
27
+ organizationId: organizationId ?? null,
28
+ }
29
+ }
30
+
31
+ async function findActiveJob(
32
+ em: EntityManager,
33
+ type: ReindexProgressType,
34
+ tenantId: string,
35
+ organizationId?: string | null,
36
+ ): Promise<ProgressJob | null> {
37
+ return em.findOne(
38
+ ProgressJob,
39
+ {
40
+ ...buildScopeFilter(type, tenantId, organizationId),
41
+ status: { $in: ['pending', 'running'] },
42
+ },
43
+ {
44
+ orderBy: { createdAt: 'DESC' },
45
+ },
46
+ )
47
+ }
48
+
49
+ export async function ensureReindexProgressJob(params: {
50
+ em: EntityManager
51
+ progressService: ProgressService
52
+ type: ReindexProgressType
53
+ tenantId: string
54
+ organizationId?: string | null
55
+ userId?: string | null
56
+ totalCount?: number | null
57
+ description?: string | null
58
+ }): Promise<string | null> {
59
+ const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)
60
+ if (current) {
61
+ if (typeof params.totalCount === 'number') {
62
+ await params.progressService.updateProgress(
63
+ current.id,
64
+ { totalCount: params.totalCount },
65
+ {
66
+ tenantId: params.tenantId,
67
+ organizationId: params.organizationId ?? null,
68
+ userId: params.userId ?? null,
69
+ },
70
+ )
71
+ }
72
+ return current.id
73
+ }
74
+
75
+ const config = REINDEX_JOB_CONFIG[params.type]
76
+ const created = await params.progressService.createJob(
77
+ {
78
+ jobType: config.jobType,
79
+ name: config.name,
80
+ description: params.description ?? undefined,
81
+ totalCount: params.totalCount ?? undefined,
82
+ cancellable: true,
83
+ meta: {
84
+ source: 'search',
85
+ type: params.type,
86
+ },
87
+ },
88
+ {
89
+ tenantId: params.tenantId,
90
+ organizationId: params.organizationId ?? null,
91
+ userId: params.userId ?? null,
92
+ },
93
+ )
94
+ await params.progressService.startJob(created.id, {
95
+ tenantId: params.tenantId,
96
+ organizationId: params.organizationId ?? null,
97
+ userId: params.userId ?? null,
98
+ })
99
+ return created.id
100
+ }
101
+
102
+ export async function incrementReindexProgress(params: {
103
+ em: EntityManager
104
+ progressService: ProgressService
105
+ type: ReindexProgressType
106
+ tenantId: string
107
+ organizationId?: string | null
108
+ delta: number
109
+ }): Promise<boolean> {
110
+ if (!Number.isFinite(params.delta) || params.delta <= 0) return false
111
+ const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)
112
+ if (!current) return false
113
+ const updated = await params.progressService.incrementProgress(
114
+ current.id,
115
+ params.delta,
116
+ {
117
+ tenantId: params.tenantId,
118
+ organizationId: params.organizationId ?? null,
119
+ userId: null,
120
+ },
121
+ )
122
+ if (updated.totalCount && updated.processedCount >= updated.totalCount) {
123
+ await params.progressService.completeJob(
124
+ updated.id,
125
+ {
126
+ resultSummary: {
127
+ processedCount: updated.processedCount,
128
+ totalCount: updated.totalCount,
129
+ },
130
+ },
131
+ {
132
+ tenantId: params.tenantId,
133
+ organizationId: params.organizationId ?? null,
134
+ userId: null,
135
+ },
136
+ )
137
+ return true
138
+ }
139
+ return false
140
+ }
141
+
142
+ export async function completeReindexProgress(params: {
143
+ em: EntityManager
144
+ progressService: ProgressService
145
+ type: ReindexProgressType
146
+ tenantId: string
147
+ organizationId?: string | null
148
+ resultSummary?: Record<string, unknown>
149
+ }): Promise<void> {
150
+ const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)
151
+ if (!current) return
152
+ await params.progressService.completeJob(
153
+ current.id,
154
+ { resultSummary: params.resultSummary ?? {} },
155
+ {
156
+ tenantId: params.tenantId,
157
+ organizationId: params.organizationId ?? null,
158
+ userId: null,
159
+ },
160
+ )
161
+ }
162
+
163
+ export async function failReindexProgress(params: {
164
+ em: EntityManager
165
+ progressService: ProgressService
166
+ type: ReindexProgressType
167
+ tenantId: string
168
+ organizationId?: string | null
169
+ errorMessage: string
170
+ }): Promise<void> {
171
+ const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)
172
+ if (!current) return
173
+ await params.progressService.failJob(
174
+ current.id,
175
+ { errorMessage: params.errorMessage },
176
+ {
177
+ tenantId: params.tenantId,
178
+ organizationId: params.organizationId ?? null,
179
+ userId: null,
180
+ },
181
+ )
182
+ }
183
+
184
+ export async function cancelReindexProgress(params: {
185
+ em: EntityManager
186
+ progressService: ProgressService
187
+ type: ReindexProgressType
188
+ tenantId: string
189
+ organizationId?: string | null
190
+ userId?: string | null
191
+ }): Promise<void> {
192
+ const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)
193
+ if (!current) return
194
+ await params.progressService.cancelJob(
195
+ current.id,
196
+ {
197
+ tenantId: params.tenantId,
198
+ organizationId: params.organizationId ?? null,
199
+ userId: params.userId ?? null,
200
+ },
201
+ )
202
+ }
@@ -7,8 +7,10 @@ import type { Knex } from 'knex'
7
7
  import type { EntityId } from '@open-mercato/shared/modules/entities'
8
8
  import { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'
9
9
  import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
10
+ import type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'
10
11
  import { searchDebug, searchDebugWarn, searchError } from '../../../lib/debug'
11
- import { updateReindexProgress } from '../lib/reindex-lock'
12
+ import { clearReindexLock, updateReindexProgress } from '../lib/reindex-lock'
13
+ import { incrementReindexProgress } from '../lib/reindex-progress'
12
14
 
13
15
  // Worker metadata for auto-discovery
14
16
  const DEFAULT_CONCURRENCY = 2
@@ -93,6 +95,13 @@ export async function handleFulltextIndexJob(
93
95
  }
94
96
 
95
97
  try {
98
+ let progressService: ProgressService | null = null
99
+ try {
100
+ progressService = ctx.resolve<ProgressService>('progressService')
101
+ } catch {
102
+ progressService = null
103
+ }
104
+
96
105
  // ========== SINGLE INDEX: Use searchIndexer.indexRecordById() for fresh data ==========
97
106
  if (jobType === 'index') {
98
107
  const { entityType, recordId, organizationId } = job.payload as {
@@ -184,9 +193,22 @@ export async function handleFulltextIndexJob(
184
193
  }
185
194
 
186
195
  // Update heartbeat to signal worker is still processing
187
- if (knex && successCount > 0) {
196
+ if (knex && records.length > 0) {
188
197
  await updateReindexProgress(knex, tenantId, 'fulltext', successCount, organizationId ?? null)
189
198
  }
199
+ if (progressService && em && records.length > 0) {
200
+ const completed = await incrementReindexProgress({
201
+ em,
202
+ progressService,
203
+ type: 'fulltext',
204
+ tenantId,
205
+ organizationId: organizationId ?? null,
206
+ delta: successCount,
207
+ })
208
+ if (completed && knex) {
209
+ await clearReindexLock(knex, tenantId, 'fulltext', organizationId ?? null)
210
+ }
211
+ }
190
212
 
191
213
  searchDebug('fulltext-index.worker', 'Batch indexed to fulltext', {
192
214
  jobId: jobCtx.jobId,
@@ -4,13 +4,15 @@ import type { SearchIndexer } from '../../../indexer/search-indexer'
4
4
  import type { EmbeddingService } from '../../../vector'
5
5
  import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import type { Knex } from 'knex'
7
+ import type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'
7
8
  import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
8
9
  import { applyCoverageAdjustments, createCoverageAdjustments } from '@open-mercato/core/modules/query_index/lib/coverage'
9
10
  import { logVectorOperation } from '../../../vector/lib/vector-logs'
10
11
  import { resolveAutoIndexingEnabled } from '../lib/auto-indexing'
11
12
  import { resolveEmbeddingConfig } from '../lib/embedding-config'
12
13
  import { searchDebugWarn } from '../../../lib/debug'
13
- import { updateReindexProgress } from '../lib/reindex-lock'
14
+ import { clearReindexLock, updateReindexProgress } from '../lib/reindex-lock'
15
+ import { incrementReindexProgress } from '../lib/reindex-progress'
14
16
 
15
17
  // Worker metadata for auto-discovery
16
18
  const DEFAULT_CONCURRENCY = 2
@@ -61,11 +63,20 @@ export async function handleVectorIndexJob(
61
63
 
62
64
  // Get knex for heartbeat updates
63
65
  let knex: Knex | null = null
66
+ let em: EntityManager | null = null
64
67
  try {
65
- const em = ctx.resolve('em') as EntityManager
68
+ em = ctx.resolve('em') as EntityManager
66
69
  knex = (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()
67
70
  } catch {
68
71
  knex = null
72
+ em = null
73
+ }
74
+
75
+ let progressService: ProgressService | null = null
76
+ try {
77
+ progressService = ctx.resolve<ProgressService>('progressService')
78
+ } catch {
79
+ progressService = null
69
80
  }
70
81
 
71
82
  // Load saved embedding config to use the correct provider/model
@@ -106,9 +117,22 @@ export async function handleVectorIndexJob(
106
117
  }
107
118
 
108
119
  // Update heartbeat to signal worker is still processing
109
- if (knex && successCount > 0) {
120
+ if (knex && records.length > 0) {
110
121
  await updateReindexProgress(knex, tenantId, 'vector', successCount, organizationId ?? null)
111
122
  }
123
+ if (progressService && em && records.length > 0) {
124
+ const completed = await incrementReindexProgress({
125
+ em,
126
+ progressService,
127
+ type: 'vector',
128
+ tenantId,
129
+ organizationId: organizationId ?? null,
130
+ delta: successCount,
131
+ })
132
+ if (completed && knex) {
133
+ await clearReindexLock(knex, tenantId, 'vector', organizationId ?? null)
134
+ }
135
+ }
112
136
 
113
137
  searchDebugWarn('vector-index.worker', 'Batch-index job completed', {
114
138
  jobId: jobCtx.jobId,