@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,15 @@
|
|
|
1
|
+
import { createQueue } from "@open-mercato/queue";
|
|
2
|
+
const VECTOR_INDEXING_QUEUE_NAME = "vector-indexing";
|
|
3
|
+
function createVectorIndexingQueue(strategy = "local", options) {
|
|
4
|
+
if (strategy === "async") {
|
|
5
|
+
return createQueue(VECTOR_INDEXING_QUEUE_NAME, "async", {
|
|
6
|
+
connection: options?.connection
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
return createQueue(VECTOR_INDEXING_QUEUE_NAME, "local");
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
VECTOR_INDEXING_QUEUE_NAME,
|
|
13
|
+
createVectorIndexingQueue
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=vector-indexing.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/queue/vector-indexing.ts"],
|
|
4
|
+
"sourcesContent": ["import { createQueue } from '@open-mercato/queue'\nimport type { Queue } from '@open-mercato/queue'\n\n/**\n * Job types for vector indexing queue\n */\nexport type VectorIndexJobType = 'index' | 'delete' | 'batch-index'\n\n/**\n * Record reference for batch indexing\n */\nexport type VectorBatchRecord = {\n entityId: string\n recordId: string\n}\n\n/**\n * Payload for vector indexing jobs\n */\nexport type VectorIndexJobPayload = {\n jobType: VectorIndexJobType\n tenantId: string\n organizationId: string | null\n // For single record jobs (index/delete)\n entityType?: string\n recordId?: string\n // For batch-index jobs\n records?: VectorBatchRecord[]\n}\n\n/**\n * Queue name for vector indexing\n */\nexport const VECTOR_INDEXING_QUEUE_NAME = 'vector-indexing'\n\n/**\n * Creates a vector indexing queue instance.\n *\n * @param strategy - Queue strategy: 'local' for file-based, 'async' for BullMQ/Redis\n * @param options - Strategy-specific options\n * @returns Queue instance for vector indexing jobs\n *\n * @example\n * ```typescript\n * // Local queue for development\n * const queue = createVectorIndexingQueue('local')\n *\n * // Async queue for production\n * const queue = createVectorIndexingQueue('async', {\n * connection: { url: process.env.REDIS_URL }\n * })\n * ```\n */\nexport function createVectorIndexingQueue(\n strategy: 'local' | 'async' = 'local',\n options?: {\n connection?: { url?: string; host?: string; port?: number }\n },\n): Queue<VectorIndexJobPayload> {\n if (strategy === 'async') {\n return createQueue<VectorIndexJobPayload>(VECTOR_INDEXING_QUEUE_NAME, 'async', {\n connection: options?.connection,\n })\n }\n return createQueue<VectorIndexJobPayload>(VECTOR_INDEXING_QUEUE_NAME, 'local')\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,mBAAmB;AAiCrB,MAAM,6BAA6B;AAoBnC,SAAS,0BACd,WAA8B,SAC9B,SAG8B;AAC9B,MAAI,aAAa,SAAS;AACxB,WAAO,YAAmC,4BAA4B,SAAS;AAAA,MAC7E,YAAY,SAAS;AAAA,IACvB,CAAC;AAAA,EACH;AACA,SAAO,YAAmC,4BAA4B,OAAO;AAC/E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { mergeAndRankResults } from "./lib/merger.js";
|
|
2
|
+
import { searchError } from "./lib/debug.js";
|
|
3
|
+
const DEFAULT_MERGE_CONFIG = {
|
|
4
|
+
duplicateHandling: "highest_score"
|
|
5
|
+
};
|
|
6
|
+
class SearchService {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.strategies = /* @__PURE__ */ new Map();
|
|
9
|
+
for (const strategy of options.strategies ?? []) {
|
|
10
|
+
this.strategies.set(strategy.id, strategy);
|
|
11
|
+
}
|
|
12
|
+
this.defaultStrategies = options.defaultStrategies ?? ["tokens"];
|
|
13
|
+
this.fallbackStrategy = options.fallbackStrategy;
|
|
14
|
+
this.mergeConfig = options.mergeConfig ?? DEFAULT_MERGE_CONFIG;
|
|
15
|
+
this.presenterEnricher = options.presenterEnricher;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get all registered strategies.
|
|
19
|
+
*/
|
|
20
|
+
getStrategies() {
|
|
21
|
+
return Array.from(this.strategies.values());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Execute a search query across configured strategies.
|
|
25
|
+
*
|
|
26
|
+
* @param query - Search query string
|
|
27
|
+
* @param options - Search options with tenant, filters, etc.
|
|
28
|
+
* @returns Merged and ranked search results
|
|
29
|
+
*/
|
|
30
|
+
async search(query, options) {
|
|
31
|
+
const strategyIds = options.strategies ?? this.defaultStrategies;
|
|
32
|
+
const activeStrategies = await this.getAvailableStrategies(strategyIds);
|
|
33
|
+
if (activeStrategies.length === 0) {
|
|
34
|
+
if (this.fallbackStrategy) {
|
|
35
|
+
const fallback = await this.getAvailableStrategies([this.fallbackStrategy]);
|
|
36
|
+
if (fallback.length > 0) {
|
|
37
|
+
activeStrategies.push(...fallback);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (activeStrategies.length === 0) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const results = await Promise.allSettled(
|
|
45
|
+
activeStrategies.map((strategy) => this.executeStrategySearch(strategy, query, options))
|
|
46
|
+
);
|
|
47
|
+
const allResults = [];
|
|
48
|
+
for (let i = 0; i < results.length; i++) {
|
|
49
|
+
const result = results[i];
|
|
50
|
+
if (result.status === "fulfilled") {
|
|
51
|
+
allResults.push(...result.value);
|
|
52
|
+
} else {
|
|
53
|
+
const strategy = activeStrategies[i];
|
|
54
|
+
searchError("SearchService", "Strategy search failed", {
|
|
55
|
+
strategyId: strategy?.id,
|
|
56
|
+
error: result.reason instanceof Error ? result.reason.message : result.reason
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const merged = mergeAndRankResults(allResults, this.mergeConfig);
|
|
61
|
+
return this.enrichResultsWithPresenter(merged, options.tenantId, options.organizationId);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Enrich results that are missing presenter data using the configured enricher.
|
|
65
|
+
* This ensures token-only results get proper titles/subtitles for display.
|
|
66
|
+
*/
|
|
67
|
+
async enrichResultsWithPresenter(results, tenantId, organizationId) {
|
|
68
|
+
if (!this.presenterEnricher) return results;
|
|
69
|
+
const needsEnrichment = (r) => {
|
|
70
|
+
if (!r.presenter?.title) return true;
|
|
71
|
+
const title = r.presenter.title;
|
|
72
|
+
if (typeof title === "string" && title.includes(":")) {
|
|
73
|
+
const parts = title.split(":");
|
|
74
|
+
if (parts.length >= 3 && parts[parts.length - 1] === "v1") return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
};
|
|
78
|
+
const hasMissing = results.some(needsEnrichment);
|
|
79
|
+
if (!hasMissing) return results;
|
|
80
|
+
try {
|
|
81
|
+
return await this.presenterEnricher(results, tenantId, organizationId);
|
|
82
|
+
} catch {
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Index a record across all available strategies.
|
|
88
|
+
*
|
|
89
|
+
* @param record - Record to index
|
|
90
|
+
*/
|
|
91
|
+
async index(record) {
|
|
92
|
+
const strategies = await this.getAvailableStrategies();
|
|
93
|
+
if (strategies.length === 0) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const results = await Promise.allSettled(
|
|
97
|
+
strategies.map((strategy) => this.executeStrategyIndex(strategy, record))
|
|
98
|
+
);
|
|
99
|
+
for (let i = 0; i < results.length; i++) {
|
|
100
|
+
const result = results[i];
|
|
101
|
+
if (result.status === "rejected") {
|
|
102
|
+
const strategy = strategies[i];
|
|
103
|
+
searchError("SearchService", "Strategy index failed", {
|
|
104
|
+
strategyId: strategy?.id,
|
|
105
|
+
entityId: record.entityId,
|
|
106
|
+
recordId: record.recordId,
|
|
107
|
+
error: result.reason instanceof Error ? result.reason.message : result.reason
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Delete a record from all strategies.
|
|
114
|
+
*
|
|
115
|
+
* @param entityId - Entity type identifier
|
|
116
|
+
* @param recordId - Record primary key
|
|
117
|
+
* @param tenantId - Tenant for isolation
|
|
118
|
+
*/
|
|
119
|
+
async delete(entityId, recordId, tenantId) {
|
|
120
|
+
const strategies = await this.getAvailableStrategies();
|
|
121
|
+
const results = await Promise.allSettled(
|
|
122
|
+
strategies.map((strategy) => this.executeStrategyDelete(strategy, entityId, recordId, tenantId))
|
|
123
|
+
);
|
|
124
|
+
for (let i = 0; i < results.length; i++) {
|
|
125
|
+
const result = results[i];
|
|
126
|
+
if (result.status === "rejected") {
|
|
127
|
+
const strategy = strategies[i];
|
|
128
|
+
searchError("SearchService", "Strategy delete failed", {
|
|
129
|
+
strategyId: strategy?.id,
|
|
130
|
+
entityId,
|
|
131
|
+
recordId,
|
|
132
|
+
error: result.reason instanceof Error ? result.reason.message : result.reason
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Bulk index multiple records.
|
|
139
|
+
*
|
|
140
|
+
* @param records - Records to index
|
|
141
|
+
*/
|
|
142
|
+
async bulkIndex(records) {
|
|
143
|
+
if (records.length === 0) return;
|
|
144
|
+
const strategies = await this.getAvailableStrategies();
|
|
145
|
+
const results = await Promise.allSettled(
|
|
146
|
+
strategies.map((strategy) => {
|
|
147
|
+
if (strategy.bulkIndex) {
|
|
148
|
+
return strategy.bulkIndex(records);
|
|
149
|
+
}
|
|
150
|
+
return Promise.all(records.map((record) => this.executeStrategyIndex(strategy, record)));
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
for (let i = 0; i < results.length; i++) {
|
|
154
|
+
const result = results[i];
|
|
155
|
+
if (result.status === "rejected") {
|
|
156
|
+
const strategy = strategies[i];
|
|
157
|
+
searchError("SearchService", "Strategy bulkIndex failed", {
|
|
158
|
+
strategyId: strategy?.id,
|
|
159
|
+
recordCount: records.length,
|
|
160
|
+
error: result.reason instanceof Error ? result.reason.message : result.reason
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Purge all records for an entity type.
|
|
167
|
+
*
|
|
168
|
+
* @param entityId - Entity type to purge
|
|
169
|
+
* @param tenantId - Tenant for isolation
|
|
170
|
+
*/
|
|
171
|
+
async purge(entityId, tenantId) {
|
|
172
|
+
const strategies = await this.getAvailableStrategies();
|
|
173
|
+
const results = await Promise.allSettled(
|
|
174
|
+
strategies.map((strategy) => {
|
|
175
|
+
if (strategy.purge) {
|
|
176
|
+
return strategy.purge(entityId, tenantId);
|
|
177
|
+
}
|
|
178
|
+
return Promise.resolve();
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
for (let i = 0; i < results.length; i++) {
|
|
182
|
+
const result = results[i];
|
|
183
|
+
if (result.status === "rejected") {
|
|
184
|
+
const strategy = strategies[i];
|
|
185
|
+
searchError("SearchService", "Strategy purge failed", {
|
|
186
|
+
strategyId: strategy?.id,
|
|
187
|
+
entityId,
|
|
188
|
+
error: result.reason instanceof Error ? result.reason.message : result.reason
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Register a new strategy at runtime.
|
|
195
|
+
*
|
|
196
|
+
* @param strategy - Strategy to register
|
|
197
|
+
*/
|
|
198
|
+
registerStrategy(strategy) {
|
|
199
|
+
this.strategies.set(strategy.id, strategy);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Unregister a strategy.
|
|
203
|
+
*
|
|
204
|
+
* @param strategyId - Strategy ID to remove
|
|
205
|
+
*/
|
|
206
|
+
unregisterStrategy(strategyId) {
|
|
207
|
+
this.strategies.delete(strategyId);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get all registered strategy IDs.
|
|
211
|
+
*/
|
|
212
|
+
getRegisteredStrategies() {
|
|
213
|
+
return Array.from(this.strategies.keys());
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get a specific strategy by ID.
|
|
217
|
+
*
|
|
218
|
+
* @param strategyId - Strategy ID to retrieve
|
|
219
|
+
* @returns The strategy if registered, undefined otherwise
|
|
220
|
+
*/
|
|
221
|
+
getStrategy(strategyId) {
|
|
222
|
+
return this.strategies.get(strategyId);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Get the default strategies list.
|
|
226
|
+
*/
|
|
227
|
+
getDefaultStrategies() {
|
|
228
|
+
return [...this.defaultStrategies];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check if a specific strategy is available.
|
|
232
|
+
*
|
|
233
|
+
* @param strategyId - Strategy ID to check
|
|
234
|
+
*/
|
|
235
|
+
async isStrategyAvailable(strategyId) {
|
|
236
|
+
const strategy = this.strategies.get(strategyId);
|
|
237
|
+
if (!strategy) return false;
|
|
238
|
+
return strategy.isAvailable();
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get available strategies from the requested list.
|
|
242
|
+
* Filters out strategies that are not registered or not available.
|
|
243
|
+
*/
|
|
244
|
+
async getAvailableStrategies(ids) {
|
|
245
|
+
const targetIds = ids ?? Array.from(this.strategies.keys());
|
|
246
|
+
const available = [];
|
|
247
|
+
for (const id of targetIds) {
|
|
248
|
+
const strategy = this.strategies.get(id);
|
|
249
|
+
if (strategy) {
|
|
250
|
+
try {
|
|
251
|
+
const isAvailable = await strategy.isAvailable();
|
|
252
|
+
if (isAvailable) {
|
|
253
|
+
available.push(strategy);
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return available.sort((a, b) => b.priority - a.priority);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Execute search on a single strategy with error handling.
|
|
263
|
+
*/
|
|
264
|
+
async executeStrategySearch(strategy, query, options) {
|
|
265
|
+
await strategy.ensureReady();
|
|
266
|
+
return strategy.search(query, options);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Execute index on a single strategy with error handling.
|
|
270
|
+
*/
|
|
271
|
+
async executeStrategyIndex(strategy, record) {
|
|
272
|
+
await strategy.ensureReady();
|
|
273
|
+
return strategy.index(record);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Execute delete on a single strategy with error handling.
|
|
277
|
+
*/
|
|
278
|
+
async executeStrategyDelete(strategy, entityId, recordId, tenantId) {
|
|
279
|
+
await strategy.ensureReady();
|
|
280
|
+
return strategy.delete(entityId, recordId, tenantId);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
export {
|
|
284
|
+
SearchService
|
|
285
|
+
};
|
|
286
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/service.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n SearchServiceOptions,\n ResultMergeConfig,\n IndexableRecord,\n PresenterEnricherFn,\n} from './types'\nimport { mergeAndRankResults } from './lib/merger'\nimport { searchError } from './lib/debug'\n\n/**\n * Default merge configuration.\n */\nconst DEFAULT_MERGE_CONFIG: ResultMergeConfig = {\n duplicateHandling: 'highest_score',\n}\n\n/**\n * SearchService orchestrates multiple search strategies, executing searches in parallel\n * and merging results using the RRF algorithm.\n *\n * Features:\n * - Parallel strategy execution for optimal performance\n * - Graceful degradation when strategies fail\n * - Result merging with configurable weights\n * - Strategy availability checking\n *\n * @example\n * ```typescript\n * const service = new SearchService({\n * strategies: [tokenStrategy, vectorStrategy, fulltextStrategy],\n * defaultStrategies: ['fulltext', 'vector', 'tokens'],\n * mergeConfig: {\n * duplicateHandling: 'highest_score',\n * strategyWeights: { fulltext: 1.2, vector: 1.0, tokens: 0.8 },\n * },\n * })\n *\n * const results = await service.search('john doe', { tenantId: 'tenant-123' })\n * ```\n */\nexport class SearchService {\n private readonly strategies: Map<SearchStrategyId, SearchStrategy>\n private readonly defaultStrategies: SearchStrategyId[]\n private readonly fallbackStrategy: SearchStrategyId | undefined\n private readonly mergeConfig: ResultMergeConfig\n private readonly presenterEnricher?: PresenterEnricherFn\n\n constructor(options: SearchServiceOptions = {}) {\n this.strategies = new Map()\n for (const strategy of options.strategies ?? []) {\n this.strategies.set(strategy.id, strategy)\n }\n this.defaultStrategies = options.defaultStrategies ?? ['tokens']\n this.fallbackStrategy = options.fallbackStrategy\n this.mergeConfig = options.mergeConfig ?? DEFAULT_MERGE_CONFIG\n this.presenterEnricher = options.presenterEnricher\n }\n\n /**\n * Get all registered strategies.\n */\n getStrategies(): SearchStrategy[] {\n return Array.from(this.strategies.values())\n }\n\n /**\n * Execute a search query across configured strategies.\n *\n * @param query - Search query string\n * @param options - Search options with tenant, filters, etc.\n * @returns Merged and ranked search results\n */\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n const strategyIds = options.strategies ?? this.defaultStrategies\n const activeStrategies = await this.getAvailableStrategies(strategyIds)\n\n if (activeStrategies.length === 0) {\n // Try fallback strategy if defined\n if (this.fallbackStrategy) {\n const fallback = await this.getAvailableStrategies([this.fallbackStrategy])\n if (fallback.length > 0) {\n activeStrategies.push(...fallback)\n }\n }\n }\n\n if (activeStrategies.length === 0) {\n return []\n }\n\n // Execute searches in parallel with graceful degradation\n const results = await Promise.allSettled(\n activeStrategies.map((strategy) => this.executeStrategySearch(strategy, query, options)),\n )\n\n // Collect successful results, log failures\n const allResults: SearchResult[] = []\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'fulfilled') {\n allResults.push(...result.value)\n } else {\n const strategy = activeStrategies[i]\n searchError('SearchService', 'Strategy search failed', {\n strategyId: strategy?.id,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n\n // Merge and rank results\n const merged = mergeAndRankResults(allResults, this.mergeConfig)\n\n // Enrich results missing presenter data\n return this.enrichResultsWithPresenter(merged, options.tenantId, options.organizationId)\n }\n\n /**\n * Enrich results that are missing presenter data using the configured enricher.\n * This ensures token-only results get proper titles/subtitles for display.\n */\n private async enrichResultsWithPresenter(\n results: SearchResult[],\n tenantId: string,\n organizationId?: string | null,\n ): Promise<SearchResult[]> {\n // If no enricher configured, return as-is\n if (!this.presenterEnricher) return results\n\n // Check if any results need enrichment (missing or encrypted presenter)\n const needsEnrichment = (r: SearchResult) => {\n if (!r.presenter?.title) return true\n // Also enrich if presenter looks encrypted (format: iv:ciphertext:authTag:v1)\n const title = r.presenter.title\n if (typeof title === 'string' && title.includes(':')) {\n const parts = title.split(':')\n if (parts.length >= 3 && parts[parts.length - 1] === 'v1') return true\n }\n return false\n }\n const hasMissing = results.some(needsEnrichment)\n if (!hasMissing) return results\n\n // Use the configured presenter enricher\n try {\n return await this.presenterEnricher(results, tenantId, organizationId)\n } catch {\n // Enrichment failed, return results as-is\n return results\n }\n }\n\n /**\n * Index a record across all available strategies.\n *\n * @param record - Record to index\n */\n async index(record: IndexableRecord): Promise<void> {\n const strategies = await this.getAvailableStrategies()\n\n if (strategies.length === 0) {\n return\n }\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => this.executeStrategyIndex(strategy, record)),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy index failed', {\n strategyId: strategy?.id,\n entityId: record.entityId,\n recordId: record.recordId,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Delete a record from all strategies.\n *\n * @param entityId - Entity type identifier\n * @param recordId - Record primary key\n * @param tenantId - Tenant for isolation\n */\n async delete(entityId: string, recordId: string, tenantId: string): Promise<void> {\n const strategies = await this.getAvailableStrategies()\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => this.executeStrategyDelete(strategy, entityId, recordId, tenantId)),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy delete failed', {\n strategyId: strategy?.id,\n entityId,\n recordId,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Bulk index multiple records.\n *\n * @param records - Records to index\n */\n async bulkIndex(records: IndexableRecord[]): Promise<void> {\n if (records.length === 0) return\n\n const strategies = await this.getAvailableStrategies()\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => {\n if (strategy.bulkIndex) {\n return strategy.bulkIndex(records)\n }\n // Fallback to individual indexing\n return Promise.all(records.map((record) => this.executeStrategyIndex(strategy, record)))\n }),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy bulkIndex failed', {\n strategyId: strategy?.id,\n recordCount: records.length,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Purge all records for an entity type.\n *\n * @param entityId - Entity type to purge\n * @param tenantId - Tenant for isolation\n */\n async purge(entityId: string, tenantId: string): Promise<void> {\n const strategies = await this.getAvailableStrategies()\n\n const results = await Promise.allSettled(\n strategies.map((strategy) => {\n if (strategy.purge) {\n return strategy.purge(entityId, tenantId)\n }\n return Promise.resolve()\n }),\n )\n\n // Log any failures\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n if (result.status === 'rejected') {\n const strategy = strategies[i]\n searchError('SearchService', 'Strategy purge failed', {\n strategyId: strategy?.id,\n entityId,\n error: result.reason instanceof Error ? result.reason.message : result.reason,\n })\n }\n }\n }\n\n /**\n * Register a new strategy at runtime.\n *\n * @param strategy - Strategy to register\n */\n registerStrategy(strategy: SearchStrategy): void {\n this.strategies.set(strategy.id, strategy)\n }\n\n /**\n * Unregister a strategy.\n *\n * @param strategyId - Strategy ID to remove\n */\n unregisterStrategy(strategyId: SearchStrategyId): void {\n this.strategies.delete(strategyId)\n }\n\n /**\n * Get all registered strategy IDs.\n */\n getRegisteredStrategies(): SearchStrategyId[] {\n return Array.from(this.strategies.keys())\n }\n\n /**\n * Get a specific strategy by ID.\n *\n * @param strategyId - Strategy ID to retrieve\n * @returns The strategy if registered, undefined otherwise\n */\n getStrategy(strategyId: SearchStrategyId): SearchStrategy | undefined {\n return this.strategies.get(strategyId)\n }\n\n /**\n * Get the default strategies list.\n */\n getDefaultStrategies(): SearchStrategyId[] {\n return [...this.defaultStrategies]\n }\n\n /**\n * Check if a specific strategy is available.\n *\n * @param strategyId - Strategy ID to check\n */\n async isStrategyAvailable(strategyId: SearchStrategyId): Promise<boolean> {\n const strategy = this.strategies.get(strategyId)\n if (!strategy) return false\n return strategy.isAvailable()\n }\n\n /**\n * Get available strategies from the requested list.\n * Filters out strategies that are not registered or not available.\n */\n private async getAvailableStrategies(ids?: SearchStrategyId[]): Promise<SearchStrategy[]> {\n const targetIds = ids ?? Array.from(this.strategies.keys())\n const available: SearchStrategy[] = []\n\n for (const id of targetIds) {\n const strategy = this.strategies.get(id)\n if (strategy) {\n try {\n const isAvailable = await strategy.isAvailable()\n if (isAvailable) {\n available.push(strategy)\n }\n } catch {\n // Strategy availability check failed, skip it\n }\n }\n }\n\n // Sort by priority (higher priority first)\n return available.sort((a, b) => b.priority - a.priority)\n }\n\n /**\n * Execute search on a single strategy with error handling.\n */\n private async executeStrategySearch(\n strategy: SearchStrategy,\n query: string,\n options: SearchOptions,\n ): Promise<SearchResult[]> {\n await strategy.ensureReady()\n return strategy.search(query, options)\n }\n\n /**\n * Execute index on a single strategy with error handling.\n */\n private async executeStrategyIndex(\n strategy: SearchStrategy,\n record: IndexableRecord,\n ): Promise<void> {\n await strategy.ensureReady()\n return strategy.index(record)\n }\n\n /**\n * Execute delete on a single strategy with error handling.\n */\n private async executeStrategyDelete(\n strategy: SearchStrategy,\n entityId: string,\n recordId: string,\n tenantId: string,\n ): Promise<void> {\n await strategy.ensureReady()\n return strategy.delete(entityId, recordId, tenantId)\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
class FullTextSearchStrategy {
|
|
2
|
+
// Highest priority when available
|
|
3
|
+
constructor(driver) {
|
|
4
|
+
this.driver = driver;
|
|
5
|
+
this.id = "fulltext";
|
|
6
|
+
this.name = "Full-Text Search";
|
|
7
|
+
this.priority = 30;
|
|
8
|
+
}
|
|
9
|
+
async isAvailable() {
|
|
10
|
+
return this.driver.isHealthy();
|
|
11
|
+
}
|
|
12
|
+
async ensureReady() {
|
|
13
|
+
return this.driver.ensureReady();
|
|
14
|
+
}
|
|
15
|
+
async search(query, options) {
|
|
16
|
+
const hits = await this.driver.search(query, {
|
|
17
|
+
tenantId: options.tenantId,
|
|
18
|
+
organizationId: options.organizationId,
|
|
19
|
+
entityTypes: options.entityTypes,
|
|
20
|
+
limit: options.limit,
|
|
21
|
+
offset: options.offset
|
|
22
|
+
});
|
|
23
|
+
return hits.map((hit) => this.mapHitToResult(hit));
|
|
24
|
+
}
|
|
25
|
+
async index(record) {
|
|
26
|
+
const doc = this.mapRecordToDocument(record);
|
|
27
|
+
await this.driver.index(doc);
|
|
28
|
+
}
|
|
29
|
+
async delete(entityId, recordId, tenantId) {
|
|
30
|
+
return this.driver.delete(recordId, tenantId);
|
|
31
|
+
}
|
|
32
|
+
async bulkIndex(records) {
|
|
33
|
+
if (!this.driver.bulkIndex) {
|
|
34
|
+
for (const record of records) {
|
|
35
|
+
await this.index(record);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const docs = records.map((record) => this.mapRecordToDocument(record));
|
|
40
|
+
return this.driver.bulkIndex(docs);
|
|
41
|
+
}
|
|
42
|
+
async purge(entityId, tenantId) {
|
|
43
|
+
if (!this.driver.purge) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
return this.driver.purge(entityId, tenantId);
|
|
47
|
+
}
|
|
48
|
+
// Additional methods exposed for enrichment and admin purposes
|
|
49
|
+
// These delegate to optional driver methods
|
|
50
|
+
async clearIndex(tenantId) {
|
|
51
|
+
if (!this.driver.clearIndex) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
return this.driver.clearIndex(tenantId);
|
|
55
|
+
}
|
|
56
|
+
async recreateIndex(tenantId) {
|
|
57
|
+
if (!this.driver.recreateIndex) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
return this.driver.recreateIndex(tenantId);
|
|
61
|
+
}
|
|
62
|
+
async getDocuments(ids, tenantId) {
|
|
63
|
+
if (!this.driver.getDocuments) {
|
|
64
|
+
return /* @__PURE__ */ new Map();
|
|
65
|
+
}
|
|
66
|
+
const hits = await this.driver.getDocuments(ids, tenantId);
|
|
67
|
+
const result = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const [key, hit] of hits) {
|
|
69
|
+
result.set(key, this.mapHitToResult(hit));
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
async getIndexStats(tenantId) {
|
|
74
|
+
if (!this.driver.getIndexStats) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return this.driver.getIndexStats(tenantId);
|
|
78
|
+
}
|
|
79
|
+
async getEntityCounts(tenantId) {
|
|
80
|
+
if (!this.driver.getEntityCounts) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return this.driver.getEntityCounts(tenantId);
|
|
84
|
+
}
|
|
85
|
+
get driverId() {
|
|
86
|
+
return this.driver.id;
|
|
87
|
+
}
|
|
88
|
+
mapHitToResult(hit) {
|
|
89
|
+
return {
|
|
90
|
+
entityId: hit.entityId,
|
|
91
|
+
recordId: hit.recordId,
|
|
92
|
+
score: hit.score,
|
|
93
|
+
source: this.id,
|
|
94
|
+
presenter: hit.presenter,
|
|
95
|
+
url: hit.url,
|
|
96
|
+
links: hit.links,
|
|
97
|
+
metadata: hit.metadata
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
mapRecordToDocument(record) {
|
|
101
|
+
return {
|
|
102
|
+
recordId: record.recordId,
|
|
103
|
+
entityId: record.entityId,
|
|
104
|
+
tenantId: record.tenantId,
|
|
105
|
+
organizationId: record.organizationId,
|
|
106
|
+
fields: record.fields,
|
|
107
|
+
presenter: record.presenter,
|
|
108
|
+
url: record.url,
|
|
109
|
+
links: record.links
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
FullTextSearchStrategy
|
|
115
|
+
};
|
|
116
|
+
//# sourceMappingURL=fulltext.strategy.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/strategies/fulltext.strategy.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type {\n FullTextSearchDriver,\n FullTextSearchDocument,\n FullTextSearchHit,\n DocumentLookupKey,\n IndexStats,\n} from '../fulltext/types'\n\n/**\n * FullTextSearchStrategy provides full-text fuzzy search using a pluggable driver.\n * Default driver is Meilisearch, but can be swapped for Algolia, Elasticsearch, etc.\n */\nexport class FullTextSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'fulltext'\n readonly name = 'Full-Text Search'\n readonly priority = 30 // Highest priority when available\n\n constructor(private readonly driver: FullTextSearchDriver) {}\n\n async isAvailable(): Promise<boolean> {\n return this.driver.isHealthy()\n }\n\n async ensureReady(): Promise<void> {\n return this.driver.ensureReady()\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n const hits = await this.driver.search(query, {\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n entityTypes: options.entityTypes,\n limit: options.limit,\n offset: options.offset,\n })\n\n return hits.map((hit) => this.mapHitToResult(hit))\n }\n\n async index(record: IndexableRecord): Promise<void> {\n const doc = this.mapRecordToDocument(record)\n await this.driver.index(doc)\n }\n\n async delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void> {\n return this.driver.delete(recordId, tenantId)\n }\n\n async bulkIndex(records: IndexableRecord[]): Promise<void> {\n if (!this.driver.bulkIndex) {\n // Fallback to sequential indexing\n for (const record of records) {\n await this.index(record)\n }\n return\n }\n\n const docs = records.map((record) => this.mapRecordToDocument(record))\n return this.driver.bulkIndex(docs)\n }\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n if (!this.driver.purge) {\n return\n }\n return this.driver.purge(entityId, tenantId)\n }\n\n // Additional methods exposed for enrichment and admin purposes\n // These delegate to optional driver methods\n\n async clearIndex(tenantId: string): Promise<void> {\n if (!this.driver.clearIndex) {\n return\n }\n return this.driver.clearIndex(tenantId)\n }\n\n async recreateIndex(tenantId: string): Promise<void> {\n if (!this.driver.recreateIndex) {\n return\n }\n return this.driver.recreateIndex(tenantId)\n }\n\n async getDocuments(\n ids: DocumentLookupKey[],\n tenantId: string\n ): Promise<Map<string, SearchResult>> {\n if (!this.driver.getDocuments) {\n return new Map()\n }\n\n const hits = await this.driver.getDocuments(ids, tenantId)\n const result = new Map<string, SearchResult>()\n\n for (const [key, hit] of hits) {\n result.set(key, this.mapHitToResult(hit))\n }\n\n return result\n }\n\n async getIndexStats(tenantId: string): Promise<IndexStats | null> {\n if (!this.driver.getIndexStats) {\n return null\n }\n return this.driver.getIndexStats(tenantId)\n }\n\n async getEntityCounts(tenantId: string): Promise<Record<string, number> | null> {\n if (!this.driver.getEntityCounts) {\n return null\n }\n return this.driver.getEntityCounts(tenantId)\n }\n\n get driverId(): string {\n return this.driver.id\n }\n\n private mapHitToResult(hit: FullTextSearchHit): SearchResult {\n return {\n entityId: hit.entityId,\n recordId: hit.recordId,\n score: hit.score,\n source: this.id,\n presenter: hit.presenter,\n url: hit.url,\n links: hit.links,\n metadata: hit.metadata,\n }\n }\n\n private mapRecordToDocument(record: IndexableRecord): FullTextSearchDocument {\n return {\n recordId: record.recordId,\n entityId: record.entityId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n fields: record.fields,\n presenter: record.presenter,\n url: record.url,\n links: record.links,\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TokenSearchStrategy } from "./token.strategy.js";
|
|
2
|
+
import { VectorSearchStrategy } from "./vector.strategy.js";
|
|
3
|
+
import { FullTextSearchStrategy } from "./fulltext.strategy.js";
|
|
4
|
+
import { createMeilisearchDriver, createFulltextDriver } from "../fulltext/drivers/index.js";
|
|
5
|
+
export {
|
|
6
|
+
FullTextSearchStrategy,
|
|
7
|
+
TokenSearchStrategy,
|
|
8
|
+
VectorSearchStrategy,
|
|
9
|
+
createFulltextDriver,
|
|
10
|
+
createMeilisearchDriver
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/strategies/index.ts"],
|
|
4
|
+
"sourcesContent": ["export { TokenSearchStrategy, type TokenStrategyConfig } from './token.strategy'\nexport { VectorSearchStrategy, type VectorStrategyConfig, type EmbeddingService } from './vector.strategy'\nexport { FullTextSearchStrategy } from './fulltext.strategy'\n\n// Re-export fulltext driver types for convenience\nexport type {\n FullTextSearchDriver,\n FullTextSearchDriverId,\n FullTextSearchDocument,\n FullTextSearchQuery,\n FullTextSearchHit,\n FullTextSearchDriverConfig,\n DocumentLookupKey,\n IndexStats,\n} from '../fulltext/types'\nexport { createMeilisearchDriver, createFulltextDriver } from '../fulltext/drivers'\nexport type { MeilisearchDriverOptions } from '../fulltext/drivers/meilisearch'\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2BAAqD;AAC9D,SAAS,4BAA8E;AACvF,SAAS,8BAA8B;AAavC,SAAS,yBAAyB,4BAA4B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
class TokenSearchStrategy {
|
|
2
|
+
constructor(knex, config) {
|
|
3
|
+
this.knex = knex;
|
|
4
|
+
this.id = "tokens";
|
|
5
|
+
this.name = "Token Search";
|
|
6
|
+
this.priority = 10;
|
|
7
|
+
this.minMatchRatio = config?.minMatchRatio ?? 0.5;
|
|
8
|
+
this.defaultLimit = config?.defaultLimit ?? 50;
|
|
9
|
+
}
|
|
10
|
+
async isAvailable() {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
async ensureReady() {
|
|
14
|
+
}
|
|
15
|
+
async search(query, options) {
|
|
16
|
+
const { tokenizeText } = await import("@open-mercato/shared/lib/search/tokenize");
|
|
17
|
+
const { resolveSearchConfig } = await import("@open-mercato/shared/lib/search/config");
|
|
18
|
+
const config = resolveSearchConfig();
|
|
19
|
+
if (!config.enabled) return [];
|
|
20
|
+
const { hashes } = tokenizeText(query, config);
|
|
21
|
+
if (hashes.length === 0) return [];
|
|
22
|
+
const minMatches = Math.max(1, Math.ceil(hashes.length * this.minMatchRatio));
|
|
23
|
+
const limit = options.limit ?? this.defaultLimit;
|
|
24
|
+
let queryBuilder = this.knex("search_tokens").select("entity_type", "entity_id").count("* as match_count").whereIn("token_hash", hashes).where("tenant_id", options.tenantId).groupBy("entity_type", "entity_id").havingRaw("COUNT(DISTINCT token_hash) >= ?", [minMatches]).orderByRaw("COUNT(DISTINCT token_hash) DESC").limit(limit);
|
|
25
|
+
if (options.organizationId) {
|
|
26
|
+
queryBuilder = queryBuilder.where("organization_id", options.organizationId);
|
|
27
|
+
}
|
|
28
|
+
if (options.entityTypes?.length) {
|
|
29
|
+
queryBuilder = queryBuilder.whereIn("entity_type", options.entityTypes);
|
|
30
|
+
}
|
|
31
|
+
const rows = await queryBuilder;
|
|
32
|
+
return rows.map((row) => {
|
|
33
|
+
const matchCount = typeof row.match_count === "string" ? parseInt(row.match_count, 10) : row.match_count;
|
|
34
|
+
const score = matchCount / hashes.length;
|
|
35
|
+
return {
|
|
36
|
+
entityId: row.entity_type,
|
|
37
|
+
recordId: row.entity_id,
|
|
38
|
+
score,
|
|
39
|
+
source: this.id
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async index(record) {
|
|
44
|
+
const { replaceSearchTokensForRecord } = await import("@open-mercato/core/modules/query_index/lib/search-tokens");
|
|
45
|
+
await replaceSearchTokensForRecord(this.knex, {
|
|
46
|
+
entityType: record.entityId,
|
|
47
|
+
recordId: record.recordId,
|
|
48
|
+
tenantId: record.tenantId,
|
|
49
|
+
organizationId: record.organizationId,
|
|
50
|
+
doc: record.fields
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async delete(entityId, recordId, tenantId) {
|
|
54
|
+
const { deleteSearchTokensForRecord } = await import("@open-mercato/core/modules/query_index/lib/search-tokens");
|
|
55
|
+
await deleteSearchTokensForRecord(this.knex, {
|
|
56
|
+
entityType: entityId,
|
|
57
|
+
recordId,
|
|
58
|
+
tenantId
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async bulkIndex(records) {
|
|
62
|
+
if (records.length === 0) return;
|
|
63
|
+
const { replaceSearchTokensForBatch } = await import("@open-mercato/core/modules/query_index/lib/search-tokens");
|
|
64
|
+
const payloads = records.map((record) => ({
|
|
65
|
+
entityType: record.entityId,
|
|
66
|
+
recordId: record.recordId,
|
|
67
|
+
tenantId: record.tenantId,
|
|
68
|
+
organizationId: record.organizationId,
|
|
69
|
+
doc: record.fields
|
|
70
|
+
}));
|
|
71
|
+
await replaceSearchTokensForBatch(this.knex, payloads);
|
|
72
|
+
}
|
|
73
|
+
async purge(entityId, tenantId) {
|
|
74
|
+
await this.knex("search_tokens").where({ entity_type: entityId, tenant_id: tenantId }).del();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
TokenSearchStrategy
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=token.strategy.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/strategies/token.strategy.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Knex } from 'knex'\nimport type {\n SearchStrategy,\n SearchStrategyId,\n SearchOptions,\n SearchResult,\n IndexableRecord,\n} from '../types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\n\n/**\n * Configuration for TokenSearchStrategy.\n */\nexport type TokenStrategyConfig = {\n /** Minimum number of query tokens that must match (0-1 ratio, default 0.5) */\n minMatchRatio?: number\n /** Default limit for search results */\n defaultLimit?: number\n}\n\n/**\n * TokenSearchStrategy provides hash-based search using the existing search_tokens table.\n * This strategy is always available and serves as a fallback when other strategies fail.\n *\n * It tokenizes queries into hashes and matches against pre-indexed token hashes,\n * enabling search on encrypted fields without exposing plaintext to external services.\n */\nexport class TokenSearchStrategy implements SearchStrategy {\n readonly id: SearchStrategyId = 'tokens'\n readonly name = 'Token Search'\n readonly priority = 10 // Lowest priority, always available as fallback\n\n private readonly minMatchRatio: number\n private readonly defaultLimit: number\n\n constructor(\n private readonly knex: Knex,\n config?: TokenStrategyConfig,\n ) {\n this.minMatchRatio = config?.minMatchRatio ?? 0.5\n this.defaultLimit = config?.defaultLimit ?? 50\n }\n\n async isAvailable(): Promise<boolean> {\n return true // Always available\n }\n\n async ensureReady(): Promise<void> {\n // No initialization needed\n }\n\n async search(query: string, options: SearchOptions): Promise<SearchResult[]> {\n // Dynamically import tokenization to avoid circular dependencies\n const { tokenizeText } = await import('@open-mercato/shared/lib/search/tokenize')\n const { resolveSearchConfig } = await import('@open-mercato/shared/lib/search/config')\n\n const config = resolveSearchConfig()\n if (!config.enabled) return []\n\n const { hashes } = tokenizeText(query, config)\n if (hashes.length === 0) return []\n\n const minMatches = Math.max(1, Math.ceil(hashes.length * this.minMatchRatio))\n const limit = options.limit ?? this.defaultLimit\n\n let queryBuilder = this.knex('search_tokens')\n .select('entity_type', 'entity_id')\n .count('* as match_count')\n .whereIn('token_hash', hashes)\n .where('tenant_id', options.tenantId)\n .groupBy('entity_type', 'entity_id')\n .havingRaw('COUNT(DISTINCT token_hash) >= ?', [minMatches])\n .orderByRaw('COUNT(DISTINCT token_hash) DESC')\n .limit(limit)\n\n if (options.organizationId) {\n queryBuilder = queryBuilder.where('organization_id', options.organizationId)\n }\n\n if (options.entityTypes?.length) {\n queryBuilder = queryBuilder.whereIn('entity_type', options.entityTypes)\n }\n\n const rows = await queryBuilder as Array<{ entity_type: string; entity_id: string; match_count: string | number }>\n\n return rows.map((row) => {\n const matchCount = typeof row.match_count === 'string'\n ? parseInt(row.match_count, 10)\n : row.match_count\n // Calculate score based on match ratio\n const score = matchCount / hashes.length\n\n return {\n entityId: row.entity_type as EntityId,\n recordId: row.entity_id,\n score,\n source: this.id,\n }\n })\n }\n\n async index(record: IndexableRecord): Promise<void> {\n // Dynamically import to avoid circular dependencies\n const { replaceSearchTokensForRecord } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n await replaceSearchTokensForRecord(this.knex, {\n entityType: record.entityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n doc: record.fields,\n })\n }\n\n async delete(entityId: EntityId, recordId: string, tenantId: string): Promise<void> {\n // Dynamically import to avoid circular dependencies\n const { deleteSearchTokensForRecord } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n await deleteSearchTokensForRecord(this.knex, {\n entityType: entityId,\n recordId,\n tenantId,\n })\n }\n\n async bulkIndex(records: IndexableRecord[]): Promise<void> {\n if (records.length === 0) return\n\n const { replaceSearchTokensForBatch } = await import(\n '@open-mercato/core/modules/query_index/lib/search-tokens'\n )\n\n const payloads = records.map((record) => ({\n entityType: record.entityId,\n recordId: record.recordId,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n doc: record.fields as Record<string, unknown>,\n }))\n\n await replaceSearchTokensForBatch(this.knex, payloads)\n }\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n await this.knex('search_tokens')\n .where({ entity_type: entityId, tenant_id: tenantId })\n .del()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|