@open-mercato/search 0.6.6-develop.5588.1.a8f6c51d1f → 0.6.6-develop.5594.1.30cd738303
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/api/embeddings/reindex/route.js +29 -1
- package/dist/modules/search/api/embeddings/reindex/route.js.map +2 -2
- package/dist/modules/search/api/reindex/route.js +6 -2
- package/dist/modules/search/api/reindex/route.js.map +2 -2
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +8 -0
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +2 -2
- package/dist/modules/search/lib/reindex-progress.js +4 -0
- package/dist/modules/search/lib/reindex-progress.js.map +2 -2
- package/dist/modules/search/workers/fulltext-index.worker.js +44 -17
- package/dist/modules/search/workers/fulltext-index.worker.js.map +2 -2
- package/dist/modules/search/workers/vector-index.worker.js +113 -70
- package/dist/modules/search/workers/vector-index.worker.js.map +3 -3
- package/package.json +4 -4
- package/src/__tests__/workers.test.ts +171 -3
- package/src/modules/search/api/__tests__/embeddings-reindex.routes.test.ts +130 -0
- package/src/modules/search/api/__tests__/reindex.routes.test.ts +165 -0
- package/src/modules/search/api/embeddings/reindex/route.ts +29 -0
- package/src/modules/search/api/reindex/route.ts +6 -2
- package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +11 -0
- package/src/modules/search/lib/reindex-progress.ts +9 -0
- package/src/modules/search/workers/fulltext-index.worker.ts +57 -18
- package/src/modules/search/workers/vector-index.worker.ts +141 -73
|
@@ -5,8 +5,9 @@ import { recordIndexerLog } from "@open-mercato/shared/lib/indexers/status-log";
|
|
|
5
5
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
6
6
|
import { resolveEmbeddingConfig } from "../../../lib/embedding-config.js";
|
|
7
7
|
import { searchDebug, searchDebugWarn, searchError } from "../../../../../lib/debug.js";
|
|
8
|
-
import { acquireReindexLock, getReindexLockStatus } from "../../../lib/reindex-lock.js";
|
|
8
|
+
import { acquireReindexLock, clearReindexLock, getReindexLockStatus } from "../../../lib/reindex-lock.js";
|
|
9
9
|
import {
|
|
10
|
+
completeReindexProgress,
|
|
10
11
|
ensureReindexProgressJob,
|
|
11
12
|
failReindexProgress
|
|
12
13
|
} from "../../../lib/reindex-progress.js";
|
|
@@ -131,6 +132,33 @@ async function POST(req) {
|
|
|
131
132
|
totalCount: result.recordsIndexed,
|
|
132
133
|
description: entityId ? `Vector reindex ${entityId} (queued)` : "Vector reindex all entities (queued)"
|
|
133
134
|
});
|
|
135
|
+
if ((result.jobsEnqueued ?? 0) === 0) {
|
|
136
|
+
if (result.success) {
|
|
137
|
+
await completeReindexProgress({
|
|
138
|
+
em: em2,
|
|
139
|
+
progressService,
|
|
140
|
+
type: "vector",
|
|
141
|
+
tenantId: auth.tenantId,
|
|
142
|
+
organizationId: auth.orgId ?? null,
|
|
143
|
+
resultSummary: {
|
|
144
|
+
entitiesProcessed: result.entitiesProcessed,
|
|
145
|
+
recordsIndexed: result.recordsIndexed,
|
|
146
|
+
jobsEnqueued: result.jobsEnqueued ?? 0,
|
|
147
|
+
errors: result.errors.length
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
await failReindexProgress({
|
|
152
|
+
em: em2,
|
|
153
|
+
progressService,
|
|
154
|
+
type: "vector",
|
|
155
|
+
tenantId: auth.tenantId,
|
|
156
|
+
organizationId: auth.orgId ?? null,
|
|
157
|
+
errorMessage: result.errors[0]?.error ?? "Vector reindex failed before queueing work"
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
await clearReindexLock(db, auth.tenantId, "vector", auth.orgId ?? null);
|
|
161
|
+
}
|
|
134
162
|
await recordIndexerLog(
|
|
135
163
|
{ em: em2 ?? void 0 },
|
|
136
164
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/search/api/embeddings/reindex/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { SearchIndexer } from '../../../../../indexer/search-indexer'\nimport type { EmbeddingService } from '../../../../../vector'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Kysely } from 'kysely'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveEmbeddingConfig } from '../../../lib/embedding-config'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { searchDebug, searchDebugWarn, searchError } from '../../../../../lib/debug'\nimport { acquireReindexLock, clearReindexLock, getReindexLockStatus } from '../../../lib/reindex-lock'\nimport {\n ensureReindexProgressJob,\n failReindexProgress,\n} from '../../../lib/reindex-progress'\nimport { embeddingsReindexOpenApi } from '../../openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['search.embeddings.manage'] },\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n let payload: { entityId?: string; purgeFirst?: boolean } = {}\n try {\n payload = await req.json()\n } catch {\n // Default values\n }\n\n const entityId = typeof payload?.entityId === 'string' ? payload.entityId : undefined\n const purgeFirst = payload?.purgeFirst === true\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const progressService = container.resolve('progressService') as ProgressService\n const db = (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n\n // Check if another vector reindex operation is already in progress\n const existingLock = await getReindexLockStatus(db, auth.tenantId, { type: 'vector' })\n if (existingLock) {\n const startedAt = new Date(existingLock.startedAt)\n return NextResponse.json(\n {\n error: t('search.api.errors.reindexInProgress', 'A reindex operation is already in progress'),\n lock: {\n type: existingLock.type,\n action: existingLock.action,\n startedAt: existingLock.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: existingLock.processedCount,\n totalCount: existingLock.totalCount,\n },\n },\n { status: 409 }\n )\n }\n\n // Acquire lock before starting the operation\n const { acquired: lockAcquired } = await acquireReindexLock(db, {\n type: 'vector',\n action: entityId ? `reindex:${entityId}` : 'reindex:all',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n\n if (!lockAcquired) {\n return NextResponse.json(\n { error: t('search.api.errors.lockFailed', 'Failed to acquire reindex lock') },\n { status: 409 }\n )\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any = null\n try {\n em = container.resolve('em')\n } catch {\n // em not available\n }\n\n let searchIndexer: SearchIndexer\n try {\n searchIndexer = container.resolve('searchIndexer') as SearchIndexer\n } catch {\n return NextResponse.json(\n { error: t('search.api.errors.indexUnavailable', 'Search indexer unavailable') },\n { status: 503 }\n )\n }\n\n // Load saved embedding config and update the embedding service\n try {\n const embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null })\n if (embeddingConfig) {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n embeddingService.updateConfig(embeddingConfig)\n searchDebug('search.embeddings.reindex', 'using embedding config', {\n providerId: embeddingConfig.providerId,\n model: embeddingConfig.model,\n dimension: embeddingConfig.dimension,\n })\n }\n } catch (err) {\n searchDebugWarn('search.embeddings.reindex', 'failed to load embedding config, using defaults', {\n error: err instanceof Error ? err.message : err,\n })\n }\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'vector',\n handler: 'api:search.embeddings.reindex',\n message: entityId\n ? `Vector reindex requested for ${entityId}`\n : 'Vector reindex requested for all entities',\n entityType: entityId ?? null,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: { purgeFirst },\n },\n ).catch(() => undefined)\n\n // Use queue-based vector reindexing (similar to fulltext)\n // This enqueues batches for background processing by workers\n let result\n if (entityId) {\n result = await searchIndexer.reindexEntityToVector({\n entityId: entityId as EntityId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n purgeFirst,\n useQueue: true,\n })\n } else {\n result = await searchIndexer.reindexAllToVector({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n purgeFirst,\n useQueue: true,\n })\n }\n\n await ensureReindexProgressJob({\n em,\n progressService,\n type: 'vector',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub ?? null,\n totalCount: result.recordsIndexed,\n description: entityId\n ? `Vector reindex ${entityId} (queued)`\n : 'Vector reindex all entities (queued)',\n })\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'vector',\n handler: 'api:search.embeddings.reindex',\n message: result.jobsEnqueued\n ? `Vector reindex enqueued ${result.jobsEnqueued} jobs for ${entityId ?? 'all entities'}`\n : `Vector reindex completed for ${entityId ?? 'all entities'}`,\n entityType: entityId ?? null,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: {\n purgeFirst,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n success: result.success,\n },\n },\n ).catch(() => undefined)\n\n return NextResponse.json({\n ok: result.success,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n entitiesProcessed: result.entitiesProcessed,\n errors: result.errors.length > 0 ? result.errors : undefined,\n })\n } catch (error: unknown) {\n const err = error as { message?: string; status?: number; statusCode?: number }\n const status = typeof err?.status === 'number'\n ? err.status\n : (typeof err?.statusCode === 'number' ? err.statusCode : 500)\n searchError('search.embeddings.reindex', 'failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n status,\n })\n await failReindexProgress({\n em,\n progressService,\n type: 'vector',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n errorMessage: error instanceof Error ? error.message : 'Vector reindex failed',\n })\n return NextResponse.json(\n { error: t('search.api.errors.reindexFailed', 'Vector reindex failed. Please try again or contact support.') },\n { status: status >= 400 ? status : 500 }\n )\n } finally {\n // Do NOT clear lock here - vector reindex always uses queue mode\n // Workers update heartbeat and stale detection handles cleanup when done\n\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = embeddingsReindexOpenApi\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AAOnC,SAAS,wBAAwB;AACjC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AAEvC,SAAS,aAAa,iBAAiB,mBAAmB;AAC1D,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { SearchIndexer } from '../../../../../indexer/search-indexer'\nimport type { EmbeddingService } from '../../../../../vector'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Kysely } from 'kysely'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveEmbeddingConfig } from '../../../lib/embedding-config'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { searchDebug, searchDebugWarn, searchError } from '../../../../../lib/debug'\nimport { acquireReindexLock, clearReindexLock, getReindexLockStatus } from '../../../lib/reindex-lock'\nimport {\n completeReindexProgress,\n ensureReindexProgressJob,\n failReindexProgress,\n} from '../../../lib/reindex-progress'\nimport { embeddingsReindexOpenApi } from '../../openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['search.embeddings.manage'] },\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n let payload: { entityId?: string; purgeFirst?: boolean } = {}\n try {\n payload = await req.json()\n } catch {\n // Default values\n }\n\n const entityId = typeof payload?.entityId === 'string' ? payload.entityId : undefined\n const purgeFirst = payload?.purgeFirst === true\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const progressService = container.resolve('progressService') as ProgressService\n const db = (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n\n // Check if another vector reindex operation is already in progress\n const existingLock = await getReindexLockStatus(db, auth.tenantId, { type: 'vector' })\n if (existingLock) {\n const startedAt = new Date(existingLock.startedAt)\n return NextResponse.json(\n {\n error: t('search.api.errors.reindexInProgress', 'A reindex operation is already in progress'),\n lock: {\n type: existingLock.type,\n action: existingLock.action,\n startedAt: existingLock.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: existingLock.processedCount,\n totalCount: existingLock.totalCount,\n },\n },\n { status: 409 }\n )\n }\n\n // Acquire lock before starting the operation\n const { acquired: lockAcquired } = await acquireReindexLock(db, {\n type: 'vector',\n action: entityId ? `reindex:${entityId}` : 'reindex:all',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n\n if (!lockAcquired) {\n return NextResponse.json(\n { error: t('search.api.errors.lockFailed', 'Failed to acquire reindex lock') },\n { status: 409 }\n )\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let em: any = null\n try {\n em = container.resolve('em')\n } catch {\n // em not available\n }\n\n let searchIndexer: SearchIndexer\n try {\n searchIndexer = container.resolve('searchIndexer') as SearchIndexer\n } catch {\n return NextResponse.json(\n { error: t('search.api.errors.indexUnavailable', 'Search indexer unavailable') },\n { status: 503 }\n )\n }\n\n // Load saved embedding config and update the embedding service\n try {\n const embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null })\n if (embeddingConfig) {\n const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')\n embeddingService.updateConfig(embeddingConfig)\n searchDebug('search.embeddings.reindex', 'using embedding config', {\n providerId: embeddingConfig.providerId,\n model: embeddingConfig.model,\n dimension: embeddingConfig.dimension,\n })\n }\n } catch (err) {\n searchDebugWarn('search.embeddings.reindex', 'failed to load embedding config, using defaults', {\n error: err instanceof Error ? err.message : err,\n })\n }\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'vector',\n handler: 'api:search.embeddings.reindex',\n message: entityId\n ? `Vector reindex requested for ${entityId}`\n : 'Vector reindex requested for all entities',\n entityType: entityId ?? null,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: { purgeFirst },\n },\n ).catch(() => undefined)\n\n // Use queue-based vector reindexing (similar to fulltext)\n // This enqueues batches for background processing by workers\n let result\n if (entityId) {\n result = await searchIndexer.reindexEntityToVector({\n entityId: entityId as EntityId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n purgeFirst,\n useQueue: true,\n })\n } else {\n result = await searchIndexer.reindexAllToVector({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n purgeFirst,\n useQueue: true,\n })\n }\n\n await ensureReindexProgressJob({\n em,\n progressService,\n type: 'vector',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub ?? null,\n totalCount: result.recordsIndexed,\n description: entityId\n ? `Vector reindex ${entityId} (queued)`\n : 'Vector reindex all entities (queued)',\n })\n\n if ((result.jobsEnqueued ?? 0) === 0) {\n if (result.success) {\n await completeReindexProgress({\n em,\n progressService,\n type: 'vector',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n resultSummary: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued ?? 0,\n errors: result.errors.length,\n },\n })\n } else {\n await failReindexProgress({\n em,\n progressService,\n type: 'vector',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n errorMessage: result.errors[0]?.error ?? 'Vector reindex failed before queueing work',\n })\n }\n await clearReindexLock(db, auth.tenantId, 'vector', auth.orgId ?? null)\n }\n\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'vector',\n handler: 'api:search.embeddings.reindex',\n message: result.jobsEnqueued\n ? `Vector reindex enqueued ${result.jobsEnqueued} jobs for ${entityId ?? 'all entities'}`\n : `Vector reindex completed for ${entityId ?? 'all entities'}`,\n entityType: entityId ?? null,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: {\n purgeFirst,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n success: result.success,\n },\n },\n ).catch(() => undefined)\n\n return NextResponse.json({\n ok: result.success,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n entitiesProcessed: result.entitiesProcessed,\n errors: result.errors.length > 0 ? result.errors : undefined,\n })\n } catch (error: unknown) {\n const err = error as { message?: string; status?: number; statusCode?: number }\n const status = typeof err?.status === 'number'\n ? err.status\n : (typeof err?.statusCode === 'number' ? err.statusCode : 500)\n searchError('search.embeddings.reindex', 'failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n status,\n })\n await failReindexProgress({\n em,\n progressService,\n type: 'vector',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n errorMessage: error instanceof Error ? error.message : 'Vector reindex failed',\n })\n return NextResponse.json(\n { error: t('search.api.errors.reindexFailed', 'Vector reindex failed. Please try again or contact support.') },\n { status: status >= 400 ? status : 500 }\n )\n } finally {\n // Do NOT clear lock here - vector reindex always uses queue mode\n // Workers update heartbeat and stale detection handles cleanup when done\n\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = embeddingsReindexOpenApi\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["em"]
|
|
7
7
|
}
|
|
@@ -54,6 +54,7 @@ async function POST(req) {
|
|
|
54
54
|
const action = payload.action === "clear" ? "clear" : payload.action === "recreate" ? "recreate" : "reindex";
|
|
55
55
|
const entityId = typeof payload.entityId === "string" ? payload.entityId : void 0;
|
|
56
56
|
const useQueue = payload.useQueue !== false;
|
|
57
|
+
let keepLockForQueuedWorkers = false;
|
|
57
58
|
const container = await createRequestContainer();
|
|
58
59
|
const em = container.resolve("em");
|
|
59
60
|
const progressService = container.resolve("progressService");
|
|
@@ -244,7 +245,8 @@ async function POST(req) {
|
|
|
244
245
|
totalCount: result.recordsIndexed,
|
|
245
246
|
description: entityId ? `Reindex ${entityId} (${useQueue ? "queued" : "sync"})` : `Reindex all entities (${useQueue ? "queued" : "sync"})`
|
|
246
247
|
});
|
|
247
|
-
|
|
248
|
+
const jobsEnqueued = result.jobsEnqueued ?? 0;
|
|
249
|
+
if (!useQueue || jobsEnqueued === 0) {
|
|
248
250
|
await completeReindexProgress({
|
|
249
251
|
em,
|
|
250
252
|
progressService,
|
|
@@ -258,6 +260,8 @@ async function POST(req) {
|
|
|
258
260
|
errors: result.errors.length
|
|
259
261
|
}
|
|
260
262
|
});
|
|
263
|
+
} else {
|
|
264
|
+
keepLockForQueuedWorkers = true;
|
|
261
265
|
}
|
|
262
266
|
const stats2 = await collectStrategyStats(searchStrategies, tenantId);
|
|
263
267
|
return toJson({
|
|
@@ -356,7 +360,7 @@ async function POST(req) {
|
|
|
356
360
|
{ status: 500 }
|
|
357
361
|
);
|
|
358
362
|
} finally {
|
|
359
|
-
if (!
|
|
363
|
+
if (!keepLockForQueuedWorkers) {
|
|
360
364
|
await clearReindexLock(db, tenantId, "fulltext", auth.orgId ?? null);
|
|
361
365
|
}
|
|
362
366
|
const disposable = container;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/search/api/reindex/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { SearchStrategy } from '@open-mercato/shared/modules/search'\nimport type { SearchIndexer } from '@open-mercato/search/indexer'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Kysely } from 'kysely'\nimport { searchDebug, searchError } from '../../../../lib/debug'\nimport {\n acquireReindexLock,\n clearReindexLock,\n getReindexLockStatus,\n} from '../../lib/reindex-lock'\nimport {\n completeReindexProgress,\n ensureReindexProgressJob,\n failReindexProgress,\n} from '../../lib/reindex-progress'\nimport { reindexOpenApi } from '../openapi'\n\n/** Strategy with optional stats support */\ntype StrategyWithStats = SearchStrategy & {\n getIndexStats?: (tenantId: string) => Promise<Record<string, unknown> | null>\n clearIndex?: (tenantId: string) => Promise<void>\n recreateIndex?: (tenantId: string) => Promise<void>\n}\n\n/** Collect stats from all strategies that support it */\nasync function collectStrategyStats(\n strategies: StrategyWithStats[],\n tenantId: string\n): Promise<Record<string, Record<string, unknown> | null>> {\n const stats: Record<string, Record<string, unknown> | null> = {}\n for (const strategy of strategies) {\n if (typeof strategy.getIndexStats === 'function') {\n try {\n const isAvailable = await strategy.isAvailable()\n if (isAvailable) {\n stats[strategy.id] = await strategy.getIndexStats(tenantId)\n }\n } catch {\n // Skip strategy if stats collection fails\n }\n }\n }\n return stats\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['search.reindex'] },\n}\n\ntype ReindexAction = 'clear' | 'recreate' | 'reindex'\n\nconst toJson = (payload: Record<string, unknown>, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return await unauthorized()\n }\n\n // Capture tenantId as non-null for TypeScript (we checked above)\n const tenantId = auth.tenantId\n\n let payload: { action?: ReindexAction; entityId?: string; useQueue?: boolean } = {}\n try {\n payload = await req.json()\n } catch {\n // Default to reindex\n }\n\n const action: ReindexAction =\n payload.action === 'clear' ? 'clear' :\n payload.action === 'recreate' ? 'recreate' : 'reindex'\n const entityId = typeof payload.entityId === 'string' ? payload.entityId : undefined\n // Use queue by default (requires queue workers to be running), can be disabled with useQueue: false\n const useQueue = payload.useQueue !== false\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const progressService = container.resolve('progressService') as ProgressService\n const db = (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n\n // Check if another fulltext reindex operation is already in progress\n const existingLock = await getReindexLockStatus(db, tenantId, { type: 'fulltext' })\n if (existingLock) {\n const startedAt = new Date(existingLock.startedAt)\n return NextResponse.json(\n {\n error: t('search.api.errors.reindexInProgress', 'A reindex operation is already in progress'),\n lock: {\n type: existingLock.type,\n action: existingLock.action,\n startedAt: existingLock.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: existingLock.processedCount,\n totalCount: existingLock.totalCount,\n },\n },\n { status: 409 }\n )\n }\n\n // Acquire lock before starting the operation\n const { acquired: lockAcquired } = await acquireReindexLock(db, {\n type: 'fulltext',\n action,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n })\n\n if (!lockAcquired) {\n return NextResponse.json(\n { error: t('search.api.errors.lockFailed', 'Failed to acquire reindex lock') },\n { status: 409 }\n )\n }\n\n try {\n // Get all search strategies\n const searchStrategies = (container.resolve('searchStrategies') as StrategyWithStats[] | undefined) ?? []\n\n // Find a strategy that supports index management (clear/recreate)\n const indexableStrategy = searchStrategies.find(\n (s) => typeof s.clearIndex === 'function' || typeof s.recreateIndex === 'function'\n )\n\n if (!indexableStrategy) {\n return toJson(\n { error: t('search.api.errors.noIndexableStrategy', 'No indexable search strategy is configured') },\n { status: 503 }\n )\n }\n\n // Check if strategy is available\n const isAvailable = await indexableStrategy.isAvailable()\n if (!isAvailable) {\n return toJson(\n { error: t('search.api.errors.strategyUnavailable', 'Search strategy is not available') },\n { status: 503 }\n )\n }\n\n // Perform the requested action\n if (action === 'reindex') {\n // Full reindex: recreate index and re-index all data\n const searchIndexer = container.resolve('searchIndexer') as SearchIndexer | undefined\n if (!searchIndexer) {\n return toJson(\n { error: t('search.api.errors.indexerUnavailable', 'Search indexer is not available') },\n { status: 503 }\n )\n }\n\n let result\n const orgId = typeof auth.orgId === 'string' ? auth.orgId : null\n\n // Debug: List enabled entities\n const enabledEntities = searchIndexer.listEnabledEntities()\n searchDebug('search.reindex', 'Starting reindex', {\n tenantId: tenantId,\n orgId,\n enabledEntities,\n entityId: entityId ?? 'all',\n useQueue,\n })\n\n // Log reindex started\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: entityId\n ? `Starting Meilisearch reindex for ${entityId}`\n : `Starting Meilisearch reindex for all entities (${enabledEntities.join(', ')})`,\n entityType: entityId ?? null,\n tenantId: tenantId,\n organizationId: orgId,\n details: { enabledEntities, useQueue },\n },\n )\n\n if (entityId) {\n // Reindex specific entity\n result = await searchIndexer.reindexEntityToFulltext({\n entityId: entityId as EntityId,\n tenantId: tenantId,\n organizationId: orgId,\n recreateIndex: true,\n useQueue,\n onProgress: (progress) => {\n searchDebug('search.reindex', 'Progress', progress)\n // Note: Heartbeat is updated by workers during job processing, not during enqueueing\n },\n })\n searchDebug('search.reindex', 'Reindexed entity to Meilisearch', {\n entityId,\n tenantId: tenantId,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n errors: result.errors,\n })\n\n // Log to indexer status logs\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: useQueue\n ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of ${entityId}`\n : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${entityId}`,\n entityType: entityId,\n tenantId: tenantId,\n organizationId: orgId,\n details: {\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n useQueue,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n },\n )\n\n // Log any batch errors to error logs\n for (const err of result.errors) {\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error: new Error(err.error),\n entityType: err.entityId,\n tenantId: tenantId,\n organizationId: orgId,\n payload: { action, useQueue },\n },\n )\n }\n } else {\n // Reindex all entities\n result = await searchIndexer.reindexAllToFulltext({\n tenantId: tenantId,\n organizationId: orgId,\n recreateIndex: true,\n useQueue,\n onProgress: (progress) => {\n searchDebug('search.reindex', 'Progress', progress)\n // Note: Heartbeat is updated by workers during job processing, not during enqueueing\n },\n })\n searchDebug('search.reindex', 'Reindexed all entities to Meilisearch', {\n tenantId: tenantId,\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n errors: result.errors,\n })\n\n // Log to indexer status logs\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: useQueue\n ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of all entities`\n : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${result.entitiesProcessed} entities`,\n tenantId: tenantId,\n organizationId: orgId,\n details: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n useQueue,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n },\n )\n\n // Log any batch errors to error logs\n for (const err of result.errors) {\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error: new Error(err.error),\n entityType: err.entityId,\n tenantId: tenantId,\n organizationId: orgId,\n payload: { action, useQueue },\n },\n )\n }\n }\n\n await ensureReindexProgressJob({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub ?? null,\n totalCount: result.recordsIndexed,\n description: entityId\n ? `Reindex ${entityId} (${useQueue ? 'queued' : 'sync'})`\n : `Reindex all entities (${useQueue ? 'queued' : 'sync'})`,\n })\n if (!useQueue) {\n await completeReindexProgress({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n resultSummary: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued ?? 0,\n errors: result.errors.length,\n },\n })\n }\n\n // Get updated stats from all strategies\n const stats = await collectStrategyStats(searchStrategies, tenantId)\n\n return toJson({\n ok: result.success,\n action,\n entityId: entityId ?? null,\n useQueue,\n result: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued ?? 0,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n stats,\n })\n } else if (entityId) {\n // Purge specific entity\n await indexableStrategy.purge?.(entityId as EntityId, tenantId)\n searchDebug('search.reindex', 'Purged entity', { strategyId: indexableStrategy.id, entityId, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: `Purged entity ${entityId} from Meilisearch`,\n entityType: entityId,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n } else if (action === 'clear') {\n // Clear all documents but keep index\n if (indexableStrategy.clearIndex) {\n await indexableStrategy.clearIndex(tenantId)\n searchDebug('search.reindex', 'Cleared index', { strategyId: indexableStrategy.id, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: 'Cleared all documents from Meilisearch index',\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n }\n } else {\n // Recreate the entire index\n if (indexableStrategy.recreateIndex) {\n await indexableStrategy.recreateIndex(tenantId)\n searchDebug('search.reindex', 'Recreated index', { strategyId: indexableStrategy.id, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: 'Recreated Meilisearch index',\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n }\n }\n\n // Get updated stats from all strategies\n const stats = await collectStrategyStats(searchStrategies, tenantId)\n\n return toJson({\n ok: true,\n action,\n entityId: entityId ?? null,\n stats,\n })\n } catch (error: unknown) {\n // Log full error details server-side only\n searchError('search.reindex', 'Failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n tenantId: tenantId,\n })\n\n // Record error to indexer error logs\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error,\n entityType: entityId ?? null,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n payload: { action, entityId, useQueue },\n },\n )\n\n await failReindexProgress({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n errorMessage: error instanceof Error ? error.message : 'Fulltext reindex failed',\n })\n\n // Return generic message to client - don't expose internal error details\n return toJson(\n { error: t('search.api.errors.reindexFailed', 'Reindex operation failed. Please try again or contact support.') },\n { status: 500 }\n )\n } finally {\n // Only clear lock immediately if NOT using queue mode\n // When using queue mode, workers update heartbeat and stale detection handles cleanup\n if (!useQueue) {\n await clearReindexLock(db, tenantId, 'fulltext', auth.orgId ?? null)\n }\n\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = reindexOpenApi\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AAIpC,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AAInC,SAAS,aAAa,mBAAmB;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAU/B,eAAe,qBACb,YACA,UACyD;AACzD,QAAM,QAAwD,CAAC;AAC/D,aAAW,YAAY,YAAY;AACjC,QAAI,OAAO,SAAS,kBAAkB,YAAY;AAChD,UAAI;AACF,cAAM,cAAc,MAAM,SAAS,YAAY;AAC/C,YAAI,aAAa;AACf,gBAAM,SAAS,EAAE,IAAI,MAAM,SAAS,cAAc,QAAQ;AAAA,QAC5D;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEO,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AACjE;AAIA,MAAM,SAAS,CAAC,SAAkC,SAAwB,aAAa,KAAK,SAAS,IAAI;AAEzG,MAAM,eAAe,YAAY;AAC/B,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,SAAO,aAAa,KAAK,EAAE,OAAO,EAAE,2BAA2B,cAAc,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AACnG;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,UAAU;AACnB,WAAO,MAAM,aAAa;AAAA,EAC5B;AAGA,QAAM,WAAW,KAAK;AAEtB,MAAI,UAA6E,CAAC;AAClF,MAAI;AACF,cAAU,MAAM,IAAI,KAAK;AAAA,EAC3B,QAAQ;AAAA,EAER;AAEA,QAAM,SACJ,QAAQ,WAAW,UAAU,UAC7B,QAAQ,WAAW,aAAa,aAAa;AAC/C,QAAM,WAAW,OAAO,QAAQ,aAAa,WAAW,QAAQ,WAAW;AAE3E,QAAM,WAAW,QAAQ,aAAa;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { SearchStrategy } from '@open-mercato/shared/modules/search'\nimport type { SearchIndexer } from '@open-mercato/search/indexer'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Kysely } from 'kysely'\nimport { searchDebug, searchError } from '../../../../lib/debug'\nimport {\n acquireReindexLock,\n clearReindexLock,\n getReindexLockStatus,\n} from '../../lib/reindex-lock'\nimport {\n completeReindexProgress,\n ensureReindexProgressJob,\n failReindexProgress,\n} from '../../lib/reindex-progress'\nimport { reindexOpenApi } from '../openapi'\n\n/** Strategy with optional stats support */\ntype StrategyWithStats = SearchStrategy & {\n getIndexStats?: (tenantId: string) => Promise<Record<string, unknown> | null>\n clearIndex?: (tenantId: string) => Promise<void>\n recreateIndex?: (tenantId: string) => Promise<void>\n}\n\n/** Collect stats from all strategies that support it */\nasync function collectStrategyStats(\n strategies: StrategyWithStats[],\n tenantId: string\n): Promise<Record<string, Record<string, unknown> | null>> {\n const stats: Record<string, Record<string, unknown> | null> = {}\n for (const strategy of strategies) {\n if (typeof strategy.getIndexStats === 'function') {\n try {\n const isAvailable = await strategy.isAvailable()\n if (isAvailable) {\n stats[strategy.id] = await strategy.getIndexStats(tenantId)\n }\n } catch {\n // Skip strategy if stats collection fails\n }\n }\n }\n return stats\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['search.reindex'] },\n}\n\ntype ReindexAction = 'clear' | 'recreate' | 'reindex'\n\nconst toJson = (payload: Record<string, unknown>, init?: ResponseInit) => NextResponse.json(payload, init)\n\nconst unauthorized = async () => {\n const { t } = await resolveTranslations()\n return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return await unauthorized()\n }\n\n // Capture tenantId as non-null for TypeScript (we checked above)\n const tenantId = auth.tenantId\n\n let payload: { action?: ReindexAction; entityId?: string; useQueue?: boolean } = {}\n try {\n payload = await req.json()\n } catch {\n // Default to reindex\n }\n\n const action: ReindexAction =\n payload.action === 'clear' ? 'clear' :\n payload.action === 'recreate' ? 'recreate' : 'reindex'\n const entityId = typeof payload.entityId === 'string' ? payload.entityId : undefined\n // Use queue by default (requires queue workers to be running), can be disabled with useQueue: false\n const useQueue = payload.useQueue !== false\n let keepLockForQueuedWorkers = false\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const progressService = container.resolve('progressService') as ProgressService\n const db = (em as unknown as { getKysely: () => Kysely<any> }).getKysely()\n\n // Check if another fulltext reindex operation is already in progress\n const existingLock = await getReindexLockStatus(db, tenantId, { type: 'fulltext' })\n if (existingLock) {\n const startedAt = new Date(existingLock.startedAt)\n return NextResponse.json(\n {\n error: t('search.api.errors.reindexInProgress', 'A reindex operation is already in progress'),\n lock: {\n type: existingLock.type,\n action: existingLock.action,\n startedAt: existingLock.startedAt,\n elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 60000),\n processedCount: existingLock.processedCount,\n totalCount: existingLock.totalCount,\n },\n },\n { status: 409 }\n )\n }\n\n // Acquire lock before starting the operation\n const { acquired: lockAcquired } = await acquireReindexLock(db, {\n type: 'fulltext',\n action,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n })\n\n if (!lockAcquired) {\n return NextResponse.json(\n { error: t('search.api.errors.lockFailed', 'Failed to acquire reindex lock') },\n { status: 409 }\n )\n }\n\n try {\n // Get all search strategies\n const searchStrategies = (container.resolve('searchStrategies') as StrategyWithStats[] | undefined) ?? []\n\n // Find a strategy that supports index management (clear/recreate)\n const indexableStrategy = searchStrategies.find(\n (s) => typeof s.clearIndex === 'function' || typeof s.recreateIndex === 'function'\n )\n\n if (!indexableStrategy) {\n return toJson(\n { error: t('search.api.errors.noIndexableStrategy', 'No indexable search strategy is configured') },\n { status: 503 }\n )\n }\n\n // Check if strategy is available\n const isAvailable = await indexableStrategy.isAvailable()\n if (!isAvailable) {\n return toJson(\n { error: t('search.api.errors.strategyUnavailable', 'Search strategy is not available') },\n { status: 503 }\n )\n }\n\n // Perform the requested action\n if (action === 'reindex') {\n // Full reindex: recreate index and re-index all data\n const searchIndexer = container.resolve('searchIndexer') as SearchIndexer | undefined\n if (!searchIndexer) {\n return toJson(\n { error: t('search.api.errors.indexerUnavailable', 'Search indexer is not available') },\n { status: 503 }\n )\n }\n\n let result\n const orgId = typeof auth.orgId === 'string' ? auth.orgId : null\n\n // Debug: List enabled entities\n const enabledEntities = searchIndexer.listEnabledEntities()\n searchDebug('search.reindex', 'Starting reindex', {\n tenantId: tenantId,\n orgId,\n enabledEntities,\n entityId: entityId ?? 'all',\n useQueue,\n })\n\n // Log reindex started\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: entityId\n ? `Starting Meilisearch reindex for ${entityId}`\n : `Starting Meilisearch reindex for all entities (${enabledEntities.join(', ')})`,\n entityType: entityId ?? null,\n tenantId: tenantId,\n organizationId: orgId,\n details: { enabledEntities, useQueue },\n },\n )\n\n if (entityId) {\n // Reindex specific entity\n result = await searchIndexer.reindexEntityToFulltext({\n entityId: entityId as EntityId,\n tenantId: tenantId,\n organizationId: orgId,\n recreateIndex: true,\n useQueue,\n onProgress: (progress) => {\n searchDebug('search.reindex', 'Progress', progress)\n // Note: Heartbeat is updated by workers during job processing, not during enqueueing\n },\n })\n searchDebug('search.reindex', 'Reindexed entity to Meilisearch', {\n entityId,\n tenantId: tenantId,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n errors: result.errors,\n })\n\n // Log to indexer status logs\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: useQueue\n ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of ${entityId}`\n : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${entityId}`,\n entityType: entityId,\n tenantId: tenantId,\n organizationId: orgId,\n details: {\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n useQueue,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n },\n )\n\n // Log any batch errors to error logs\n for (const err of result.errors) {\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error: new Error(err.error),\n entityType: err.entityId,\n tenantId: tenantId,\n organizationId: orgId,\n payload: { action, useQueue },\n },\n )\n }\n } else {\n // Reindex all entities\n result = await searchIndexer.reindexAllToFulltext({\n tenantId: tenantId,\n organizationId: orgId,\n recreateIndex: true,\n useQueue,\n onProgress: (progress) => {\n searchDebug('search.reindex', 'Progress', progress)\n // Note: Heartbeat is updated by workers during job processing, not during enqueueing\n },\n })\n searchDebug('search.reindex', 'Reindexed all entities to Meilisearch', {\n tenantId: tenantId,\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n errors: result.errors,\n })\n\n // Log to indexer status logs\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: useQueue\n ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of all entities`\n : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${result.entitiesProcessed} entities`,\n tenantId: tenantId,\n organizationId: orgId,\n details: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued,\n useQueue,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n },\n )\n\n // Log any batch errors to error logs\n for (const err of result.errors) {\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error: new Error(err.error),\n entityType: err.entityId,\n tenantId: tenantId,\n organizationId: orgId,\n payload: { action, useQueue },\n },\n )\n }\n }\n\n await ensureReindexProgressJob({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub ?? null,\n totalCount: result.recordsIndexed,\n description: entityId\n ? `Reindex ${entityId} (${useQueue ? 'queued' : 'sync'})`\n : `Reindex all entities (${useQueue ? 'queued' : 'sync'})`,\n })\n const jobsEnqueued = result.jobsEnqueued ?? 0\n if (!useQueue || jobsEnqueued === 0) {\n await completeReindexProgress({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n resultSummary: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued ?? 0,\n errors: result.errors.length,\n },\n })\n } else {\n keepLockForQueuedWorkers = true\n }\n\n // Get updated stats from all strategies\n const stats = await collectStrategyStats(searchStrategies, tenantId)\n\n return toJson({\n ok: result.success,\n action,\n entityId: entityId ?? null,\n useQueue,\n result: {\n entitiesProcessed: result.entitiesProcessed,\n recordsIndexed: result.recordsIndexed,\n jobsEnqueued: result.jobsEnqueued ?? 0,\n errors: result.errors.length > 0 ? result.errors : undefined,\n },\n stats,\n })\n } else if (entityId) {\n // Purge specific entity\n await indexableStrategy.purge?.(entityId as EntityId, tenantId)\n searchDebug('search.reindex', 'Purged entity', { strategyId: indexableStrategy.id, entityId, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: `Purged entity ${entityId} from Meilisearch`,\n entityType: entityId,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n } else if (action === 'clear') {\n // Clear all documents but keep index\n if (indexableStrategy.clearIndex) {\n await indexableStrategy.clearIndex(tenantId)\n searchDebug('search.reindex', 'Cleared index', { strategyId: indexableStrategy.id, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: 'Cleared all documents from Meilisearch index',\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n }\n } else {\n // Recreate the entire index\n if (indexableStrategy.recreateIndex) {\n await indexableStrategy.recreateIndex(tenantId)\n searchDebug('search.reindex', 'Recreated index', { strategyId: indexableStrategy.id, tenantId: tenantId })\n\n await recordIndexerLog(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n message: 'Recreated Meilisearch index',\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n },\n )\n }\n }\n\n // Get updated stats from all strategies\n const stats = await collectStrategyStats(searchStrategies, tenantId)\n\n return toJson({\n ok: true,\n action,\n entityId: entityId ?? null,\n stats,\n })\n } catch (error: unknown) {\n // Log full error details server-side only\n searchError('search.reindex', 'Failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n tenantId: tenantId,\n })\n\n // Record error to indexer error logs\n await recordIndexerError(\n { em },\n {\n source: 'fulltext',\n handler: 'api:search.reindex',\n error,\n entityType: entityId ?? null,\n tenantId: tenantId,\n organizationId: auth.orgId ?? null,\n payload: { action, entityId, useQueue },\n },\n )\n\n await failReindexProgress({\n em,\n progressService,\n type: 'fulltext',\n tenantId,\n organizationId: auth.orgId ?? null,\n errorMessage: error instanceof Error ? error.message : 'Fulltext reindex failed',\n })\n\n // Return generic message to client - don't expose internal error details\n return toJson(\n { error: t('search.api.errors.reindexFailed', 'Reindex operation failed. Please try again or contact support.') },\n { status: 500 }\n )\n } finally {\n // Only clear lock immediately if NOT using queue mode\n // When using queue mode, workers update heartbeat and stale detection handles cleanup\n if (!keepLockForQueuedWorkers) {\n await clearReindexLock(db, tenantId, 'fulltext', auth.orgId ?? null)\n }\n\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n\nexport const openApi = reindexOpenApi\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["stats"]
|
|
7
7
|
}
|
|
@@ -78,6 +78,14 @@ function SearchSettingsPageClient() {
|
|
|
78
78
|
} catch {
|
|
79
79
|
}
|
|
80
80
|
}, []);
|
|
81
|
+
const hasActiveReindexLock = Boolean(settings?.fulltextReindexLock || settings?.vectorReindexLock);
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
if (!hasActiveReindexLock) return;
|
|
84
|
+
const interval = setInterval(() => {
|
|
85
|
+
void refreshStatsOnly();
|
|
86
|
+
}, 5e3);
|
|
87
|
+
return () => clearInterval(interval);
|
|
88
|
+
}, [hasActiveReindexLock, refreshStatsOnly]);
|
|
81
89
|
const refreshEmbeddingStatsOnly = React.useCallback(async () => {
|
|
82
90
|
try {
|
|
83
91
|
const body = await readApiResultOrThrow(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/search/frontend/components/SearchSettingsPageClient.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { GlobalSearchSection } from './sections/GlobalSearchSection'\nimport { FulltextSearchSection } from './sections/FulltextSearchSection'\nimport { VectorSearchSection } from './sections/VectorSearchSection'\n\n// Types\ntype StrategyStatus = {\n id: string\n name: string\n priority: number\n available: boolean\n}\n\ntype FulltextStats = {\n numberOfDocuments: number\n isIndexing: boolean\n fieldDistribution: Record<string, number>\n}\n\ntype ReindexLock = {\n type: 'fulltext' | 'vector'\n action: string\n startedAt: string\n elapsedMinutes: number\n}\n\ntype SearchSettings = {\n strategies: StrategyStatus[]\n fulltextConfigured: boolean\n fulltextStats: FulltextStats | null\n vectorConfigured: boolean\n tokensEnabled: boolean\n defaultStrategies: string[]\n reindexLock: ReindexLock | null\n fulltextReindexLock: ReindexLock | null\n vectorReindexLock: ReindexLock | null\n}\n\ntype SettingsResponse = {\n settings?: SearchSettings\n error?: string\n}\n\n// Embedding types\ntype EmbeddingProviderId = 'openai' | 'google' | 'mistral' | 'cohere' | 'bedrock' | 'ollama'\n\ntype EmbeddingProviderConfig = {\n providerId: EmbeddingProviderId\n model: string\n dimension: number\n outputDimensionality?: number\n baseUrl?: string\n updatedAt: string\n}\n\ntype EmbeddingSettings = {\n openaiConfigured: boolean\n autoIndexingEnabled: boolean\n autoIndexingLocked: boolean\n lockReason: string | null\n embeddingConfig: EmbeddingProviderConfig | null\n configuredProviders: EmbeddingProviderId[]\n indexedDimension: number | null\n reindexRequired: boolean\n documentCount: number | null\n}\n\ntype EmbeddingSettingsResponse = {\n settings?: EmbeddingSettings\n error?: string\n}\n\n// Full-text search config types\ntype FulltextEnvVarStatus = {\n set: boolean\n hint: string\n}\n\ntype FulltextOptionalEnvVarStatus = {\n set: boolean\n value?: string | boolean\n default?: string | boolean\n hint: string\n}\n\ntype FulltextConfigResponse = {\n driver: 'meilisearch' | null\n configured: boolean\n envVars: {\n MEILISEARCH_HOST: FulltextEnvVarStatus\n MEILISEARCH_API_KEY: FulltextEnvVarStatus\n }\n optionalEnvVars: {\n MEILISEARCH_INDEX_PREFIX: FulltextOptionalEnvVarStatus\n SEARCH_EXCLUDE_ENCRYPTED_FIELDS: FulltextOptionalEnvVarStatus\n }\n}\n\n// Vector store driver types\ntype VectorDriverId = 'pgvector' | 'qdrant' | 'chromadb'\n\ntype VectorDriverEnvVar = {\n name: string\n set: boolean\n hint: string\n}\n\ntype VectorDriverStatus = {\n id: VectorDriverId\n name: string\n configured: boolean\n implemented: boolean\n envVars: VectorDriverEnvVar[]\n}\n\ntype VectorStoreConfigResponse = {\n currentDriver: VectorDriverId\n configured: boolean\n drivers: VectorDriverStatus[]\n}\n\nconst normalizeErrorMessage = (error: unknown, fallback: string): string => {\n if (typeof error === 'string' && error.trim().length) return error.trim()\n if (error instanceof Error && error.message.trim().length) return error.message.trim()\n return fallback\n}\n\nexport function SearchSettingsPageClient() {\n const t = useT()\n\n // Main settings state\n const [settings, setSettings] = React.useState<SearchSettings | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n // Embedding settings state\n const [embeddingSettings, setEmbeddingSettings] = React.useState<EmbeddingSettings | null>(null)\n const [embeddingLoading, setEmbeddingLoading] = React.useState(true)\n\n // Global search settings state\n const [globalSearchStrategies, setGlobalSearchStrategies] = React.useState<Set<string>>(() => new Set(['fulltext', 'vector', 'tokens']))\n const [globalSearchInitial, setGlobalSearchInitial] = React.useState<Set<string>>(() => new Set(['fulltext', 'vector', 'tokens']))\n const [globalSearchLoading, setGlobalSearchLoading] = React.useState(true)\n const [globalSearchSaving, setGlobalSearchSaving] = React.useState(false)\n\n // Full-text search config state\n const [fulltextConfig, setFulltextConfig] = React.useState<FulltextConfigResponse | null>(null)\n const [fulltextConfigLoading, setFulltextConfigLoading] = React.useState(true)\n\n // Vector store config state\n const [vectorStoreConfig, setVectorStoreConfig] = React.useState<VectorStoreConfigResponse | null>(null)\n const [vectorStoreConfigLoading, setVectorStoreConfigLoading] = React.useState(true)\n\n // Fetch main settings\n const fetchSettings = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const body = await readApiResultOrThrow<SettingsResponse>(\n '/api/search/settings',\n undefined,\n { errorMessage: t('search.settings.errorLabel', 'Failed to load settings'), allowNullResult: true },\n )\n if (body?.settings) {\n setSettings(body.settings)\n } else {\n setSettings({\n strategies: [],\n fulltextConfigured: false,\n fulltextStats: null,\n vectorConfigured: false,\n tokensEnabled: true,\n defaultStrategies: [],\n reindexLock: null,\n fulltextReindexLock: null,\n vectorReindexLock: null,\n })\n }\n } catch (err) {\n const message = normalizeErrorMessage(err, t('search.settings.errorLabel', 'Failed to load settings'))\n setError(message)\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n fetchSettings()\n }, [fetchSettings])\n\n // Lightweight stats refresh for polling during reindex\n const refreshStatsOnly = React.useCallback(async () => {\n try {\n const body = await readApiResultOrThrow<SettingsResponse>(\n '/api/search/settings',\n { cache: 'no-store' },\n { errorMessage: '', allowNullResult: true },\n )\n if (body?.settings) {\n setSettings(body.settings)\n }\n } catch {\n // Silently ignore errors during polling\n }\n }, [])\n\n // Lightweight embedding stats refresh\n const refreshEmbeddingStatsOnly = React.useCallback(async () => {\n try {\n const body = await readApiResultOrThrow<EmbeddingSettingsResponse>(\n '/api/search/embeddings',\n { cache: 'no-store' },\n { errorMessage: '', allowNullResult: true },\n )\n if (body?.settings) {\n setEmbeddingSettings(body.settings)\n }\n } catch {\n // Silently ignore errors during polling\n }\n }, [])\n\n useAppEvent('progress.job.updated', () => {\n void refreshStatsOnly()\n void refreshEmbeddingStatsOnly()\n }, [refreshStatsOnly, refreshEmbeddingStatsOnly])\n\n useAppEvent('progress.job.completed', () => {\n void refreshStatsOnly()\n void refreshEmbeddingStatsOnly()\n }, [refreshStatsOnly, refreshEmbeddingStatsOnly])\n\n useAppEvent('om:bridge:reconnected', () => {\n void refreshStatsOnly()\n void refreshEmbeddingStatsOnly()\n }, [refreshStatsOnly, refreshEmbeddingStatsOnly])\n\n // Fetch embedding settings\n const fetchEmbeddingSettings = React.useCallback(async () => {\n setEmbeddingLoading(true)\n try {\n const body = await readApiResultOrThrow<EmbeddingSettingsResponse>(\n '/api/search/embeddings',\n undefined,\n { errorMessage: t('search.settings.errors.loadFailed', 'Failed to load settings'), allowNullResult: true },\n )\n if (body?.settings) {\n setEmbeddingSettings(body.settings)\n } else {\n setEmbeddingSettings({\n openaiConfigured: false,\n autoIndexingEnabled: true,\n autoIndexingLocked: false,\n lockReason: null,\n embeddingConfig: null,\n configuredProviders: [],\n indexedDimension: null,\n reindexRequired: false,\n documentCount: null,\n })\n }\n } catch {\n // Error already handled\n } finally {\n setEmbeddingLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n fetchEmbeddingSettings()\n }, [fetchEmbeddingSettings])\n\n // Fetch global search settings\n const fetchGlobalSearchSettings = React.useCallback(async () => {\n setGlobalSearchLoading(true)\n try {\n const response = await fetch('/api/search/settings/global-search')\n if (response.ok) {\n const body = await response.json() as { enabledStrategies?: string[] }\n if (body.enabledStrategies && Array.isArray(body.enabledStrategies) && body.enabledStrategies.length > 0) {\n const strategies = new Set(body.enabledStrategies)\n setGlobalSearchStrategies(strategies)\n setGlobalSearchInitial(new Set(strategies))\n }\n }\n } catch {\n // Silently use defaults\n } finally {\n setGlobalSearchLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n fetchGlobalSearchSettings()\n }, [fetchGlobalSearchSettings])\n\n // Fetch fulltext config\n const fetchFulltextConfig = React.useCallback(async () => {\n setFulltextConfigLoading(true)\n try {\n const response = await fetch('/api/search/settings/fulltext')\n if (response.ok) {\n const body = await response.json() as FulltextConfigResponse\n setFulltextConfig(body)\n }\n } catch {\n // Silently use null\n } finally {\n setFulltextConfigLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n fetchFulltextConfig()\n }, [fetchFulltextConfig])\n\n // Fetch vector store config\n const fetchVectorStoreConfig = React.useCallback(async () => {\n setVectorStoreConfigLoading(true)\n try {\n const response = await fetch('/api/search/settings/vector-store')\n if (response.ok) {\n const body = await response.json() as VectorStoreConfigResponse\n setVectorStoreConfig(body)\n }\n } catch {\n // Silently use null\n } finally {\n setVectorStoreConfigLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n fetchVectorStoreConfig()\n }, [fetchVectorStoreConfig])\n\n // Global search settings handlers - auto-save on toggle\n const toggleGlobalSearchStrategy = React.useCallback(async (strategyId: string) => {\n const newStrategies = new Set(globalSearchStrategies)\n if (newStrategies.has(strategyId)) {\n if (newStrategies.size > 1) {\n newStrategies.delete(strategyId)\n } else {\n return\n }\n } else {\n newStrategies.add(strategyId)\n }\n\n setGlobalSearchStrategies(newStrategies)\n setGlobalSearchSaving(true)\n\n try {\n const response = await fetch('/api/search/settings/global-search', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ enabledStrategies: Array.from(newStrategies) }),\n })\n\n if (!response.ok) {\n const body = await response.json().catch(() => ({})) as { error?: string }\n throw new Error(body.error || t('search.settings.globalSearch.saveError', 'Failed to save settings'))\n }\n\n setGlobalSearchInitial(new Set(newStrategies))\n } catch (err) {\n setGlobalSearchStrategies(globalSearchInitial)\n flash(normalizeErrorMessage(err, t('search.settings.globalSearch.saveError', 'Failed to save settings')), 'error')\n } finally {\n setGlobalSearchSaving(false)\n }\n }, [globalSearchStrategies, globalSearchInitial, t])\n\n // Callbacks for section components\n const handleFulltextStatsUpdate = React.useCallback((stats: FulltextStats | null) => {\n setSettings(prev => prev ? { ...prev, fulltextStats: stats } : prev)\n }, [])\n\n const handleEmbeddingSettingsUpdate = React.useCallback((newSettings: EmbeddingSettings) => {\n setEmbeddingSettings(newSettings)\n }, [])\n\n return (\n <div className=\"flex flex-col gap-6\">\n {/* Header */}\n <div className=\"space-y-1\">\n <h1 className=\"text-2xl font-bold\">{t('search.settings.pageTitle', 'Search Settings')}</h1>\n <p className=\"text-muted-foreground\">{t('search.settings.pageDescription', 'Configure search strategies and view their availability.')}</p>\n </div>\n\n {/* Section 1: Global Search Settings */}\n <GlobalSearchSection\n loading={globalSearchLoading}\n saving={globalSearchSaving}\n strategies={globalSearchStrategies}\n fulltextConfigured={settings?.fulltextConfigured ?? false}\n vectorConfigured={settings?.vectorConfigured ?? false}\n onToggleStrategy={toggleGlobalSearchStrategy}\n />\n\n {/* Section 2: Full-Text Search (with tabs) */}\n <FulltextSearchSection\n fulltextConfig={fulltextConfig}\n fulltextConfigLoading={fulltextConfigLoading}\n fulltextStats={settings?.fulltextStats ?? null}\n fulltextReindexLock={settings?.fulltextReindexLock ?? null}\n loading={loading}\n onStatsUpdate={handleFulltextStatsUpdate}\n onRefresh={fetchSettings}\n />\n\n {/* Section 3: Vector Search (with tabs) */}\n <VectorSearchSection\n embeddingSettings={embeddingSettings}\n embeddingLoading={embeddingLoading}\n vectorStoreConfig={vectorStoreConfig}\n vectorStoreConfigLoading={vectorStoreConfigLoading}\n vectorReindexLock={settings?.vectorReindexLock ?? null}\n onEmbeddingSettingsUpdate={handleEmbeddingSettingsUpdate}\n onRefreshEmbeddings={fetchEmbeddingSettings}\n />\n\n {/* Refresh Button */}\n <div className=\"flex items-center gap-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => fetchSettings()}\n disabled={loading}\n >\n {loading ? (\n <>\n <Spinner size=\"sm\" className=\"mr-2\" />\n {t('search.settings.loadingLabel', 'Loading settings...')}\n </>\n ) : (\n t('search.settings.refreshLabel', 'Refresh')\n )}\n </Button>\n {error && <span className=\"text-sm text-destructive\">{error}</span>}\n </div>\n </div>\n )\n}\n\nexport default SearchSettingsPageClient\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'\nimport { GlobalSearchSection } from './sections/GlobalSearchSection'\nimport { FulltextSearchSection } from './sections/FulltextSearchSection'\nimport { VectorSearchSection } from './sections/VectorSearchSection'\n\n// Types\ntype StrategyStatus = {\n id: string\n name: string\n priority: number\n available: boolean\n}\n\ntype FulltextStats = {\n numberOfDocuments: number\n isIndexing: boolean\n fieldDistribution: Record<string, number>\n}\n\ntype ReindexLock = {\n type: 'fulltext' | 'vector'\n action: string\n startedAt: string\n elapsedMinutes: number\n}\n\ntype SearchSettings = {\n strategies: StrategyStatus[]\n fulltextConfigured: boolean\n fulltextStats: FulltextStats | null\n vectorConfigured: boolean\n tokensEnabled: boolean\n defaultStrategies: string[]\n reindexLock: ReindexLock | null\n fulltextReindexLock: ReindexLock | null\n vectorReindexLock: ReindexLock | null\n}\n\ntype SettingsResponse = {\n settings?: SearchSettings\n error?: string\n}\n\n// Embedding types\ntype EmbeddingProviderId = 'openai' | 'google' | 'mistral' | 'cohere' | 'bedrock' | 'ollama'\n\ntype EmbeddingProviderConfig = {\n providerId: EmbeddingProviderId\n model: string\n dimension: number\n outputDimensionality?: number\n baseUrl?: string\n updatedAt: string\n}\n\ntype EmbeddingSettings = {\n openaiConfigured: boolean\n autoIndexingEnabled: boolean\n autoIndexingLocked: boolean\n lockReason: string | null\n embeddingConfig: EmbeddingProviderConfig | null\n configuredProviders: EmbeddingProviderId[]\n indexedDimension: number | null\n reindexRequired: boolean\n documentCount: number | null\n}\n\ntype EmbeddingSettingsResponse = {\n settings?: EmbeddingSettings\n error?: string\n}\n\n// Full-text search config types\ntype FulltextEnvVarStatus = {\n set: boolean\n hint: string\n}\n\ntype FulltextOptionalEnvVarStatus = {\n set: boolean\n value?: string | boolean\n default?: string | boolean\n hint: string\n}\n\ntype FulltextConfigResponse = {\n driver: 'meilisearch' | null\n configured: boolean\n envVars: {\n MEILISEARCH_HOST: FulltextEnvVarStatus\n MEILISEARCH_API_KEY: FulltextEnvVarStatus\n }\n optionalEnvVars: {\n MEILISEARCH_INDEX_PREFIX: FulltextOptionalEnvVarStatus\n SEARCH_EXCLUDE_ENCRYPTED_FIELDS: FulltextOptionalEnvVarStatus\n }\n}\n\n// Vector store driver types\ntype VectorDriverId = 'pgvector' | 'qdrant' | 'chromadb'\n\ntype VectorDriverEnvVar = {\n name: string\n set: boolean\n hint: string\n}\n\ntype VectorDriverStatus = {\n id: VectorDriverId\n name: string\n configured: boolean\n implemented: boolean\n envVars: VectorDriverEnvVar[]\n}\n\ntype VectorStoreConfigResponse = {\n currentDriver: VectorDriverId\n configured: boolean\n drivers: VectorDriverStatus[]\n}\n\nconst normalizeErrorMessage = (error: unknown, fallback: string): string => {\n if (typeof error === 'string' && error.trim().length) return error.trim()\n if (error instanceof Error && error.message.trim().length) return error.message.trim()\n return fallback\n}\n\nexport function SearchSettingsPageClient() {\n const t = useT()\n\n // Main settings state\n const [settings, setSettings] = React.useState<SearchSettings | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n // Embedding settings state\n const [embeddingSettings, setEmbeddingSettings] = React.useState<EmbeddingSettings | null>(null)\n const [embeddingLoading, setEmbeddingLoading] = React.useState(true)\n\n // Global search settings state\n const [globalSearchStrategies, setGlobalSearchStrategies] = React.useState<Set<string>>(() => new Set(['fulltext', 'vector', 'tokens']))\n const [globalSearchInitial, setGlobalSearchInitial] = React.useState<Set<string>>(() => new Set(['fulltext', 'vector', 'tokens']))\n const [globalSearchLoading, setGlobalSearchLoading] = React.useState(true)\n const [globalSearchSaving, setGlobalSearchSaving] = React.useState(false)\n\n // Full-text search config state\n const [fulltextConfig, setFulltextConfig] = React.useState<FulltextConfigResponse | null>(null)\n const [fulltextConfigLoading, setFulltextConfigLoading] = React.useState(true)\n\n // Vector store config state\n const [vectorStoreConfig, setVectorStoreConfig] = React.useState<VectorStoreConfigResponse | null>(null)\n const [vectorStoreConfigLoading, setVectorStoreConfigLoading] = React.useState(true)\n\n // Fetch main settings\n const fetchSettings = React.useCallback(async () => {\n setLoading(true)\n setError(null)\n try {\n const body = await readApiResultOrThrow<SettingsResponse>(\n '/api/search/settings',\n undefined,\n { errorMessage: t('search.settings.errorLabel', 'Failed to load settings'), allowNullResult: true },\n )\n if (body?.settings) {\n setSettings(body.settings)\n } else {\n setSettings({\n strategies: [],\n fulltextConfigured: false,\n fulltextStats: null,\n vectorConfigured: false,\n tokensEnabled: true,\n defaultStrategies: [],\n reindexLock: null,\n fulltextReindexLock: null,\n vectorReindexLock: null,\n })\n }\n } catch (err) {\n const message = normalizeErrorMessage(err, t('search.settings.errorLabel', 'Failed to load settings'))\n setError(message)\n flash(message, 'error')\n } finally {\n setLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n fetchSettings()\n }, [fetchSettings])\n\n // Lightweight stats refresh for polling during reindex\n const refreshStatsOnly = React.useCallback(async () => {\n try {\n const body = await readApiResultOrThrow<SettingsResponse>(\n '/api/search/settings',\n { cache: 'no-store' },\n { errorMessage: '', allowNullResult: true },\n )\n if (body?.settings) {\n setSettings(body.settings)\n }\n } catch {\n // Silently ignore errors during polling\n }\n }, [])\n\n const hasActiveReindexLock = Boolean(settings?.fulltextReindexLock || settings?.vectorReindexLock)\n React.useEffect(() => {\n if (!hasActiveReindexLock) return\n\n const interval = setInterval(() => {\n void refreshStatsOnly()\n }, 5000)\n\n return () => clearInterval(interval)\n }, [hasActiveReindexLock, refreshStatsOnly])\n\n // Lightweight embedding stats refresh\n const refreshEmbeddingStatsOnly = React.useCallback(async () => {\n try {\n const body = await readApiResultOrThrow<EmbeddingSettingsResponse>(\n '/api/search/embeddings',\n { cache: 'no-store' },\n { errorMessage: '', allowNullResult: true },\n )\n if (body?.settings) {\n setEmbeddingSettings(body.settings)\n }\n } catch {\n // Silently ignore errors during polling\n }\n }, [])\n\n useAppEvent('progress.job.updated', () => {\n void refreshStatsOnly()\n void refreshEmbeddingStatsOnly()\n }, [refreshStatsOnly, refreshEmbeddingStatsOnly])\n\n useAppEvent('progress.job.completed', () => {\n void refreshStatsOnly()\n void refreshEmbeddingStatsOnly()\n }, [refreshStatsOnly, refreshEmbeddingStatsOnly])\n\n useAppEvent('om:bridge:reconnected', () => {\n void refreshStatsOnly()\n void refreshEmbeddingStatsOnly()\n }, [refreshStatsOnly, refreshEmbeddingStatsOnly])\n\n // Fetch embedding settings\n const fetchEmbeddingSettings = React.useCallback(async () => {\n setEmbeddingLoading(true)\n try {\n const body = await readApiResultOrThrow<EmbeddingSettingsResponse>(\n '/api/search/embeddings',\n undefined,\n { errorMessage: t('search.settings.errors.loadFailed', 'Failed to load settings'), allowNullResult: true },\n )\n if (body?.settings) {\n setEmbeddingSettings(body.settings)\n } else {\n setEmbeddingSettings({\n openaiConfigured: false,\n autoIndexingEnabled: true,\n autoIndexingLocked: false,\n lockReason: null,\n embeddingConfig: null,\n configuredProviders: [],\n indexedDimension: null,\n reindexRequired: false,\n documentCount: null,\n })\n }\n } catch {\n // Error already handled\n } finally {\n setEmbeddingLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n fetchEmbeddingSettings()\n }, [fetchEmbeddingSettings])\n\n // Fetch global search settings\n const fetchGlobalSearchSettings = React.useCallback(async () => {\n setGlobalSearchLoading(true)\n try {\n const response = await fetch('/api/search/settings/global-search')\n if (response.ok) {\n const body = await response.json() as { enabledStrategies?: string[] }\n if (body.enabledStrategies && Array.isArray(body.enabledStrategies) && body.enabledStrategies.length > 0) {\n const strategies = new Set(body.enabledStrategies)\n setGlobalSearchStrategies(strategies)\n setGlobalSearchInitial(new Set(strategies))\n }\n }\n } catch {\n // Silently use defaults\n } finally {\n setGlobalSearchLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n fetchGlobalSearchSettings()\n }, [fetchGlobalSearchSettings])\n\n // Fetch fulltext config\n const fetchFulltextConfig = React.useCallback(async () => {\n setFulltextConfigLoading(true)\n try {\n const response = await fetch('/api/search/settings/fulltext')\n if (response.ok) {\n const body = await response.json() as FulltextConfigResponse\n setFulltextConfig(body)\n }\n } catch {\n // Silently use null\n } finally {\n setFulltextConfigLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n fetchFulltextConfig()\n }, [fetchFulltextConfig])\n\n // Fetch vector store config\n const fetchVectorStoreConfig = React.useCallback(async () => {\n setVectorStoreConfigLoading(true)\n try {\n const response = await fetch('/api/search/settings/vector-store')\n if (response.ok) {\n const body = await response.json() as VectorStoreConfigResponse\n setVectorStoreConfig(body)\n }\n } catch {\n // Silently use null\n } finally {\n setVectorStoreConfigLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n fetchVectorStoreConfig()\n }, [fetchVectorStoreConfig])\n\n // Global search settings handlers - auto-save on toggle\n const toggleGlobalSearchStrategy = React.useCallback(async (strategyId: string) => {\n const newStrategies = new Set(globalSearchStrategies)\n if (newStrategies.has(strategyId)) {\n if (newStrategies.size > 1) {\n newStrategies.delete(strategyId)\n } else {\n return\n }\n } else {\n newStrategies.add(strategyId)\n }\n\n setGlobalSearchStrategies(newStrategies)\n setGlobalSearchSaving(true)\n\n try {\n const response = await fetch('/api/search/settings/global-search', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ enabledStrategies: Array.from(newStrategies) }),\n })\n\n if (!response.ok) {\n const body = await response.json().catch(() => ({})) as { error?: string }\n throw new Error(body.error || t('search.settings.globalSearch.saveError', 'Failed to save settings'))\n }\n\n setGlobalSearchInitial(new Set(newStrategies))\n } catch (err) {\n setGlobalSearchStrategies(globalSearchInitial)\n flash(normalizeErrorMessage(err, t('search.settings.globalSearch.saveError', 'Failed to save settings')), 'error')\n } finally {\n setGlobalSearchSaving(false)\n }\n }, [globalSearchStrategies, globalSearchInitial, t])\n\n // Callbacks for section components\n const handleFulltextStatsUpdate = React.useCallback((stats: FulltextStats | null) => {\n setSettings(prev => prev ? { ...prev, fulltextStats: stats } : prev)\n }, [])\n\n const handleEmbeddingSettingsUpdate = React.useCallback((newSettings: EmbeddingSettings) => {\n setEmbeddingSettings(newSettings)\n }, [])\n\n return (\n <div className=\"flex flex-col gap-6\">\n {/* Header */}\n <div className=\"space-y-1\">\n <h1 className=\"text-2xl font-bold\">{t('search.settings.pageTitle', 'Search Settings')}</h1>\n <p className=\"text-muted-foreground\">{t('search.settings.pageDescription', 'Configure search strategies and view their availability.')}</p>\n </div>\n\n {/* Section 1: Global Search Settings */}\n <GlobalSearchSection\n loading={globalSearchLoading}\n saving={globalSearchSaving}\n strategies={globalSearchStrategies}\n fulltextConfigured={settings?.fulltextConfigured ?? false}\n vectorConfigured={settings?.vectorConfigured ?? false}\n onToggleStrategy={toggleGlobalSearchStrategy}\n />\n\n {/* Section 2: Full-Text Search (with tabs) */}\n <FulltextSearchSection\n fulltextConfig={fulltextConfig}\n fulltextConfigLoading={fulltextConfigLoading}\n fulltextStats={settings?.fulltextStats ?? null}\n fulltextReindexLock={settings?.fulltextReindexLock ?? null}\n loading={loading}\n onStatsUpdate={handleFulltextStatsUpdate}\n onRefresh={fetchSettings}\n />\n\n {/* Section 3: Vector Search (with tabs) */}\n <VectorSearchSection\n embeddingSettings={embeddingSettings}\n embeddingLoading={embeddingLoading}\n vectorStoreConfig={vectorStoreConfig}\n vectorStoreConfigLoading={vectorStoreConfigLoading}\n vectorReindexLock={settings?.vectorReindexLock ?? null}\n onEmbeddingSettingsUpdate={handleEmbeddingSettingsUpdate}\n onRefreshEmbeddings={fetchEmbeddingSettings}\n />\n\n {/* Refresh Button */}\n <div className=\"flex items-center gap-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => fetchSettings()}\n disabled={loading}\n >\n {loading ? (\n <>\n <Spinner size=\"sm\" className=\"mr-2\" />\n {t('search.settings.loadingLabel', 'Loading settings...')}\n </>\n ) : (\n t('search.settings.refreshLabel', 'Refresh')\n )}\n </Button>\n {error && <span className=\"text-sm text-destructive\">{error}</span>}\n </div>\n </div>\n )\n}\n\nexport default SearchSettingsPageClient\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -103,6 +103,9 @@ async function incrementReindexProgress(params) {
|
|
|
103
103
|
}
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
|
+
async function hasActiveReindexProgress(params) {
|
|
107
|
+
return await findActiveJob(params.em, params.type, params.tenantId, params.organizationId) != null;
|
|
108
|
+
}
|
|
106
109
|
async function completeReindexProgress(params) {
|
|
107
110
|
const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId);
|
|
108
111
|
if (!current) return;
|
|
@@ -146,6 +149,7 @@ export {
|
|
|
146
149
|
completeReindexProgress,
|
|
147
150
|
ensureReindexProgressJob,
|
|
148
151
|
failReindexProgress,
|
|
152
|
+
hasActiveReindexProgress,
|
|
149
153
|
incrementReindexProgress
|
|
150
154
|
};
|
|
151
155
|
//# sourceMappingURL=reindex-progress.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/search/lib/reindex-progress.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { ProgressJob } from '@open-mercato/core/modules/progress/data/entities'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\n\nexport type ReindexProgressType = 'fulltext' | 'vector'\n\nconst REINDEX_JOB_CONFIG: Record<ReindexProgressType, { jobType: string; name: string }> = {\n fulltext: {\n jobType: 'search.reindex.fulltext',\n name: 'Search fulltext reindex',\n },\n vector: {\n jobType: 'search.reindex.vector',\n name: 'Search vector reindex',\n },\n}\n\nfunction buildScopeFilter(\n type: ReindexProgressType,\n tenantId: string,\n organizationId?: string | null,\n) {\n const config = REINDEX_JOB_CONFIG[type]\n return {\n jobType: config.jobType,\n tenantId,\n organizationId: organizationId ?? null,\n }\n}\n\nasync function findActiveJob(\n em: EntityManager,\n type: ReindexProgressType,\n tenantId: string,\n organizationId?: string | null,\n): Promise<ProgressJob | null> {\n return em.findOne(\n ProgressJob,\n {\n ...buildScopeFilter(type, tenantId, organizationId),\n status: { $in: ['pending', 'running'] },\n },\n {\n orderBy: { createdAt: 'DESC' },\n },\n )\n}\n\nexport async function ensureReindexProgressJob(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n totalCount?: number | null\n description?: string | null\n}): Promise<string | null> {\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (current) {\n if (typeof params.totalCount === 'number') {\n await params.progressService.updateProgress(\n current.id,\n { totalCount: params.totalCount },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: params.userId ?? null,\n },\n )\n }\n return current.id\n }\n\n const config = REINDEX_JOB_CONFIG[params.type]\n const created = await params.progressService.createJob(\n {\n jobType: config.jobType,\n name: config.name,\n description: params.description ?? undefined,\n totalCount: params.totalCount ?? undefined,\n cancellable: true,\n meta: {\n source: 'search',\n type: params.type,\n },\n },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: params.userId ?? null,\n },\n )\n await params.progressService.startJob(created.id, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: params.userId ?? null,\n })\n return created.id\n}\n\nexport async function incrementReindexProgress(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n delta: number\n}): Promise<boolean> {\n if (!Number.isFinite(params.delta) || params.delta <= 0) return false\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (!current) return false\n const updated = await params.progressService.incrementProgress(\n current.id,\n params.delta,\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: null,\n },\n )\n if (updated.totalCount && updated.processedCount >= updated.totalCount) {\n await params.progressService.completeJob(\n updated.id,\n {\n resultSummary: {\n processedCount: updated.processedCount,\n totalCount: updated.totalCount,\n },\n },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: null,\n },\n )\n return true\n }\n return false\n}\n\nexport async function completeReindexProgress(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n resultSummary?: Record<string, unknown>\n}): Promise<void> {\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (!current) return\n await params.progressService.completeJob(\n current.id,\n { resultSummary: params.resultSummary ?? {} },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: null,\n },\n )\n}\n\nexport async function failReindexProgress(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n errorMessage: string\n}): Promise<void> {\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (!current) return\n await params.progressService.failJob(\n current.id,\n { errorMessage: params.errorMessage },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: null,\n },\n )\n}\n\nexport async function cancelReindexProgress(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}): Promise<void> {\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (!current) return\n await params.progressService.cancelJob(\n current.id,\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: params.userId ?? null,\n },\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { ProgressJob } from '@open-mercato/core/modules/progress/data/entities'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\n\nexport type ReindexProgressType = 'fulltext' | 'vector'\n\nconst REINDEX_JOB_CONFIG: Record<ReindexProgressType, { jobType: string; name: string }> = {\n fulltext: {\n jobType: 'search.reindex.fulltext',\n name: 'Search fulltext reindex',\n },\n vector: {\n jobType: 'search.reindex.vector',\n name: 'Search vector reindex',\n },\n}\n\nfunction buildScopeFilter(\n type: ReindexProgressType,\n tenantId: string,\n organizationId?: string | null,\n) {\n const config = REINDEX_JOB_CONFIG[type]\n return {\n jobType: config.jobType,\n tenantId,\n organizationId: organizationId ?? null,\n }\n}\n\nasync function findActiveJob(\n em: EntityManager,\n type: ReindexProgressType,\n tenantId: string,\n organizationId?: string | null,\n): Promise<ProgressJob | null> {\n return em.findOne(\n ProgressJob,\n {\n ...buildScopeFilter(type, tenantId, organizationId),\n status: { $in: ['pending', 'running'] },\n },\n {\n orderBy: { createdAt: 'DESC' },\n },\n )\n}\n\nexport async function ensureReindexProgressJob(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n totalCount?: number | null\n description?: string | null\n}): Promise<string | null> {\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (current) {\n if (typeof params.totalCount === 'number') {\n await params.progressService.updateProgress(\n current.id,\n { totalCount: params.totalCount },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: params.userId ?? null,\n },\n )\n }\n return current.id\n }\n\n const config = REINDEX_JOB_CONFIG[params.type]\n const created = await params.progressService.createJob(\n {\n jobType: config.jobType,\n name: config.name,\n description: params.description ?? undefined,\n totalCount: params.totalCount ?? undefined,\n cancellable: true,\n meta: {\n source: 'search',\n type: params.type,\n },\n },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: params.userId ?? null,\n },\n )\n await params.progressService.startJob(created.id, {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: params.userId ?? null,\n })\n return created.id\n}\n\nexport async function incrementReindexProgress(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n delta: number\n}): Promise<boolean> {\n if (!Number.isFinite(params.delta) || params.delta <= 0) return false\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (!current) return false\n const updated = await params.progressService.incrementProgress(\n current.id,\n params.delta,\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: null,\n },\n )\n if (updated.totalCount && updated.processedCount >= updated.totalCount) {\n await params.progressService.completeJob(\n updated.id,\n {\n resultSummary: {\n processedCount: updated.processedCount,\n totalCount: updated.totalCount,\n },\n },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: null,\n },\n )\n return true\n }\n return false\n}\n\nexport async function hasActiveReindexProgress(params: {\n em: EntityManager\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n}): Promise<boolean> {\n return (await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)) != null\n}\n\nexport async function completeReindexProgress(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n resultSummary?: Record<string, unknown>\n}): Promise<void> {\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (!current) return\n await params.progressService.completeJob(\n current.id,\n { resultSummary: params.resultSummary ?? {} },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: null,\n },\n )\n}\n\nexport async function failReindexProgress(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n errorMessage: string\n}): Promise<void> {\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (!current) return\n await params.progressService.failJob(\n current.id,\n { errorMessage: params.errorMessage },\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: null,\n },\n )\n}\n\nexport async function cancelReindexProgress(params: {\n em: EntityManager\n progressService: ProgressService\n type: ReindexProgressType\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}): Promise<void> {\n const current = await findActiveJob(params.em, params.type, params.tenantId, params.organizationId)\n if (!current) return\n await params.progressService.cancelJob(\n current.id,\n {\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n userId: params.userId ?? null,\n },\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,13 +3,48 @@ import { recordIndexerLog } from "@open-mercato/shared/lib/indexers/status-log";
|
|
|
3
3
|
import { recordIndexerError } from "@open-mercato/shared/lib/indexers/error-log";
|
|
4
4
|
import { searchDebug, searchDebugWarn, searchError } from "../../../lib/debug.js";
|
|
5
5
|
import { clearReindexLock, updateReindexProgress } from "../lib/reindex-lock.js";
|
|
6
|
-
import { incrementReindexProgress } from "../lib/reindex-progress.js";
|
|
6
|
+
import { hasActiveReindexProgress, incrementReindexProgress } from "../lib/reindex-progress.js";
|
|
7
7
|
const DEFAULT_CONCURRENCY = 2;
|
|
8
8
|
const envConcurrency = process.env.WORKERS_FULLTEXT_INDEXING_CONCURRENCY;
|
|
9
9
|
const metadata = {
|
|
10
10
|
queue: FULLTEXT_INDEXING_QUEUE_NAME,
|
|
11
11
|
concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY
|
|
12
12
|
};
|
|
13
|
+
async function advanceFulltextReindexProgress(params) {
|
|
14
|
+
if (!Number.isFinite(params.delta) || params.delta <= 0) return;
|
|
15
|
+
if (params.progressService && params.em) {
|
|
16
|
+
const hasActiveProgress = await hasActiveReindexProgress({
|
|
17
|
+
em: params.em,
|
|
18
|
+
type: "fulltext",
|
|
19
|
+
tenantId: params.tenantId,
|
|
20
|
+
organizationId: params.organizationId ?? null
|
|
21
|
+
});
|
|
22
|
+
if (!hasActiveProgress) {
|
|
23
|
+
if (params.db) {
|
|
24
|
+
await clearReindexLock(params.db, params.tenantId, "fulltext", params.organizationId ?? null);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (params.db) {
|
|
29
|
+
await updateReindexProgress(params.db, params.tenantId, "fulltext", params.delta, params.organizationId ?? null);
|
|
30
|
+
}
|
|
31
|
+
const completed = await incrementReindexProgress({
|
|
32
|
+
em: params.em,
|
|
33
|
+
progressService: params.progressService,
|
|
34
|
+
type: "fulltext",
|
|
35
|
+
tenantId: params.tenantId,
|
|
36
|
+
organizationId: params.organizationId ?? null,
|
|
37
|
+
delta: params.delta
|
|
38
|
+
});
|
|
39
|
+
if (completed && params.db) {
|
|
40
|
+
await clearReindexLock(params.db, params.tenantId, "fulltext", params.organizationId ?? null);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (params.db) {
|
|
45
|
+
await updateReindexProgress(params.db, params.tenantId, "fulltext", params.delta, params.organizationId ?? null);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
13
48
|
async function handleFulltextIndexJob(job, jobCtx, ctx) {
|
|
14
49
|
const { jobType, tenantId } = job.payload;
|
|
15
50
|
if (!tenantId) {
|
|
@@ -132,22 +167,14 @@ async function handleFulltextIndexJob(job, jobCtx, ctx) {
|
|
|
132
167
|
});
|
|
133
168
|
}
|
|
134
169
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
tenantId,
|
|
144
|
-
organizationId: organizationId ?? null,
|
|
145
|
-
delta: successCount
|
|
146
|
-
});
|
|
147
|
-
if (completed && db) {
|
|
148
|
-
await clearReindexLock(db, tenantId, "fulltext", organizationId ?? null);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
170
|
+
await advanceFulltextReindexProgress({
|
|
171
|
+
db,
|
|
172
|
+
em,
|
|
173
|
+
progressService,
|
|
174
|
+
tenantId,
|
|
175
|
+
organizationId: organizationId ?? null,
|
|
176
|
+
delta: records.length
|
|
177
|
+
});
|
|
151
178
|
searchDebug("fulltext-index.worker", "Batch indexed to fulltext", {
|
|
152
179
|
jobId: jobCtx.jobId,
|
|
153
180
|
tenantId,
|