sweet-search 0.0.1 → 2.4.1
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/LICENSE +190 -0
- package/NOTICE +23 -0
- package/core/cli.js +51 -0
- package/core/config.js +27 -0
- package/core/embedding/embedding-cache.js +467 -0
- package/core/embedding/embedding-local-model.js +845 -0
- package/core/embedding/embedding-remote.js +492 -0
- package/core/embedding/embedding-service.js +712 -0
- package/core/embedding/embedding-telemetry.js +219 -0
- package/core/embedding/index.js +40 -0
- package/core/graph/community-detector.js +294 -0
- package/core/graph/graph-expansion.js +839 -0
- package/core/graph/graph-extractor.js +2304 -0
- package/core/graph/graph-search.js +2148 -0
- package/core/graph/hcgs-generator.js +666 -0
- package/core/graph/index.js +16 -0
- package/core/graph/leiden-algorithm.js +547 -0
- package/core/graph/relationship-resolver.js +366 -0
- package/core/graph/repo-map.js +408 -0
- package/core/graph/summary-manager.js +549 -0
- package/core/indexing/artifact-builder.js +1054 -0
- package/core/indexing/ast-chunker.js +709 -0
- package/core/indexing/chunking/chunk-builder.js +170 -0
- package/core/indexing/chunking/markdown-chunker.js +503 -0
- package/core/indexing/chunking/plaintext-chunker.js +104 -0
- package/core/indexing/dedup/dedup-phase.js +159 -0
- package/core/indexing/dedup/exemplar-selector.js +65 -0
- package/core/indexing/document-chunker.js +56 -0
- package/core/indexing/incremental-parser.js +390 -0
- package/core/indexing/incremental-tracker.js +761 -0
- package/core/indexing/index-codebase-v21.js +472 -0
- package/core/indexing/index-maintainer.mjs +1674 -0
- package/core/indexing/index.js +90 -0
- package/core/indexing/indexer-ann.js +1077 -0
- package/core/indexing/indexer-build.js +742 -0
- package/core/indexing/indexer-phases.js +800 -0
- package/core/indexing/indexer-pool.js +764 -0
- package/core/indexing/indexer-sparse-gram.js +98 -0
- package/core/indexing/indexer-utils.js +536 -0
- package/core/indexing/indexer-worker.js +148 -0
- package/core/indexing/li-skip-policy.js +225 -0
- package/core/indexing/merkle-tracker.js +244 -0
- package/core/indexing/model-pool.js +166 -0
- package/core/infrastructure/code-graph-repository.js +120 -0
- package/core/infrastructure/codebase-repository.js +131 -0
- package/core/infrastructure/config/dedup.js +54 -0
- package/core/infrastructure/config/embedding.js +298 -0
- package/core/infrastructure/config/graph.js +80 -0
- package/core/infrastructure/config/index.js +82 -0
- package/core/infrastructure/config/indexing.js +8 -0
- package/core/infrastructure/config/platform.js +254 -0
- package/core/infrastructure/config/ranking.js +221 -0
- package/core/infrastructure/config/search.js +396 -0
- package/core/infrastructure/config/translation.js +89 -0
- package/core/infrastructure/config/vector-store.js +114 -0
- package/core/infrastructure/constants.js +86 -0
- package/core/infrastructure/coreml-cascade.js +909 -0
- package/core/infrastructure/coreml-cascade.json +46 -0
- package/core/infrastructure/coreml-provider.js +81 -0
- package/core/infrastructure/db-utils.js +69 -0
- package/core/infrastructure/dedup-hashing.js +83 -0
- package/core/infrastructure/hardware-capability.js +332 -0
- package/core/infrastructure/index.js +104 -0
- package/core/infrastructure/language-patterns/maps.js +121 -0
- package/core/infrastructure/language-patterns/registry-core.js +323 -0
- package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
- package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
- package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
- package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
- package/core/infrastructure/language-patterns/registry.js +19 -0
- package/core/infrastructure/language-patterns.js +141 -0
- package/core/infrastructure/llm-provider.js +733 -0
- package/core/infrastructure/manifest.json +46 -0
- package/core/infrastructure/maxsim.wasm +0 -0
- package/core/infrastructure/model-fetcher.js +423 -0
- package/core/infrastructure/model-registry.js +214 -0
- package/core/infrastructure/native-inference.js +598 -0
- package/core/infrastructure/native-resolver.js +187 -0
- package/core/infrastructure/native-sparse-gram.js +257 -0
- package/core/infrastructure/native-tokenizer.js +160 -0
- package/core/infrastructure/onnx-mutex.js +45 -0
- package/core/infrastructure/onnx-session-utils.js +261 -0
- package/core/infrastructure/ort-pipeline.js +111 -0
- package/core/infrastructure/project-detector.js +102 -0
- package/core/infrastructure/quantization.js +410 -0
- package/core/infrastructure/simd-distance.js +502 -0
- package/core/infrastructure/simd-distance.wasm +0 -0
- package/core/infrastructure/tree-sitter-provider.js +665 -0
- package/core/infrastructure/webgpu-maxsim.js +222 -0
- package/core/query/index.js +35 -0
- package/core/query/intent-detector.js +201 -0
- package/core/query/intent-router.js +156 -0
- package/core/query/query-router-catboost.js +222 -0
- package/core/query/query-router-ml.js +266 -0
- package/core/query/query-router.js +213 -0
- package/core/ranking/cascaded-scorer.js +379 -0
- package/core/ranking/flashrank.js +810 -0
- package/core/ranking/index.js +49 -0
- package/core/ranking/late-interaction-index.js +2383 -0
- package/core/ranking/late-interaction-model.js +812 -0
- package/core/ranking/local-reranker.js +374 -0
- package/core/ranking/mmr.js +379 -0
- package/core/ranking/quality-scorer.js +363 -0
- package/core/search/context-expander.js +1167 -0
- package/core/search/dedup/sibling-expander.js +327 -0
- package/core/search/index.js +16 -0
- package/core/search/search-boost.js +259 -0
- package/core/search/search-cli.js +544 -0
- package/core/search/search-format.js +282 -0
- package/core/search/search-fusion.js +327 -0
- package/core/search/search-hybrid.js +204 -0
- package/core/search/search-pattern-chunks.js +337 -0
- package/core/search/search-pattern-planner.js +439 -0
- package/core/search/search-pattern-prefilter.js +412 -0
- package/core/search/search-pattern-ripgrep.js +663 -0
- package/core/search/search-pattern.js +463 -0
- package/core/search/search-postprocess.js +452 -0
- package/core/search/search-semantic.js +706 -0
- package/core/search/search-server.js +554 -0
- package/core/search/session-daemon-prewarm.mjs +164 -0
- package/core/search/session-warmup.js +595 -0
- package/core/search/sweet-search.js +632 -0
- package/core/search/warmup-metrics.js +532 -0
- package/core/start-server.js +6 -0
- package/core/training/query-router/features/extractor.js +762 -0
- package/core/training/query-router/features/multilingual-patterns.js +431 -0
- package/core/training/query-router/features/text-segmenter.js +303 -0
- package/core/training/query-router/features/unicode-utils.js +383 -0
- package/core/training/query-router/output/v45_router_d4.js +11521 -0
- package/core/training/query-router/output/v46_router_d4.js +11498 -0
- package/core/vector-store/binary-heap.js +227 -0
- package/core/vector-store/binary-hnsw-index.js +1004 -0
- package/core/vector-store/float-vector-store.js +234 -0
- package/core/vector-store/hnsw-index.js +580 -0
- package/core/vector-store/index.js +39 -0
- package/core/vector-store/seismic-index.js +498 -0
- package/core/vocabulary/index.js +84 -0
- package/core/vocabulary/vocab-constants.js +20 -0
- package/core/vocabulary/vocab-miner-extractors.js +375 -0
- package/core/vocabulary/vocab-miner-nl.js +404 -0
- package/core/vocabulary/vocab-miner-utils.js +146 -0
- package/core/vocabulary/vocab-miner.js +574 -0
- package/core/vocabulary/vocab-prewarm-cli.js +110 -0
- package/core/vocabulary/vocab-ranker.js +492 -0
- package/core/vocabulary/vocab-warmer.js +523 -0
- package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
- package/core/vocabulary/vocabulary-utils.js +704 -0
- package/crates/wasm-router/pkg/package.json +13 -0
- package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
- package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
- package/mcp/config-gen.js +121 -0
- package/mcp/server.js +335 -0
- package/mcp/tool-handlers.js +476 -0
- package/package.json +131 -9
- package/scripts/benchmark-harness.js +794 -0
- package/scripts/init.js +1058 -0
- package/scripts/smoke-test.js +435 -0
- package/scripts/uninstall.js +478 -0
- package/scripts/verify-runtime.js +176 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedding Cache - LRU cache, vocabulary, semantic cache, query deduplication.
|
|
3
|
+
* Extracted from embedding-service.js for file size compliance (<500 lines).
|
|
4
|
+
*
|
|
5
|
+
* Telemetry functions live in ./embedding-telemetry.js and are barrel-re-exported
|
|
6
|
+
* below so that existing import sites continue to work unchanged.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { EMBEDDING_CONFIG, DB_PATHS } from '../infrastructure/config/index.js';
|
|
14
|
+
import { fetchModel, getModelCacheDir } from '../infrastructure/model-fetcher.js';
|
|
15
|
+
import { createOrtPipeline, buildFeed } from '../infrastructure/ort-pipeline.js';
|
|
16
|
+
import { meanPoolWithAttentionMask } from './embedding-local-model.js';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// LRU CACHE
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
export class LRUCache {
|
|
23
|
+
constructor(maxSize = 1000) {
|
|
24
|
+
this.maxSize = maxSize;
|
|
25
|
+
this.cache = new Map();
|
|
26
|
+
this.hitCount = new Map();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get(key) {
|
|
30
|
+
if (!this.cache.has(key)) return null;
|
|
31
|
+
const value = this.cache.get(key);
|
|
32
|
+
this.cache.delete(key);
|
|
33
|
+
this.cache.set(key, value);
|
|
34
|
+
this.hitCount.set(key, (this.hitCount.get(key) || 0) + 1);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
set(key, value) {
|
|
39
|
+
if (this.cache.has(key)) this.cache.delete(key);
|
|
40
|
+
if (this.cache.size >= this.maxSize) {
|
|
41
|
+
const oldestEntry = this.cache.entries().next().value;
|
|
42
|
+
if (oldestEntry) this.cache.delete(oldestEntry[0]);
|
|
43
|
+
}
|
|
44
|
+
this.cache.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
has(key) { return this.cache.has(key); }
|
|
48
|
+
getHitCount(key) { return this.hitCount.get(key) || 0; }
|
|
49
|
+
size() { return this.cache.size; }
|
|
50
|
+
clear() { this.cache.clear(); this.hitCount.clear(); }
|
|
51
|
+
|
|
52
|
+
getFrequentQueries(threshold) {
|
|
53
|
+
const frequent = [];
|
|
54
|
+
for (const [key, count] of this.hitCount) {
|
|
55
|
+
if (count >= threshold && this.cache.has(key)) {
|
|
56
|
+
frequent.push({ query: key, count, embedding: this.cache.get(key) });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return frequent.sort((a, b) => b.count - a.count);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// QUERY STATS (Cross-session usage tracking)
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
export class QueryStats {
|
|
68
|
+
constructor(statsPath) {
|
|
69
|
+
this.statsPath = statsPath;
|
|
70
|
+
this.stats = new Map();
|
|
71
|
+
this.loaded = false;
|
|
72
|
+
this.dirty = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async load() {
|
|
76
|
+
if (this.loaded) return;
|
|
77
|
+
try {
|
|
78
|
+
if (existsSync(this.statsPath)) {
|
|
79
|
+
const data = JSON.parse(await fs.readFile(this.statsPath, 'utf-8'));
|
|
80
|
+
for (const [query, count] of Object.entries(data.queries || {})) {
|
|
81
|
+
this.stats.set(query, count);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
|
|
86
|
+
}
|
|
87
|
+
this.loaded = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async save() {
|
|
91
|
+
if (!this.dirty) return;
|
|
92
|
+
const data = { queries: Object.fromEntries(this.stats), lastUpdated: new Date().toISOString() };
|
|
93
|
+
await fs.mkdir(path.dirname(this.statsPath), { recursive: true });
|
|
94
|
+
await fs.writeFile(this.statsPath, JSON.stringify(data));
|
|
95
|
+
this.dirty = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
increment(query) {
|
|
99
|
+
const normalized = query.toLowerCase().trim();
|
|
100
|
+
const count = (this.stats.get(normalized) || 0) + 1;
|
|
101
|
+
this.stats.set(normalized, count);
|
|
102
|
+
this.dirty = true;
|
|
103
|
+
return count;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getCount(query) {
|
|
107
|
+
return this.stats.get(query.toLowerCase().trim()) || 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// VOCABULARY
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
export class Vocabulary {
|
|
116
|
+
constructor(vocabPath) {
|
|
117
|
+
this.vocabPath = vocabPath;
|
|
118
|
+
this.terms = new Map();
|
|
119
|
+
this.metadata = { created: null, lastUpdated: null, version: 2, provider: null };
|
|
120
|
+
this.loaded = false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async load() {
|
|
124
|
+
if (this.loaded) return;
|
|
125
|
+
try {
|
|
126
|
+
if (existsSync(this.vocabPath)) {
|
|
127
|
+
const data = JSON.parse(await fs.readFile(this.vocabPath, 'utf-8'));
|
|
128
|
+
if (data.metadata?.provider && data.metadata.provider !== EMBEDDING_CONFIG.provider) {
|
|
129
|
+
console.log(`Vocabulary: Provider changed (${data.metadata.provider} → ${EMBEDDING_CONFIG.provider}), clearing cache`);
|
|
130
|
+
this.terms.clear();
|
|
131
|
+
} else {
|
|
132
|
+
this.metadata = data.metadata || this.metadata;
|
|
133
|
+
for (const [term, embedding] of Object.entries(data.terms || {})) {
|
|
134
|
+
this.terms.set(term, embedding);
|
|
135
|
+
}
|
|
136
|
+
console.log(`Vocabulary: Loaded ${this.terms.size} pre-computed embeddings`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.log(`Vocabulary: Starting fresh (${err.message})`);
|
|
141
|
+
}
|
|
142
|
+
this.loaded = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async save() {
|
|
146
|
+
this.metadata.lastUpdated = new Date().toISOString();
|
|
147
|
+
this.metadata.provider = EMBEDDING_CONFIG.provider;
|
|
148
|
+
if (!this.metadata.created) this.metadata.created = this.metadata.lastUpdated;
|
|
149
|
+
const data = { metadata: this.metadata, terms: Object.fromEntries(this.terms) };
|
|
150
|
+
await fs.mkdir(path.dirname(this.vocabPath), { recursive: true });
|
|
151
|
+
await fs.writeFile(this.vocabPath, JSON.stringify(data, null, 2));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get(term) { return this.terms.get(this.normalize(term)) || null; }
|
|
155
|
+
set(term, embedding) { this.terms.set(this.normalize(term), embedding); }
|
|
156
|
+
has(term) { return this.terms.has(this.normalize(term)); }
|
|
157
|
+
normalize(term) { return term.toLowerCase().trim(); }
|
|
158
|
+
size() { return this.terms.size; }
|
|
159
|
+
|
|
160
|
+
async addDefaultTerms(embedFn) {
|
|
161
|
+
const defaultTerms = [
|
|
162
|
+
'AuthService', 'EmployeeService', 'LoginService', 'UserService',
|
|
163
|
+
'ProcessService', 'EventService', 'ScreenshotService', 'SessionService',
|
|
164
|
+
'AuthController', 'EmployeeController', 'LoginController', 'UserController',
|
|
165
|
+
'authentication', 'authorization', 'login', 'logout', 'password',
|
|
166
|
+
'JWT', 'token', 'session', 'employee', 'monitoring', 'tracking',
|
|
167
|
+
'gRPC', 'REST', 'API', 'endpoint', 'request', 'response',
|
|
168
|
+
'bot detection', 'heuristic', 'trajectory', 'mouse movement',
|
|
169
|
+
'Spring Boot', 'React', 'Java', 'JavaScript', 'proto', 'protobuf',
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
let added = 0;
|
|
173
|
+
for (const term of defaultTerms) {
|
|
174
|
+
if (!this.has(term)) {
|
|
175
|
+
const embedding = await embedFn(term);
|
|
176
|
+
this.set(term, embedding);
|
|
177
|
+
added++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (added > 0) {
|
|
181
|
+
await this.save();
|
|
182
|
+
console.log(`Vocabulary: Added ${added} default terms`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// =============================================================================
|
|
188
|
+
// SEMANTIC CACHE (Local Model for Cache Keys)
|
|
189
|
+
// =============================================================================
|
|
190
|
+
|
|
191
|
+
export class SemanticCache {
|
|
192
|
+
constructor(options = {}) {
|
|
193
|
+
this.threshold = options.threshold ?? 0.85;
|
|
194
|
+
this.maxSize = options.maxSize ?? 500;
|
|
195
|
+
this.entries = [];
|
|
196
|
+
this.localModel = null;
|
|
197
|
+
this.loadingModel = false;
|
|
198
|
+
this.loadPromise = null;
|
|
199
|
+
this.stats = { hits: 0, misses: 0, localModelCalls: 0 };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async getLocalModel() {
|
|
203
|
+
if (this.localModel) return this.localModel;
|
|
204
|
+
if (this.loadingModel && this.loadPromise) return this.loadPromise;
|
|
205
|
+
|
|
206
|
+
this.loadingModel = true;
|
|
207
|
+
this.loadPromise = (async () => {
|
|
208
|
+
const start = Date.now();
|
|
209
|
+
console.log('[SemanticCache] Loading local model for cache keys...');
|
|
210
|
+
await fetchModel('all-minilm-l6-v2');
|
|
211
|
+
const cacheDir = getModelCacheDir('Xenova/all-MiniLM-L6-v2');
|
|
212
|
+
this.localModel = await createOrtPipeline(
|
|
213
|
+
join(cacheDir, 'onnx', 'model_quantized.onnx'),
|
|
214
|
+
join(cacheDir, 'tokenizer.json'),
|
|
215
|
+
{ graphOptimizationLevel: 'all' },
|
|
216
|
+
);
|
|
217
|
+
console.log(`[SemanticCache] Local model loaded in ${Date.now() - start}ms`);
|
|
218
|
+
this.loadingModel = false;
|
|
219
|
+
return this.localModel;
|
|
220
|
+
})();
|
|
221
|
+
|
|
222
|
+
return this.loadPromise;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async computeLocalEmbedding(text) {
|
|
226
|
+
const { session, tokenizer } = await this.getLocalModel();
|
|
227
|
+
this.stats.localModelCalls++;
|
|
228
|
+
const tokenized = tokenizer(text, { padding: true, truncation: true, max_length: 256 });
|
|
229
|
+
const feed = buildFeed(tokenized, session.inputNames);
|
|
230
|
+
const output = await session.run(feed);
|
|
231
|
+
const tensor = output[session.outputNames[0]];
|
|
232
|
+
// Mean pooling with L2 normalization
|
|
233
|
+
const pooled = meanPoolWithAttentionMask(tensor, tokenized.attention_mask, true);
|
|
234
|
+
return Array.from(pooled.data);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
cosineSimilarity(a, b) {
|
|
238
|
+
let dot = 0, normA = 0, normB = 0;
|
|
239
|
+
for (let i = 0; i < a.length; i++) {
|
|
240
|
+
dot += a[i] * b[i];
|
|
241
|
+
normA += a[i] * a[i];
|
|
242
|
+
normB += b[i] * b[i];
|
|
243
|
+
}
|
|
244
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async findSimilar(text) {
|
|
248
|
+
const localEmb = await this.computeLocalEmbedding(text);
|
|
249
|
+
|
|
250
|
+
if (this.entries.length === 0) {
|
|
251
|
+
this.stats.misses++;
|
|
252
|
+
return { voyageEmb: null, localEmb };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let bestMatch = null;
|
|
256
|
+
let bestSimilarity = -1;
|
|
257
|
+
|
|
258
|
+
for (const entry of this.entries) {
|
|
259
|
+
const similarity = this.cosineSimilarity(localEmb, entry.localEmb);
|
|
260
|
+
if (similarity > this.threshold && similarity > bestSimilarity) {
|
|
261
|
+
bestSimilarity = similarity;
|
|
262
|
+
bestMatch = entry;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (bestMatch) {
|
|
267
|
+
this.stats.hits++;
|
|
268
|
+
return {
|
|
269
|
+
voyageEmb: bestMatch.voyageEmb,
|
|
270
|
+
similarity: bestSimilarity,
|
|
271
|
+
matchedQuery: bestMatch.query,
|
|
272
|
+
localEmb,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.stats.misses++;
|
|
277
|
+
return { voyageEmb: null, localEmb };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
add(text, localEmb, voyageEmb) {
|
|
281
|
+
if (this.entries.length >= this.maxSize) {
|
|
282
|
+
this.entries.shift();
|
|
283
|
+
}
|
|
284
|
+
this.entries.push({
|
|
285
|
+
query: text,
|
|
286
|
+
localEmb,
|
|
287
|
+
voyageEmb,
|
|
288
|
+
addedAt: Date.now(),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
getStats() {
|
|
293
|
+
const total = this.stats.hits + this.stats.misses;
|
|
294
|
+
const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(1) : 0;
|
|
295
|
+
return {
|
|
296
|
+
...this.stats,
|
|
297
|
+
size: this.entries.length,
|
|
298
|
+
hitRate: `${hitRate}%`,
|
|
299
|
+
threshold: this.threshold,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// =============================================================================
|
|
305
|
+
// QUERY DEDUPLICATION
|
|
306
|
+
// =============================================================================
|
|
307
|
+
|
|
308
|
+
export class QueryDeduplicator {
|
|
309
|
+
constructor() {
|
|
310
|
+
this.inflight = new Map();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
get(text) {
|
|
314
|
+
return this.inflight.get(text) || null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
set(text, promise) {
|
|
318
|
+
this.inflight.set(text, promise);
|
|
319
|
+
promise.finally(() => this.inflight.delete(text));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
has(text) {
|
|
323
|
+
return this.inflight.has(text);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// =============================================================================
|
|
328
|
+
// SINGLETONS
|
|
329
|
+
// =============================================================================
|
|
330
|
+
|
|
331
|
+
export const queryCache = new LRUCache(EMBEDDING_CONFIG.cache?.maxSize || 1000);
|
|
332
|
+
export const vocabulary = new Vocabulary(DB_PATHS.vocabulary);
|
|
333
|
+
export const semanticCache = new SemanticCache({ threshold: 0.85, maxSize: 500 });
|
|
334
|
+
export const queryDeduplicator = new QueryDeduplicator();
|
|
335
|
+
export const queryStats = new QueryStats(DB_PATHS.vocabulary.replace('.json', '-stats.json'));
|
|
336
|
+
/** Mutable cache counters — access via getCacheStatsRef() to make singleton dependency explicit. */
|
|
337
|
+
const _cacheStats = { hits: 0, misses: 0, vocabularyHits: 0, apiCalls: 0 };
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Returns a reference to the mutable stats object.
|
|
341
|
+
* Prefer this getter over a bare export so that the singleton
|
|
342
|
+
* dependency is explicit and mockable in tests.
|
|
343
|
+
*/
|
|
344
|
+
export function getCacheStatsRef() {
|
|
345
|
+
return _cacheStats;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** @deprecated Use getCacheStatsRef(). Kept for backward compatibility during migration. */
|
|
349
|
+
export const cacheStats = _cacheStats;
|
|
350
|
+
// Integration point: search modes (lexical/semantic/hybrid) should call
|
|
351
|
+
// recordQueryTelemetry(mode, hit, latencyMs) after each query to feed
|
|
352
|
+
// per-mode cache-hit telemetry for the vocabulary prewarm pipeline.
|
|
353
|
+
|
|
354
|
+
// =============================================================================
|
|
355
|
+
// TELEMETRY (barrel re-exports from ./embedding-telemetry.js)
|
|
356
|
+
// =============================================================================
|
|
357
|
+
|
|
358
|
+
export { telemetryStats, recordQueryTelemetry, getTelemetryReport, resetTelemetryStats, flushTelemetry } from './embedding-telemetry.js';
|
|
359
|
+
|
|
360
|
+
// =============================================================================
|
|
361
|
+
// CACHE MANAGEMENT
|
|
362
|
+
// =============================================================================
|
|
363
|
+
|
|
364
|
+
export function getCacheStats(circuitBreakerState) {
|
|
365
|
+
const total = cacheStats.hits + cacheStats.misses;
|
|
366
|
+
const hitRate = total > 0 ? ((cacheStats.hits + cacheStats.vocabularyHits) / total * 100).toFixed(1) : 0;
|
|
367
|
+
return {
|
|
368
|
+
...cacheStats,
|
|
369
|
+
total,
|
|
370
|
+
hitRate: `${hitRate}%`,
|
|
371
|
+
cacheSize: queryCache.size(),
|
|
372
|
+
vocabularySize: vocabulary.size(),
|
|
373
|
+
semanticCache: semanticCache.getStats(),
|
|
374
|
+
circuitBreaker: circuitBreakerState || null,
|
|
375
|
+
provider: EMBEDDING_CONFIG.provider,
|
|
376
|
+
model: EMBEDDING_CONFIG.model,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function getSemanticCacheStats() {
|
|
381
|
+
return semanticCache.getStats();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function clearCache() {
|
|
385
|
+
queryCache.clear();
|
|
386
|
+
cacheStats.hits = 0;
|
|
387
|
+
cacheStats.misses = 0;
|
|
388
|
+
cacheStats.vocabularyHits = 0;
|
|
389
|
+
cacheStats.apiCalls = 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function getFrequentQueries(threshold = 3) {
|
|
393
|
+
return queryCache.getFrequentQueries(threshold);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* @param {string} term
|
|
398
|
+
* @param {Function} embedFn - Injected embed function (avoids circular dep with facade)
|
|
399
|
+
*/
|
|
400
|
+
export async function addToVocabulary(term, embedFn) {
|
|
401
|
+
await vocabulary.load();
|
|
402
|
+
if (!vocabulary.has(term)) {
|
|
403
|
+
const embedding = await embedFn(term);
|
|
404
|
+
vocabulary.set(term, embedding);
|
|
405
|
+
await vocabulary.save();
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* @param {string[]} terms
|
|
413
|
+
* @param {Function} embedFn - Injected embed function (avoids circular dep with facade)
|
|
414
|
+
*/
|
|
415
|
+
export async function expandVocabulary(terms, embedFn) {
|
|
416
|
+
await vocabulary.load();
|
|
417
|
+
let added = 0;
|
|
418
|
+
for (const term of terms) {
|
|
419
|
+
if (!vocabulary.has(term)) {
|
|
420
|
+
const embedding = await embedFn(term);
|
|
421
|
+
vocabulary.set(term, embedding);
|
|
422
|
+
added++;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (added > 0) await vocabulary.save();
|
|
426
|
+
return added;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export async function autoPersistFrequentQueries(threshold = 2) {
|
|
430
|
+
const frequent = queryCache.getFrequentQueries(threshold);
|
|
431
|
+
if (frequent.length === 0) return { persisted: 0, total: 0 };
|
|
432
|
+
|
|
433
|
+
await vocabulary.load();
|
|
434
|
+
let persisted = 0;
|
|
435
|
+
|
|
436
|
+
for (const { query, embedding } of frequent) {
|
|
437
|
+
if (!vocabulary.has(query)) {
|
|
438
|
+
vocabulary.set(query, embedding);
|
|
439
|
+
persisted++;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (persisted > 0) {
|
|
444
|
+
await vocabulary.save();
|
|
445
|
+
console.log(`Auto-persist: Saved ${persisted} frequent queries to vocabulary`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return { persisted, total: frequent.length };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let exitHandlerRegistered = false;
|
|
452
|
+
export function registerAutoPersistOnExit(threshold = 2) {
|
|
453
|
+
if (exitHandlerRegistered) return;
|
|
454
|
+
exitHandlerRegistered = true;
|
|
455
|
+
|
|
456
|
+
const persist = async () => {
|
|
457
|
+
try {
|
|
458
|
+
await autoPersistFrequentQueries(threshold);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
// Ignore errors on exit
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
process.on('beforeExit', persist);
|
|
465
|
+
process.on('SIGINT', async () => { await persist(); process.exit(0); });
|
|
466
|
+
process.on('SIGTERM', async () => { await persist(); process.exit(0); });
|
|
467
|
+
}
|