@open-mercato/search 0.4.2-canary-c02407ff85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +678 -0
- package/build.mjs +92 -0
- package/dist/di.js +157 -0
- package/dist/di.js.map +7 -0
- package/dist/fulltext/drivers/index.js +21 -0
- package/dist/fulltext/drivers/index.js.map +7 -0
- package/dist/fulltext/drivers/meilisearch/index.js +320 -0
- package/dist/fulltext/drivers/meilisearch/index.js.map +7 -0
- package/dist/fulltext/index.js +7 -0
- package/dist/fulltext/index.js.map +7 -0
- package/dist/fulltext/types.js +1 -0
- package/dist/fulltext/types.js.map +7 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +7 -0
- package/dist/indexer/index.js +8 -0
- package/dist/indexer/index.js.map +7 -0
- package/dist/indexer/search-indexer.js +848 -0
- package/dist/indexer/search-indexer.js.map +7 -0
- package/dist/indexer/subscribers/delete.js +41 -0
- package/dist/indexer/subscribers/delete.js.map +7 -0
- package/dist/lib/debug.js +34 -0
- package/dist/lib/debug.js.map +7 -0
- package/dist/lib/fallback-presenter.js +107 -0
- package/dist/lib/fallback-presenter.js.map +7 -0
- package/dist/lib/field-policy.js +75 -0
- package/dist/lib/field-policy.js.map +7 -0
- package/dist/lib/index.js +19 -0
- package/dist/lib/index.js.map +7 -0
- package/dist/lib/merger.js +93 -0
- package/dist/lib/merger.js.map +7 -0
- package/dist/lib/presenter-enricher.js +192 -0
- package/dist/lib/presenter-enricher.js.map +7 -0
- package/dist/modules/search/acl.js +14 -0
- package/dist/modules/search/acl.js.map +7 -0
- package/dist/modules/search/ai-tools.js +284 -0
- package/dist/modules/search/ai-tools.js.map +7 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js +65 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +7 -0
- package/dist/modules/search/api/embeddings/reindex/route.js +165 -0
- package/dist/modules/search/api/embeddings/reindex/route.js.map +7 -0
- package/dist/modules/search/api/embeddings/route.js +246 -0
- package/dist/modules/search/api/embeddings/route.js.map +7 -0
- package/dist/modules/search/api/index/route.js +245 -0
- package/dist/modules/search/api/index/route.js.map +7 -0
- package/dist/modules/search/api/reindex/cancel/route.js +65 -0
- package/dist/modules/search/api/reindex/cancel/route.js.map +7 -0
- package/dist/modules/search/api/reindex/route.js +332 -0
- package/dist/modules/search/api/reindex/route.js.map +7 -0
- package/dist/modules/search/api/search/global/route.js +100 -0
- package/dist/modules/search/api/search/global/route.js.map +7 -0
- package/dist/modules/search/api/search/route.js +101 -0
- package/dist/modules/search/api/search/route.js.map +7 -0
- package/dist/modules/search/api/settings/fulltext/route.js +55 -0
- package/dist/modules/search/api/settings/fulltext/route.js.map +7 -0
- package/dist/modules/search/api/settings/global-search/route.js +80 -0
- package/dist/modules/search/api/settings/global-search/route.js.map +7 -0
- package/dist/modules/search/api/settings/route.js +118 -0
- package/dist/modules/search/api/settings/route.js.map +7 -0
- package/dist/modules/search/api/settings/vector-store/route.js +77 -0
- package/dist/modules/search/api/settings/vector-store/route.js.map +7 -0
- package/dist/modules/search/backend/config/search/page.js +10 -0
- package/dist/modules/search/backend/config/search/page.js.map +7 -0
- package/dist/modules/search/backend/config/search/page.meta.js +24 -0
- package/dist/modules/search/backend/config/search/page.meta.js.map +7 -0
- package/dist/modules/search/cli.js +698 -0
- package/dist/modules/search/cli.js.map +7 -0
- package/dist/modules/search/di.js +32 -0
- package/dist/modules/search/di.js.map +7 -0
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js +357 -0
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +7 -0
- package/dist/modules/search/frontend/components/HybridSearchTable.js +343 -0
- package/dist/modules/search/frontend/components/HybridSearchTable.js.map +7 -0
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +303 -0
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +360 -0
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js +101 -0
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +608 -0
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/index.js +9 -0
- package/dist/modules/search/frontend/index.js.map +7 -0
- package/dist/modules/search/frontend/utils.js +41 -0
- package/dist/modules/search/frontend/utils.js.map +7 -0
- package/dist/modules/search/i18n/de.json +61 -0
- package/dist/modules/search/i18n/en.json +72 -0
- package/dist/modules/search/i18n/es.json +61 -0
- package/dist/modules/search/i18n/pl.json +61 -0
- package/dist/modules/search/index.js +11 -0
- package/dist/modules/search/index.js.map +7 -0
- package/dist/modules/search/lib/auto-indexing.js +29 -0
- package/dist/modules/search/lib/auto-indexing.js.map +7 -0
- package/dist/modules/search/lib/embedding-config.js +131 -0
- package/dist/modules/search/lib/embedding-config.js.map +7 -0
- package/dist/modules/search/lib/global-search-config.js +45 -0
- package/dist/modules/search/lib/global-search-config.js.map +7 -0
- package/dist/modules/search/lib/reindex-lock.js +99 -0
- package/dist/modules/search/lib/reindex-lock.js.map +7 -0
- package/dist/modules/search/subscribers/fulltext_upsert.js +64 -0
- package/dist/modules/search/subscribers/fulltext_upsert.js.map +7 -0
- package/dist/modules/search/subscribers/vector_delete.js +58 -0
- package/dist/modules/search/subscribers/vector_delete.js.map +7 -0
- package/dist/modules/search/subscribers/vector_purge.js +142 -0
- package/dist/modules/search/subscribers/vector_purge.js.map +7 -0
- package/dist/modules/search/subscribers/vector_upsert.js +58 -0
- package/dist/modules/search/subscribers/vector_upsert.js.map +7 -0
- package/dist/modules/search/workers/fulltext-index.worker.js +240 -0
- package/dist/modules/search/workers/fulltext-index.worker.js.map +7 -0
- package/dist/modules/search/workers/vector-index.worker.js +234 -0
- package/dist/modules/search/workers/vector-index.worker.js.map +7 -0
- package/dist/queue/fulltext-indexing.js +15 -0
- package/dist/queue/fulltext-indexing.js.map +7 -0
- package/dist/queue/index.js +3 -0
- package/dist/queue/index.js.map +7 -0
- package/dist/queue/vector-indexing.js +15 -0
- package/dist/queue/vector-indexing.js.map +7 -0
- package/dist/service.js +286 -0
- package/dist/service.js.map +7 -0
- package/dist/strategies/fulltext.strategy.js +116 -0
- package/dist/strategies/fulltext.strategy.js.map +7 -0
- package/dist/strategies/index.js +12 -0
- package/dist/strategies/index.js.map +7 -0
- package/dist/strategies/token.strategy.js +80 -0
- package/dist/strategies/token.strategy.js.map +7 -0
- package/dist/strategies/vector.strategy.js +137 -0
- package/dist/strategies/vector.strategy.js.map +7 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +7 -0
- package/dist/vector/drivers/chromadb/index.js +44 -0
- package/dist/vector/drivers/chromadb/index.js.map +7 -0
- package/dist/vector/drivers/index.js +9 -0
- package/dist/vector/drivers/index.js.map +7 -0
- package/dist/vector/drivers/pgvector/index.js +509 -0
- package/dist/vector/drivers/pgvector/index.js.map +7 -0
- package/dist/vector/drivers/qdrant/index.js +44 -0
- package/dist/vector/drivers/qdrant/index.js.map +7 -0
- package/dist/vector/index.js +4 -0
- package/dist/vector/index.js.map +7 -0
- package/dist/vector/lib/vector-logs.js +33 -0
- package/dist/vector/lib/vector-logs.js.map +7 -0
- package/dist/vector/services/checksum.js +20 -0
- package/dist/vector/services/checksum.js.map +7 -0
- package/dist/vector/services/embedding.js +222 -0
- package/dist/vector/services/embedding.js.map +7 -0
- package/dist/vector/services/index.js +4 -0
- package/dist/vector/services/index.js.map +7 -0
- package/dist/vector/services/vector-index.service.js +960 -0
- package/dist/vector/services/vector-index.service.js.map +7 -0
- package/dist/vector/types/pg.d.js +1 -0
- package/dist/vector/types/pg.d.js.map +7 -0
- package/dist/vector/types.js +75 -0
- package/dist/vector/types.js.map +7 -0
- package/jest.config.cjs +19 -0
- package/package.json +142 -0
- package/src/__tests__/queue.test.ts +148 -0
- package/src/__tests__/service.test.ts +345 -0
- package/src/__tests__/workers.test.ts +319 -0
- package/src/di.ts +291 -0
- package/src/fulltext/drivers/index.ts +41 -0
- package/src/fulltext/drivers/meilisearch/index.ts +410 -0
- package/src/fulltext/index.ts +13 -0
- package/src/fulltext/types.ts +115 -0
- package/src/index.ts +36 -0
- package/src/indexer/index.ts +13 -0
- package/src/indexer/search-indexer.ts +1141 -0
- package/src/indexer/subscribers/delete.ts +49 -0
- package/src/lib/debug.ts +46 -0
- package/src/lib/fallback-presenter.ts +106 -0
- package/src/lib/field-policy.ts +169 -0
- package/src/lib/index.ts +13 -0
- package/src/lib/merger.ts +159 -0
- package/src/lib/presenter-enricher.ts +323 -0
- package/src/modules/search/README.md +694 -0
- package/src/modules/search/acl.ts +10 -0
- package/src/modules/search/ai-tools.ts +467 -0
- package/src/modules/search/api/embeddings/reindex/cancel/route.ts +77 -0
- package/src/modules/search/api/embeddings/reindex/route.ts +197 -0
- package/src/modules/search/api/embeddings/route.ts +304 -0
- package/src/modules/search/api/index/route.ts +297 -0
- package/src/modules/search/api/reindex/cancel/route.ts +77 -0
- package/src/modules/search/api/reindex/route.ts +419 -0
- package/src/modules/search/api/search/global/route.ts +120 -0
- package/src/modules/search/api/search/route.ts +121 -0
- package/src/modules/search/api/settings/fulltext/route.ts +82 -0
- package/src/modules/search/api/settings/global-search/route.ts +91 -0
- package/src/modules/search/api/settings/route.ts +187 -0
- package/src/modules/search/api/settings/vector-store/route.ts +105 -0
- package/src/modules/search/backend/config/search/page.meta.ts +22 -0
- package/src/modules/search/backend/config/search/page.tsx +12 -0
- package/src/modules/search/cli.ts +818 -0
- package/src/modules/search/di.ts +50 -0
- package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +436 -0
- package/src/modules/search/frontend/components/HybridSearchTable.tsx +418 -0
- package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +476 -0
- package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +624 -0
- package/src/modules/search/frontend/components/sections/GlobalSearchSection.tsx +124 -0
- package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +943 -0
- package/src/modules/search/frontend/index.ts +3 -0
- package/src/modules/search/frontend/utils.ts +82 -0
- package/src/modules/search/i18n/de.json +61 -0
- package/src/modules/search/i18n/en.json +72 -0
- package/src/modules/search/i18n/es.json +61 -0
- package/src/modules/search/i18n/pl.json +61 -0
- package/src/modules/search/index.ts +9 -0
- package/src/modules/search/lib/auto-indexing.ts +35 -0
- package/src/modules/search/lib/embedding-config.ts +161 -0
- package/src/modules/search/lib/global-search-config.ts +69 -0
- package/src/modules/search/lib/reindex-lock.ts +201 -0
- package/src/modules/search/subscribers/fulltext_upsert.ts +83 -0
- package/src/modules/search/subscribers/vector_delete.ts +75 -0
- package/src/modules/search/subscribers/vector_purge.ts +161 -0
- package/src/modules/search/subscribers/vector_upsert.ts +75 -0
- package/src/modules/search/workers/fulltext-index.worker.ts +318 -0
- package/src/modules/search/workers/vector-index.worker.ts +292 -0
- package/src/queue/fulltext-indexing.ts +87 -0
- package/src/queue/index.ts +2 -0
- package/src/queue/vector-indexing.ts +66 -0
- package/src/service.ts +397 -0
- package/src/strategies/fulltext.strategy.ts +155 -0
- package/src/strategies/index.ts +17 -0
- package/src/strategies/token.strategy.ts +153 -0
- package/src/strategies/vector.strategy.ts +234 -0
- package/src/types.ts +38 -0
- package/src/vector/drivers/chromadb/index.ts +49 -0
- package/src/vector/drivers/index.ts +4 -0
- package/src/vector/drivers/pgvector/index.ts +627 -0
- package/src/vector/drivers/qdrant/index.ts +49 -0
- package/src/vector/index.ts +3 -0
- package/src/vector/lib/vector-logs.ts +46 -0
- package/src/vector/services/checksum.ts +18 -0
- package/src/vector/services/embedding.ts +275 -0
- package/src/vector/services/index.ts +3 -0
- package/src/vector/services/vector-index.service.ts +1234 -0
- package/src/vector/types/pg.d.ts +1 -0
- package/src/vector/types.ts +220 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { recordIndexerLog } from "@open-mercato/shared/lib/indexers/status-log";
|
|
6
|
+
import { recordIndexerError } from "@open-mercato/shared/lib/indexers/error-log";
|
|
7
|
+
import { searchDebug, searchError } from "../../../../lib/debug.js";
|
|
8
|
+
import {
|
|
9
|
+
acquireReindexLock,
|
|
10
|
+
clearReindexLock,
|
|
11
|
+
getReindexLockStatus
|
|
12
|
+
} from "../../lib/reindex-lock.js";
|
|
13
|
+
async function collectStrategyStats(strategies, tenantId) {
|
|
14
|
+
const stats = {};
|
|
15
|
+
for (const strategy of strategies) {
|
|
16
|
+
if (typeof strategy.getIndexStats === "function") {
|
|
17
|
+
try {
|
|
18
|
+
const isAvailable = await strategy.isAvailable();
|
|
19
|
+
if (isAvailable) {
|
|
20
|
+
stats[strategy.id] = await strategy.getIndexStats(tenantId);
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return stats;
|
|
27
|
+
}
|
|
28
|
+
const metadata = {
|
|
29
|
+
POST: { requireAuth: true, requireFeatures: ["search.reindex"] }
|
|
30
|
+
};
|
|
31
|
+
const toJson = (payload, init) => NextResponse.json(payload, init);
|
|
32
|
+
const unauthorized = async () => {
|
|
33
|
+
const { t } = await resolveTranslations();
|
|
34
|
+
return NextResponse.json({ error: t("api.errors.unauthorized", "Unauthorized") }, { status: 401 });
|
|
35
|
+
};
|
|
36
|
+
async function POST(req) {
|
|
37
|
+
const { t } = await resolveTranslations();
|
|
38
|
+
const auth = await getAuthFromRequest(req);
|
|
39
|
+
if (!auth?.tenantId) {
|
|
40
|
+
return await unauthorized();
|
|
41
|
+
}
|
|
42
|
+
const tenantId = auth.tenantId;
|
|
43
|
+
let payload = {};
|
|
44
|
+
try {
|
|
45
|
+
payload = await req.json();
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
const action = payload.action === "clear" ? "clear" : payload.action === "recreate" ? "recreate" : "reindex";
|
|
49
|
+
const entityId = typeof payload.entityId === "string" ? payload.entityId : void 0;
|
|
50
|
+
const useQueue = payload.useQueue !== false;
|
|
51
|
+
const container = await createRequestContainer();
|
|
52
|
+
const em = container.resolve("em");
|
|
53
|
+
const knex = em.getConnection().getKnex();
|
|
54
|
+
const existingLock = await getReindexLockStatus(knex, tenantId, { type: "fulltext" });
|
|
55
|
+
if (existingLock) {
|
|
56
|
+
const startedAt = new Date(existingLock.startedAt);
|
|
57
|
+
return NextResponse.json(
|
|
58
|
+
{
|
|
59
|
+
error: t("search.api.errors.reindexInProgress", "A reindex operation is already in progress"),
|
|
60
|
+
lock: {
|
|
61
|
+
type: existingLock.type,
|
|
62
|
+
action: existingLock.action,
|
|
63
|
+
startedAt: existingLock.startedAt,
|
|
64
|
+
elapsedMinutes: Math.round((Date.now() - startedAt.getTime()) / 6e4),
|
|
65
|
+
processedCount: existingLock.processedCount,
|
|
66
|
+
totalCount: existingLock.totalCount
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{ status: 409 }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const { acquired: lockAcquired } = await acquireReindexLock(knex, {
|
|
73
|
+
type: "fulltext",
|
|
74
|
+
action,
|
|
75
|
+
tenantId,
|
|
76
|
+
organizationId: auth.orgId ?? null
|
|
77
|
+
});
|
|
78
|
+
if (!lockAcquired) {
|
|
79
|
+
return NextResponse.json(
|
|
80
|
+
{ error: t("search.api.errors.lockFailed", "Failed to acquire reindex lock") },
|
|
81
|
+
{ status: 409 }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const searchStrategies = container.resolve("searchStrategies") ?? [];
|
|
86
|
+
const indexableStrategy = searchStrategies.find(
|
|
87
|
+
(s) => typeof s.clearIndex === "function" || typeof s.recreateIndex === "function"
|
|
88
|
+
);
|
|
89
|
+
if (!indexableStrategy) {
|
|
90
|
+
return toJson(
|
|
91
|
+
{ error: t("search.api.errors.noIndexableStrategy", "No indexable search strategy is configured") },
|
|
92
|
+
{ status: 503 }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const isAvailable = await indexableStrategy.isAvailable();
|
|
96
|
+
if (!isAvailable) {
|
|
97
|
+
return toJson(
|
|
98
|
+
{ error: t("search.api.errors.strategyUnavailable", "Search strategy is not available") },
|
|
99
|
+
{ status: 503 }
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (action === "reindex") {
|
|
103
|
+
const searchIndexer = container.resolve("searchIndexer");
|
|
104
|
+
if (!searchIndexer) {
|
|
105
|
+
return toJson(
|
|
106
|
+
{ error: t("search.api.errors.indexerUnavailable", "Search indexer is not available") },
|
|
107
|
+
{ status: 503 }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
let result;
|
|
111
|
+
const orgId = typeof auth.orgId === "string" ? auth.orgId : null;
|
|
112
|
+
const enabledEntities = searchIndexer.listEnabledEntities();
|
|
113
|
+
searchDebug("search.reindex", "Starting reindex", {
|
|
114
|
+
tenantId,
|
|
115
|
+
orgId,
|
|
116
|
+
enabledEntities,
|
|
117
|
+
entityId: entityId ?? "all",
|
|
118
|
+
useQueue
|
|
119
|
+
});
|
|
120
|
+
await recordIndexerLog(
|
|
121
|
+
{ em },
|
|
122
|
+
{
|
|
123
|
+
source: "fulltext",
|
|
124
|
+
handler: "api:search.reindex",
|
|
125
|
+
message: entityId ? `Starting Meilisearch reindex for ${entityId}` : `Starting Meilisearch reindex for all entities (${enabledEntities.join(", ")})`,
|
|
126
|
+
entityType: entityId ?? null,
|
|
127
|
+
tenantId,
|
|
128
|
+
organizationId: orgId,
|
|
129
|
+
details: { enabledEntities, useQueue }
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
if (entityId) {
|
|
133
|
+
result = await searchIndexer.reindexEntityToFulltext({
|
|
134
|
+
entityId,
|
|
135
|
+
tenantId,
|
|
136
|
+
organizationId: orgId,
|
|
137
|
+
recreateIndex: true,
|
|
138
|
+
useQueue,
|
|
139
|
+
onProgress: async (progress) => {
|
|
140
|
+
searchDebug("search.reindex", "Progress", progress);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
searchDebug("search.reindex", "Reindexed entity to Meilisearch", {
|
|
144
|
+
entityId,
|
|
145
|
+
tenantId,
|
|
146
|
+
recordsIndexed: result.recordsIndexed,
|
|
147
|
+
jobsEnqueued: result.jobsEnqueued,
|
|
148
|
+
errors: result.errors
|
|
149
|
+
});
|
|
150
|
+
await recordIndexerLog(
|
|
151
|
+
{ em },
|
|
152
|
+
{
|
|
153
|
+
source: "fulltext",
|
|
154
|
+
handler: "api:search.reindex",
|
|
155
|
+
message: useQueue ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of ${entityId}` : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${entityId}`,
|
|
156
|
+
entityType: entityId,
|
|
157
|
+
tenantId,
|
|
158
|
+
organizationId: orgId,
|
|
159
|
+
details: {
|
|
160
|
+
recordsIndexed: result.recordsIndexed,
|
|
161
|
+
jobsEnqueued: result.jobsEnqueued,
|
|
162
|
+
useQueue,
|
|
163
|
+
errors: result.errors.length > 0 ? result.errors : void 0
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
for (const err of result.errors) {
|
|
168
|
+
await recordIndexerError(
|
|
169
|
+
{ em },
|
|
170
|
+
{
|
|
171
|
+
source: "fulltext",
|
|
172
|
+
handler: "api:search.reindex",
|
|
173
|
+
error: new Error(err.error),
|
|
174
|
+
entityType: err.entityId,
|
|
175
|
+
tenantId,
|
|
176
|
+
organizationId: orgId,
|
|
177
|
+
payload: { action, useQueue }
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
result = await searchIndexer.reindexAllToFulltext({
|
|
183
|
+
tenantId,
|
|
184
|
+
organizationId: orgId,
|
|
185
|
+
recreateIndex: true,
|
|
186
|
+
useQueue,
|
|
187
|
+
onProgress: async (progress) => {
|
|
188
|
+
searchDebug("search.reindex", "Progress", progress);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
searchDebug("search.reindex", "Reindexed all entities to Meilisearch", {
|
|
192
|
+
tenantId,
|
|
193
|
+
entitiesProcessed: result.entitiesProcessed,
|
|
194
|
+
recordsIndexed: result.recordsIndexed,
|
|
195
|
+
jobsEnqueued: result.jobsEnqueued,
|
|
196
|
+
errors: result.errors
|
|
197
|
+
});
|
|
198
|
+
await recordIndexerLog(
|
|
199
|
+
{ em },
|
|
200
|
+
{
|
|
201
|
+
source: "fulltext",
|
|
202
|
+
handler: "api:search.reindex",
|
|
203
|
+
message: useQueue ? `Enqueued ${result.jobsEnqueued ?? 0} jobs for Meilisearch reindex of all entities` : `Reindexed ${result.recordsIndexed} records to Meilisearch for ${result.entitiesProcessed} entities`,
|
|
204
|
+
tenantId,
|
|
205
|
+
organizationId: orgId,
|
|
206
|
+
details: {
|
|
207
|
+
entitiesProcessed: result.entitiesProcessed,
|
|
208
|
+
recordsIndexed: result.recordsIndexed,
|
|
209
|
+
jobsEnqueued: result.jobsEnqueued,
|
|
210
|
+
useQueue,
|
|
211
|
+
errors: result.errors.length > 0 ? result.errors : void 0
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
for (const err of result.errors) {
|
|
216
|
+
await recordIndexerError(
|
|
217
|
+
{ em },
|
|
218
|
+
{
|
|
219
|
+
source: "fulltext",
|
|
220
|
+
handler: "api:search.reindex",
|
|
221
|
+
error: new Error(err.error),
|
|
222
|
+
entityType: err.entityId,
|
|
223
|
+
tenantId,
|
|
224
|
+
organizationId: orgId,
|
|
225
|
+
payload: { action, useQueue }
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const stats2 = await collectStrategyStats(searchStrategies, tenantId);
|
|
231
|
+
return toJson({
|
|
232
|
+
ok: result.success,
|
|
233
|
+
action,
|
|
234
|
+
entityId: entityId ?? null,
|
|
235
|
+
useQueue,
|
|
236
|
+
result: {
|
|
237
|
+
entitiesProcessed: result.entitiesProcessed,
|
|
238
|
+
recordsIndexed: result.recordsIndexed,
|
|
239
|
+
jobsEnqueued: result.jobsEnqueued ?? 0,
|
|
240
|
+
errors: result.errors.length > 0 ? result.errors : void 0
|
|
241
|
+
},
|
|
242
|
+
stats: stats2
|
|
243
|
+
});
|
|
244
|
+
} else if (entityId) {
|
|
245
|
+
await indexableStrategy.purge?.(entityId, tenantId);
|
|
246
|
+
searchDebug("search.reindex", "Purged entity", { strategyId: indexableStrategy.id, entityId, tenantId });
|
|
247
|
+
await recordIndexerLog(
|
|
248
|
+
{ em },
|
|
249
|
+
{
|
|
250
|
+
source: "fulltext",
|
|
251
|
+
handler: "api:search.reindex",
|
|
252
|
+
message: `Purged entity ${entityId} from Meilisearch`,
|
|
253
|
+
entityType: entityId,
|
|
254
|
+
tenantId,
|
|
255
|
+
organizationId: auth.orgId ?? null
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
} else if (action === "clear") {
|
|
259
|
+
if (indexableStrategy.clearIndex) {
|
|
260
|
+
await indexableStrategy.clearIndex(tenantId);
|
|
261
|
+
searchDebug("search.reindex", "Cleared index", { strategyId: indexableStrategy.id, tenantId });
|
|
262
|
+
await recordIndexerLog(
|
|
263
|
+
{ em },
|
|
264
|
+
{
|
|
265
|
+
source: "fulltext",
|
|
266
|
+
handler: "api:search.reindex",
|
|
267
|
+
message: "Cleared all documents from Meilisearch index",
|
|
268
|
+
tenantId,
|
|
269
|
+
organizationId: auth.orgId ?? null
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
if (indexableStrategy.recreateIndex) {
|
|
275
|
+
await indexableStrategy.recreateIndex(tenantId);
|
|
276
|
+
searchDebug("search.reindex", "Recreated index", { strategyId: indexableStrategy.id, tenantId });
|
|
277
|
+
await recordIndexerLog(
|
|
278
|
+
{ em },
|
|
279
|
+
{
|
|
280
|
+
source: "fulltext",
|
|
281
|
+
handler: "api:search.reindex",
|
|
282
|
+
message: "Recreated Meilisearch index",
|
|
283
|
+
tenantId,
|
|
284
|
+
organizationId: auth.orgId ?? null
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const stats = await collectStrategyStats(searchStrategies, tenantId);
|
|
290
|
+
return toJson({
|
|
291
|
+
ok: true,
|
|
292
|
+
action,
|
|
293
|
+
entityId: entityId ?? null,
|
|
294
|
+
stats
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
searchError("search.reindex", "Failed", {
|
|
298
|
+
error: error instanceof Error ? error.message : error,
|
|
299
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
300
|
+
tenantId
|
|
301
|
+
});
|
|
302
|
+
await recordIndexerError(
|
|
303
|
+
{ em },
|
|
304
|
+
{
|
|
305
|
+
source: "fulltext",
|
|
306
|
+
handler: "api:search.reindex",
|
|
307
|
+
error,
|
|
308
|
+
entityType: entityId ?? null,
|
|
309
|
+
tenantId,
|
|
310
|
+
organizationId: auth.orgId ?? null,
|
|
311
|
+
payload: { action, entityId, useQueue }
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
return toJson(
|
|
315
|
+
{ error: t("search.api.errors.reindexFailed", "Reindex operation failed. Please try again or contact support.") },
|
|
316
|
+
{ status: 500 }
|
|
317
|
+
);
|
|
318
|
+
} finally {
|
|
319
|
+
if (!useQueue) {
|
|
320
|
+
await clearReindexLock(knex, tenantId, "fulltext", auth.orgId ?? null);
|
|
321
|
+
}
|
|
322
|
+
const disposable = container;
|
|
323
|
+
if (typeof disposable.dispose === "function") {
|
|
324
|
+
await disposable.dispose();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
export {
|
|
329
|
+
POST,
|
|
330
|
+
metadata
|
|
331
|
+
};
|
|
332
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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 { EntityManager } from '@mikro-orm/postgresql'\nimport type { Knex } from 'knex'\nimport { searchDebug, searchError } from '../../../../lib/debug'\nimport {\n acquireReindexLock,\n clearReindexLock,\n getReindexLockStatus,\n} from '../../lib/reindex-lock'\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 knex = (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()\n\n // Check if another fulltext reindex operation is already in progress\n const existingLock = await getReindexLockStatus(knex, 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(knex, {\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: async (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: async (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 // 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 // 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(knex, 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"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["stats"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { resolveEmbeddingConfig } from "../../../lib/embedding-config.js";
|
|
6
|
+
import { resolveGlobalSearchStrategies } from "../../../lib/global-search-config.js";
|
|
7
|
+
import { searchError } from "../../../../../lib/debug.js";
|
|
8
|
+
const metadata = {
|
|
9
|
+
GET: { requireAuth: true, requireFeatures: ["search.view"] }
|
|
10
|
+
};
|
|
11
|
+
function parseLimit(value) {
|
|
12
|
+
if (!value) return 50;
|
|
13
|
+
const parsed = Number.parseInt(value, 10);
|
|
14
|
+
if (Number.isNaN(parsed) || parsed <= 0) return 50;
|
|
15
|
+
return Math.min(parsed, 100);
|
|
16
|
+
}
|
|
17
|
+
function parseEntityTypes(value) {
|
|
18
|
+
if (!value) return void 0;
|
|
19
|
+
const entityTypes = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
20
|
+
return entityTypes.length > 0 ? entityTypes : void 0;
|
|
21
|
+
}
|
|
22
|
+
async function GET(req) {
|
|
23
|
+
const { t } = await resolveTranslations();
|
|
24
|
+
const url = new URL(req.url);
|
|
25
|
+
const query = (url.searchParams.get("q") || "").trim();
|
|
26
|
+
const limit = parseLimit(url.searchParams.get("limit"));
|
|
27
|
+
const entityTypes = parseEntityTypes(url.searchParams.get("entityTypes"));
|
|
28
|
+
if (!query) {
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{ error: t("search.api.errors.missingQuery", "Missing query") },
|
|
31
|
+
{ status: 400 }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const auth = await getAuthFromRequest(req);
|
|
35
|
+
if (!auth?.tenantId) {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: t("api.errors.unauthorized", "Unauthorized") },
|
|
38
|
+
{ status: 401 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const container = await createRequestContainer();
|
|
42
|
+
try {
|
|
43
|
+
const searchService = container.resolve("searchService");
|
|
44
|
+
if (!searchService) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: t("search.api.errors.serviceUnavailable", "Search service unavailable") },
|
|
47
|
+
{ status: 503 }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const strategies = await resolveGlobalSearchStrategies(container);
|
|
51
|
+
if (strategies.includes("vector")) {
|
|
52
|
+
try {
|
|
53
|
+
const embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null });
|
|
54
|
+
if (embeddingConfig) {
|
|
55
|
+
const embeddingService = container.resolve("vectorEmbeddingService");
|
|
56
|
+
embeddingService.updateConfig(embeddingConfig);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const startTime = Date.now();
|
|
62
|
+
const searchOptions = {
|
|
63
|
+
tenantId: auth.tenantId,
|
|
64
|
+
organizationId: null,
|
|
65
|
+
limit,
|
|
66
|
+
strategies,
|
|
67
|
+
entityTypes
|
|
68
|
+
};
|
|
69
|
+
const results = await searchService.search(query, searchOptions);
|
|
70
|
+
const timing = Date.now() - startTime;
|
|
71
|
+
const strategiesUsed = [...new Set(results.map((r) => r.source))];
|
|
72
|
+
return NextResponse.json({
|
|
73
|
+
results,
|
|
74
|
+
strategiesUsed,
|
|
75
|
+
strategiesEnabled: strategies,
|
|
76
|
+
timing,
|
|
77
|
+
query,
|
|
78
|
+
limit
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
searchError("search.api.global", "failed", {
|
|
82
|
+
error: error instanceof Error ? error.message : error,
|
|
83
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
84
|
+
});
|
|
85
|
+
return NextResponse.json(
|
|
86
|
+
{ error: t("search.api.errors.searchFailed", "Search failed. Please try again.") },
|
|
87
|
+
{ status: 500 }
|
|
88
|
+
);
|
|
89
|
+
} finally {
|
|
90
|
+
const disposable = container;
|
|
91
|
+
if (typeof disposable.dispose === "function") {
|
|
92
|
+
await disposable.dispose();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
GET,
|
|
98
|
+
metadata
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/search/api/search/global/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 { SearchService } from '@open-mercato/search'\nimport type { EmbeddingService } from '../../../../../vector'\nimport { resolveEmbeddingConfig } from '../../../lib/embedding-config'\nimport { resolveGlobalSearchStrategies } from '../../../lib/global-search-config'\nimport { searchError } from '../../../../../lib/debug'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.view'] },\n}\n\nfunction parseLimit(value: string | null): number {\n if (!value) return 50\n const parsed = Number.parseInt(value, 10)\n if (Number.isNaN(parsed) || parsed <= 0) return 50\n return Math.min(parsed, 100)\n}\n\nfunction parseEntityTypes(value: string | null): string[] | undefined {\n if (!value) return undefined\n const entityTypes = value.split(',').map((s) => s.trim()).filter(Boolean)\n return entityTypes.length > 0 ? entityTypes : undefined\n}\n\n/**\n * Global search endpoint for Cmd+K.\n * Always uses saved global search settings - does NOT accept strategies from URL.\n */\nexport async function GET(req: Request) {\n const { t } = await resolveTranslations()\n const url = new URL(req.url)\n const query = (url.searchParams.get('q') || '').trim()\n const limit = parseLimit(url.searchParams.get('limit'))\n const entityTypes = parseEntityTypes(url.searchParams.get('entityTypes'))\n\n if (!query) {\n return NextResponse.json(\n { error: t('search.api.errors.missingQuery', 'Missing query') },\n { status: 400 }\n )\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json(\n { error: t('api.errors.unauthorized', 'Unauthorized') },\n { status: 401 }\n )\n }\n\n const container = await createRequestContainer()\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n if (!searchService) {\n return NextResponse.json(\n { error: t('search.api.errors.serviceUnavailable', 'Search service unavailable') },\n { status: 503 }\n )\n }\n\n // Fetch saved global search strategies\n const strategies = await resolveGlobalSearchStrategies(container)\n\n // Load embedding config for vector strategy (only if vector is enabled)\n if (strategies.includes('vector')) {\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 }\n } catch {\n // Embedding config not available, vector strategy may not work\n }\n }\n\n const startTime = Date.now()\n\n const searchOptions = {\n tenantId: auth.tenantId,\n organizationId: null,\n limit,\n strategies,\n entityTypes,\n }\n\n const results = await searchService.search(query, searchOptions)\n\n const timing = Date.now() - startTime\n\n // Collect unique strategies that returned results\n const strategiesUsed = [...new Set(results.map((r) => r.source))]\n\n return NextResponse.json({\n results,\n strategiesUsed,\n strategiesEnabled: strategies,\n timing,\n query,\n limit,\n })\n } catch (error: unknown) {\n searchError('search.api.global', 'failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n })\n return NextResponse.json(\n { error: t('search.api.errors.searchFailed', 'Search failed. Please try again.') },\n { status: 500 }\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { resolveEmbeddingConfig } from "../../lib/embedding-config.js";
|
|
6
|
+
import { searchError } from "../../../../lib/debug.js";
|
|
7
|
+
const metadata = {
|
|
8
|
+
GET: { requireAuth: true, requireFeatures: ["search.view"] }
|
|
9
|
+
};
|
|
10
|
+
function parseLimit(value) {
|
|
11
|
+
if (!value) return 50;
|
|
12
|
+
const parsed = Number.parseInt(value, 10);
|
|
13
|
+
if (Number.isNaN(parsed) || parsed <= 0) return 50;
|
|
14
|
+
return Math.min(parsed, 100);
|
|
15
|
+
}
|
|
16
|
+
function parseStrategies(value) {
|
|
17
|
+
if (!value) return void 0;
|
|
18
|
+
const strategies = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
19
|
+
return strategies.length > 0 ? strategies : void 0;
|
|
20
|
+
}
|
|
21
|
+
function parseEntityTypes(value) {
|
|
22
|
+
if (!value) return void 0;
|
|
23
|
+
const entityTypes = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
24
|
+
return entityTypes.length > 0 ? entityTypes : void 0;
|
|
25
|
+
}
|
|
26
|
+
async function GET(req) {
|
|
27
|
+
const { t } = await resolveTranslations();
|
|
28
|
+
const url = new URL(req.url);
|
|
29
|
+
const query = (url.searchParams.get("q") || "").trim();
|
|
30
|
+
const limit = parseLimit(url.searchParams.get("limit"));
|
|
31
|
+
const strategies = parseStrategies(url.searchParams.get("strategies"));
|
|
32
|
+
const entityTypes = parseEntityTypes(url.searchParams.get("entityTypes"));
|
|
33
|
+
if (!query) {
|
|
34
|
+
return NextResponse.json(
|
|
35
|
+
{ error: t("search.api.errors.missingQuery", "Missing query") },
|
|
36
|
+
{ status: 400 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const auth = await getAuthFromRequest(req);
|
|
40
|
+
if (!auth?.tenantId) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: t("api.errors.unauthorized", "Unauthorized") },
|
|
43
|
+
{ status: 401 }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const container = await createRequestContainer();
|
|
47
|
+
try {
|
|
48
|
+
const searchService = container.resolve("searchService");
|
|
49
|
+
if (!searchService) {
|
|
50
|
+
return NextResponse.json(
|
|
51
|
+
{ error: t("search.api.errors.serviceUnavailable", "Search service unavailable") },
|
|
52
|
+
{ status: 503 }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const embeddingConfig = await resolveEmbeddingConfig(container, { defaultValue: null });
|
|
57
|
+
if (embeddingConfig) {
|
|
58
|
+
const embeddingService = container.resolve("vectorEmbeddingService");
|
|
59
|
+
embeddingService.updateConfig(embeddingConfig);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
const searchOptions = {
|
|
65
|
+
tenantId: auth.tenantId,
|
|
66
|
+
organizationId: null,
|
|
67
|
+
limit,
|
|
68
|
+
strategies,
|
|
69
|
+
entityTypes
|
|
70
|
+
};
|
|
71
|
+
const results = await searchService.search(query, searchOptions);
|
|
72
|
+
const timing = Date.now() - startTime;
|
|
73
|
+
const strategiesUsed = [...new Set(results.map((r) => r.source))];
|
|
74
|
+
return NextResponse.json({
|
|
75
|
+
results,
|
|
76
|
+
strategiesUsed,
|
|
77
|
+
timing,
|
|
78
|
+
query,
|
|
79
|
+
limit
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
searchError("search.api.search", "failed", {
|
|
83
|
+
error: error instanceof Error ? error.message : error,
|
|
84
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
85
|
+
});
|
|
86
|
+
return NextResponse.json(
|
|
87
|
+
{ error: t("search.api.errors.searchFailed", "Search failed. Please try again.") },
|
|
88
|
+
{ status: 500 }
|
|
89
|
+
);
|
|
90
|
+
} finally {
|
|
91
|
+
const disposable = container;
|
|
92
|
+
if (typeof disposable.dispose === "function") {
|
|
93
|
+
await disposable.dispose();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
GET,
|
|
99
|
+
metadata
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/search/api/search/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 { SearchService } from '@open-mercato/search'\nimport type { SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport type { EmbeddingService } from '../../../../vector'\nimport { resolveEmbeddingConfig } from '../../lib/embedding-config'\nimport { searchError } from '../../../../lib/debug'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['search.view'] },\n}\n\nfunction parseLimit(value: string | null): number {\n if (!value) return 50\n const parsed = Number.parseInt(value, 10)\n if (Number.isNaN(parsed) || parsed <= 0) return 50\n return Math.min(parsed, 100)\n}\n\nfunction parseStrategies(value: string | null): SearchStrategyId[] | undefined {\n if (!value) return undefined\n const strategies = value.split(',').map((s) => s.trim()).filter(Boolean)\n return strategies.length > 0 ? strategies : undefined\n}\n\nfunction parseEntityTypes(value: string | null): string[] | undefined {\n if (!value) return undefined\n const entityTypes = value.split(',').map((s) => s.trim()).filter(Boolean)\n return entityTypes.length > 0 ? entityTypes : undefined\n}\n\nexport async function GET(req: Request) {\n const { t } = await resolveTranslations()\n const url = new URL(req.url)\n const query = (url.searchParams.get('q') || '').trim()\n const limit = parseLimit(url.searchParams.get('limit'))\n const strategies = parseStrategies(url.searchParams.get('strategies'))\n const entityTypes = parseEntityTypes(url.searchParams.get('entityTypes'))\n\n if (!query) {\n return NextResponse.json(\n { error: t('search.api.errors.missingQuery', 'Missing query') },\n { status: 400 }\n )\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json(\n { error: t('api.errors.unauthorized', 'Unauthorized') },\n { status: 401 }\n )\n }\n\n const container = await createRequestContainer()\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n if (!searchService) {\n return NextResponse.json(\n { error: t('search.api.errors.serviceUnavailable', 'Search service unavailable') },\n { status: 503 }\n )\n }\n\n // Load embedding config for vector strategy (same as Vector Search playground)\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 }\n } catch {\n // Embedding config not available, vector strategy may not work\n }\n\n const startTime = Date.now()\n\n // Don't filter by organization in the playground - show all results\n // Both strategies handle null as \"no organization filter\"\n const searchOptions = {\n tenantId: auth.tenantId,\n organizationId: null,\n limit,\n strategies,\n entityTypes,\n }\n\n const results = await searchService.search(query, searchOptions)\n\n const timing = Date.now() - startTime\n\n // Collect unique strategies that returned results\n const strategiesUsed = [...new Set(results.map((r) => r.source))]\n\n return NextResponse.json({\n results,\n strategiesUsed,\n timing,\n query,\n limit,\n })\n } catch (error: unknown) {\n // Log full error details server-side only\n searchError('search.api.search', 'failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n })\n // Return generic message to client - don't expose internal error details\n return NextResponse.json(\n { error: t('search.api.errors.searchFailed', 'Search failed. Please try again.') },\n { status: 500 }\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|