@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.
- package/dist/modules/search/__integration__/TC-SEARCH-001.spec.js +64 -0
- package/dist/modules/search/__integration__/TC-SEARCH-001.spec.js.map +7 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js +10 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +2 -2
- package/dist/modules/search/api/embeddings/reindex/route.js +23 -0
- package/dist/modules/search/api/embeddings/reindex/route.js.map +2 -2
- package/dist/modules/search/api/reindex/cancel/route.js +10 -0
- package/dist/modules/search/api/reindex/cancel/route.js.map +2 -2
- package/dist/modules/search/api/reindex/route.js +39 -0
- package/dist/modules/search/api/reindex/route.js.map +2 -2
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +13 -26
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +2 -2
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +10 -6
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +2 -2
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +10 -6
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +2 -2
- package/dist/modules/search/lib/reindex-progress.js +151 -0
- package/dist/modules/search/lib/reindex-progress.js.map +7 -0
- package/dist/modules/search/workers/fulltext-index.worker.js +22 -2
- package/dist/modules/search/workers/fulltext-index.worker.js.map +2 -2
- package/dist/modules/search/workers/vector-index.worker.js +25 -3
- package/dist/modules/search/workers/vector-index.worker.js.map +2 -2
- package/package.json +4 -4
- package/src/modules/search/README.md +13 -0
- package/src/modules/search/__integration__/TC-SEARCH-001.spec.ts +80 -0
- package/src/modules/search/api/embeddings/reindex/cancel/route.ts +11 -0
- package/src/modules/search/api/embeddings/reindex/route.ts +27 -0
- package/src/modules/search/api/reindex/cancel/route.ts +11 -0
- package/src/modules/search/api/reindex/route.ts +44 -0
- package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +13 -33
- package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +12 -7
- package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +12 -7
- package/src/modules/search/lib/reindex-progress.ts +202 -0
- package/src/modules/search/workers/fulltext-index.worker.ts +24 -2
- 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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}, [
|
|
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 &&
|
|
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
|
-
|
|
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 &&
|
|
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,
|