sweet-search 0.0.1 → 2.3.0
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 +587 -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,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEISMIC Sparse Vector Search Index
|
|
3
|
+
*
|
|
4
|
+
* Pure JS implementation of SEISMIC's core data structures for Maximum Inner
|
|
5
|
+
* Product Search (MIPS) on sparse vectors. Organizes inverted lists into
|
|
6
|
+
* geometrically-cohesive blocks, each with a summary vector that enables
|
|
7
|
+
* aggressive block skipping during query.
|
|
8
|
+
*
|
|
9
|
+
* Retrieval pathway: FTS5 (lexical) + HNSW (dense) + SEISMIC (sparse)
|
|
10
|
+
*
|
|
11
|
+
* Reference: SEISMIC paper - block-based inverted index with summary pruning
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// TOP-K MIN-HEAP
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Min-heap for efficient top-k tracking during search.
|
|
20
|
+
* Maintains the k highest-scoring items; root is always the minimum.
|
|
21
|
+
*/
|
|
22
|
+
export class TopKHeap {
|
|
23
|
+
/** @param {number} k - Maximum number of results to keep */
|
|
24
|
+
constructor(k) {
|
|
25
|
+
this.k = k;
|
|
26
|
+
/** @type {Array<{ id: string, score: number }>} */
|
|
27
|
+
this.heap = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Current minimum score in the heap (pruning threshold) */
|
|
31
|
+
threshold() {
|
|
32
|
+
return this.heap.length < this.k ? -Infinity : this.heap[0].score;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {string} id @param {number} score */
|
|
36
|
+
push(id, score) {
|
|
37
|
+
if (this.heap.length < this.k) {
|
|
38
|
+
this.heap.push({ id, score });
|
|
39
|
+
this._bubbleUp(this.heap.length - 1);
|
|
40
|
+
} else if (score > this.heap[0].score) {
|
|
41
|
+
this.heap[0] = { id, score };
|
|
42
|
+
this._sinkDown(0);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Return results sorted descending by score */
|
|
47
|
+
results() {
|
|
48
|
+
return this.heap.slice().sort((a, b) => b.score - a.score);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @private */
|
|
52
|
+
_bubbleUp(i) {
|
|
53
|
+
while (i > 0) {
|
|
54
|
+
const parent = (i - 1) >> 1;
|
|
55
|
+
if (this.heap[i].score < this.heap[parent].score) {
|
|
56
|
+
[this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]];
|
|
57
|
+
i = parent;
|
|
58
|
+
} else break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @private */
|
|
63
|
+
_sinkDown(i) {
|
|
64
|
+
const n = this.heap.length;
|
|
65
|
+
while (true) {
|
|
66
|
+
let smallest = i;
|
|
67
|
+
const left = 2 * i + 1;
|
|
68
|
+
const right = 2 * i + 2;
|
|
69
|
+
if (left < n && this.heap[left].score < this.heap[smallest].score) smallest = left;
|
|
70
|
+
if (right < n && this.heap[right].score < this.heap[smallest].score) smallest = right;
|
|
71
|
+
if (smallest === i) break;
|
|
72
|
+
[this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
|
|
73
|
+
i = smallest;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// SPARSE DOT PRODUCT
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sparse dot product (MIPS score) between two sparse vectors.
|
|
84
|
+
* Only iterates over the smaller vector's dimensions.
|
|
85
|
+
* @param {Map<number, number>} a
|
|
86
|
+
* @param {Map<number, number>} b
|
|
87
|
+
* @returns {number}
|
|
88
|
+
*/
|
|
89
|
+
export function sparseDotProduct(a, b) {
|
|
90
|
+
let score = 0;
|
|
91
|
+
// Iterate over the smaller map for efficiency
|
|
92
|
+
const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];
|
|
93
|
+
for (const [dim, weight] of smaller) {
|
|
94
|
+
const otherWeight = larger.get(dim);
|
|
95
|
+
if (otherWeight !== undefined) {
|
|
96
|
+
score += weight * otherWeight;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return score;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// BLOCK
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A contiguous group of postings within an inverted list.
|
|
108
|
+
* Stores a summary (max weight) for upper-bound pruning.
|
|
109
|
+
*/
|
|
110
|
+
export class Block {
|
|
111
|
+
/** @param {Array<{ docId: string, weight: number }>} postings */
|
|
112
|
+
constructor(postings) {
|
|
113
|
+
this.postings = postings;
|
|
114
|
+
this._summary = 0;
|
|
115
|
+
for (let i = 0; i < postings.length; i++) {
|
|
116
|
+
if (postings[i].weight > this._summary) {
|
|
117
|
+
this._summary = postings[i].weight;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Maximum weight in this block (upper bound for scoring) */
|
|
123
|
+
get summary() {
|
|
124
|
+
return this._summary;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether this block can be skipped given the query weight and current threshold.
|
|
129
|
+
* If the best possible contribution from this block (summary * queryWeight)
|
|
130
|
+
* is less than the threshold, the block cannot improve the result set.
|
|
131
|
+
*
|
|
132
|
+
* Note: SeismicIndex.alpha is intentionally NOT used here. The summary already
|
|
133
|
+
* represents the maximum weight in the block, which is the tightest upper bound
|
|
134
|
+
* for pruning. Multiplying by alpha would weaken the bound (more false skips).
|
|
135
|
+
* Alpha is reserved for future use in score normalization at the index level,
|
|
136
|
+
* not at the block-pruning level.
|
|
137
|
+
*
|
|
138
|
+
* @param {number} queryWeight - Query weight for this term
|
|
139
|
+
* @param {number} currentThreshold - Current minimum score to beat
|
|
140
|
+
* @returns {boolean}
|
|
141
|
+
*/
|
|
142
|
+
canSkip(queryWeight, currentThreshold) {
|
|
143
|
+
return this._summary * queryWeight < currentThreshold;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// =============================================================================
|
|
148
|
+
// INVERTED LIST
|
|
149
|
+
// =============================================================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Per-term inverted list: posting entries sorted by weight descending,
|
|
153
|
+
* organized into blocks for pruning.
|
|
154
|
+
*/
|
|
155
|
+
export class InvertedList {
|
|
156
|
+
/** @param {number} termId - The dimension/term index */
|
|
157
|
+
constructor(termId) {
|
|
158
|
+
this.termId = termId;
|
|
159
|
+
/** @type {Array<{ docId: string, weight: number }>} */
|
|
160
|
+
this.postings = [];
|
|
161
|
+
/** @type {Block[]} */
|
|
162
|
+
this.blocks = [];
|
|
163
|
+
this.built = false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @param {string} docId @param {number} weight */
|
|
167
|
+
addPosting(docId, weight) {
|
|
168
|
+
this.postings.push({ docId, weight });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Sort postings by weight descending and split into fixed-size blocks.
|
|
173
|
+
* @param {number} blockSize
|
|
174
|
+
*/
|
|
175
|
+
buildBlocks(blockSize) {
|
|
176
|
+
this.postings.sort((a, b) => b.weight - a.weight);
|
|
177
|
+
this.blocks = [];
|
|
178
|
+
for (let i = 0; i < this.postings.length; i += blockSize) {
|
|
179
|
+
this.blocks.push(new Block(this.postings.slice(i, i + blockSize)));
|
|
180
|
+
}
|
|
181
|
+
this.built = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// =============================================================================
|
|
186
|
+
// SEISMIC INDEX
|
|
187
|
+
// =============================================================================
|
|
188
|
+
|
|
189
|
+
const SERIALIZE_MAGIC = 'SEISM';
|
|
190
|
+
const SERIALIZE_VERSION = 1;
|
|
191
|
+
const MAX_DESERIALIZE_DOCS = 1_000_000;
|
|
192
|
+
const MAX_DESERIALIZE_DIMS_PER_DOC = 1_000_000;
|
|
193
|
+
|
|
194
|
+
function ensureReadable(buffer, off, bytes, what) {
|
|
195
|
+
if (!Number.isInteger(off) || off < 0) {
|
|
196
|
+
throw new Error(`Corrupt SEISMIC index: invalid offset while reading ${what}`);
|
|
197
|
+
}
|
|
198
|
+
if (!Number.isInteger(bytes) || bytes < 0) {
|
|
199
|
+
throw new Error(`Corrupt SEISMIC index: invalid length while reading ${what}`);
|
|
200
|
+
}
|
|
201
|
+
if (off + bytes > buffer.length) {
|
|
202
|
+
throw new Error(`Corrupt SEISMIC index: truncated while reading ${what}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* SEISMIC sparse vector search index.
|
|
208
|
+
*
|
|
209
|
+
* Build from sparse vectors (Map<dimension, weight>), then query with
|
|
210
|
+
* sparse query vectors. Uses block-based inverted lists with summary
|
|
211
|
+
* pruning for efficient MIPS.
|
|
212
|
+
*/
|
|
213
|
+
export class SeismicIndex {
|
|
214
|
+
/**
|
|
215
|
+
* @param {Object} [options]
|
|
216
|
+
* @param {number} [options.blockSize=64] - Postings per block
|
|
217
|
+
* @param {number} [options.alpha=0.8] - Importance fraction (reserved for future use)
|
|
218
|
+
*/
|
|
219
|
+
constructor(options = {}) {
|
|
220
|
+
this.blockSize = options.blockSize || 64;
|
|
221
|
+
this.alpha = options.alpha ?? 0.8;
|
|
222
|
+
|
|
223
|
+
/** @type {Map<number, InvertedList>} term → inverted list */
|
|
224
|
+
this.lists = new Map();
|
|
225
|
+
/** @type {Map<string, Map<number, number>>} docId → sparse vector */
|
|
226
|
+
this.docs = new Map();
|
|
227
|
+
this.built = false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Build the index from an array of sparse vectors.
|
|
232
|
+
* @param {Array<{ id: string, vector: Map<number, number> }>} sparseVectors
|
|
233
|
+
*/
|
|
234
|
+
build(sparseVectors) {
|
|
235
|
+
this.lists.clear();
|
|
236
|
+
this.docs.clear();
|
|
237
|
+
|
|
238
|
+
for (const { id, vector } of sparseVectors) {
|
|
239
|
+
this.docs.set(id, vector);
|
|
240
|
+
for (const [dim, weight] of vector) {
|
|
241
|
+
if (weight === 0) continue;
|
|
242
|
+
let list = this.lists.get(dim);
|
|
243
|
+
if (!list) {
|
|
244
|
+
list = new InvertedList(dim);
|
|
245
|
+
this.lists.set(dim, list);
|
|
246
|
+
}
|
|
247
|
+
list.addPosting(id, weight);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Build blocks for every inverted list
|
|
252
|
+
for (const list of this.lists.values()) {
|
|
253
|
+
list.buildBlocks(this.blockSize);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.built = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Query the index for top-k results by MIPS.
|
|
261
|
+
* @param {Map<number, number>} queryVector - Sparse query vector
|
|
262
|
+
* @param {number} [k=10] - Number of results
|
|
263
|
+
* @returns {Array<{ id: string, score: number }>}
|
|
264
|
+
*/
|
|
265
|
+
query(queryVector, k = 10) {
|
|
266
|
+
if (!this.built || this.docs.size === 0) return [];
|
|
267
|
+
if (queryVector.size === 0) return [];
|
|
268
|
+
|
|
269
|
+
// Accumulate scores per document via block-structured inverted lists
|
|
270
|
+
const scores = new Map();
|
|
271
|
+
|
|
272
|
+
// Sort query dimensions by weight descending (process most important first)
|
|
273
|
+
const queryDims = [...queryVector.entries()]
|
|
274
|
+
.filter(([, w]) => w !== 0)
|
|
275
|
+
.sort((a, b) => b[1] - a[1]);
|
|
276
|
+
|
|
277
|
+
let blocksExamined = 0;
|
|
278
|
+
|
|
279
|
+
for (const [dim, queryWeight] of queryDims) {
|
|
280
|
+
const list = this.lists.get(dim);
|
|
281
|
+
if (!list) continue;
|
|
282
|
+
|
|
283
|
+
for (const block of list.blocks) {
|
|
284
|
+
blocksExamined++;
|
|
285
|
+
for (const { docId, weight } of block.postings) {
|
|
286
|
+
scores.set(docId, (scores.get(docId) || 0) + queryWeight * weight);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Build final top-k from accumulated scores
|
|
292
|
+
const heap = new TopKHeap(k);
|
|
293
|
+
for (const [docId, score] of scores) {
|
|
294
|
+
heap.push(docId, score);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this._lastQueryStats = { blocksExamined };
|
|
298
|
+
return heap.results();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Approximate query with block skipping for single-dimension queries.
|
|
303
|
+
* Uses SEISMIC block pruning: blocks sorted by summary descending within
|
|
304
|
+
* each inverted list enable early termination when the remaining blocks
|
|
305
|
+
* cannot beat the current threshold.
|
|
306
|
+
*
|
|
307
|
+
* Sound for single-dimension queries; for multi-dimension queries the caller
|
|
308
|
+
* should verify results or use exact query().
|
|
309
|
+
*
|
|
310
|
+
* @param {Map<number, number>} queryVector - Sparse query vector
|
|
311
|
+
* @param {number} [k=10]
|
|
312
|
+
* @returns {{ results: Array<{ id: string, score: number }>, blocksExamined: number, blocksSkipped: number }}
|
|
313
|
+
*/
|
|
314
|
+
approximateQuery(queryVector, k = 10) {
|
|
315
|
+
if (!this.built || this.docs.size === 0) {
|
|
316
|
+
return { results: [], blocksExamined: 0, blocksSkipped: 0 };
|
|
317
|
+
}
|
|
318
|
+
if (queryVector.size === 0) {
|
|
319
|
+
return { results: [], blocksExamined: 0, blocksSkipped: 0 };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const scores = new Map();
|
|
323
|
+
let threshold = 0;
|
|
324
|
+
|
|
325
|
+
const queryDims = [...queryVector.entries()]
|
|
326
|
+
.filter(([, w]) => w !== 0)
|
|
327
|
+
.sort((a, b) => b[1] - a[1]);
|
|
328
|
+
|
|
329
|
+
let blocksExamined = 0;
|
|
330
|
+
let blocksSkipped = 0;
|
|
331
|
+
|
|
332
|
+
for (const [dim, queryWeight] of queryDims) {
|
|
333
|
+
const list = this.lists.get(dim);
|
|
334
|
+
if (!list) continue;
|
|
335
|
+
|
|
336
|
+
for (const block of list.blocks) {
|
|
337
|
+
// Blocks are sorted by summary descending. Once a block's max
|
|
338
|
+
// possible contribution falls below threshold, early-terminate this list.
|
|
339
|
+
if (block.canSkip(queryWeight, threshold)) {
|
|
340
|
+
blocksSkipped += list.blocks.length - list.blocks.indexOf(block);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
blocksExamined++;
|
|
345
|
+
for (const { docId, weight } of block.postings) {
|
|
346
|
+
scores.set(docId, (scores.get(docId) || 0) + queryWeight * weight);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Update threshold from accumulated scores after each dimension
|
|
351
|
+
if (scores.size >= k) {
|
|
352
|
+
const vals = [...scores.values()];
|
|
353
|
+
vals.sort((a, b) => b - a);
|
|
354
|
+
threshold = vals[k - 1];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const heap = new TopKHeap(k);
|
|
359
|
+
for (const [docId, score] of scores) {
|
|
360
|
+
heap.push(docId, score);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { results: heap.results(), blocksExamined, blocksSkipped };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Stats from the last query (for testing block skipping) */
|
|
367
|
+
get lastQueryStats() {
|
|
368
|
+
return this._lastQueryStats || { blocksExamined: 0, blocksSkipped: 0 };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Index statistics.
|
|
373
|
+
* @returns {{ totalDocs: number, totalTerms: number, totalBlocks: number, avgBlockSize: number }}
|
|
374
|
+
*/
|
|
375
|
+
getStats() {
|
|
376
|
+
let totalBlocks = 0;
|
|
377
|
+
let totalPostings = 0;
|
|
378
|
+
for (const list of this.lists.values()) {
|
|
379
|
+
totalBlocks += list.blocks.length;
|
|
380
|
+
totalPostings += list.postings.length;
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
totalDocs: this.docs.size,
|
|
384
|
+
totalTerms: this.lists.size,
|
|
385
|
+
totalBlocks,
|
|
386
|
+
avgBlockSize: totalBlocks > 0 ? totalPostings / totalBlocks : 0,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Serialize the index to a Buffer for persistence.
|
|
392
|
+
* Format: [magic(5B)][version(1B)][blockSize(4B)][alpha(8B)]
|
|
393
|
+
* [numDocs(4B)][ ...docs... ][numLists(4B)][ ...lists... ]
|
|
394
|
+
* @returns {Buffer}
|
|
395
|
+
*/
|
|
396
|
+
serialize() {
|
|
397
|
+
const parts = [];
|
|
398
|
+
|
|
399
|
+
// Header: magic(5B) + version(1B) + blockSize(4B) + alpha(8B) = 18B
|
|
400
|
+
const header = Buffer.alloc(18);
|
|
401
|
+
header.write(SERIALIZE_MAGIC, 0, 5, 'ascii');
|
|
402
|
+
header.writeUInt8(SERIALIZE_VERSION, 5);
|
|
403
|
+
header.writeUInt32BE(this.blockSize, 6);
|
|
404
|
+
header.writeDoubleBE(this.alpha, 10);
|
|
405
|
+
parts.push(header);
|
|
406
|
+
|
|
407
|
+
// Documents: [numDocs(4B)][foreach: idLen(2B) id(utf8) numDims(4B) [dim(4B) weight(8B)]...]
|
|
408
|
+
const docsBuf = [Buffer.alloc(4)];
|
|
409
|
+
docsBuf[0].writeUInt32BE(this.docs.size, 0);
|
|
410
|
+
for (const [id, vec] of this.docs) {
|
|
411
|
+
const idBuf = Buffer.from(id, 'utf8');
|
|
412
|
+
const entry = Buffer.alloc(2 + idBuf.length + 4 + vec.size * 12);
|
|
413
|
+
let off = 0;
|
|
414
|
+
entry.writeUInt16BE(idBuf.length, off); off += 2;
|
|
415
|
+
idBuf.copy(entry, off); off += idBuf.length;
|
|
416
|
+
entry.writeUInt32BE(vec.size, off); off += 4;
|
|
417
|
+
for (const [dim, weight] of vec) {
|
|
418
|
+
entry.writeUInt32BE(dim, off); off += 4;
|
|
419
|
+
entry.writeDoubleBE(weight, off); off += 8;
|
|
420
|
+
}
|
|
421
|
+
docsBuf.push(entry);
|
|
422
|
+
}
|
|
423
|
+
parts.push(...docsBuf);
|
|
424
|
+
|
|
425
|
+
return Buffer.concat(parts);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Deserialize a Buffer back into a SeismicIndex.
|
|
430
|
+
* @param {Buffer} buffer
|
|
431
|
+
* @returns {SeismicIndex}
|
|
432
|
+
*/
|
|
433
|
+
static deserialize(buffer) {
|
|
434
|
+
let off = 0;
|
|
435
|
+
|
|
436
|
+
// Validate magic
|
|
437
|
+
ensureReadable(buffer, off, 5, 'magic');
|
|
438
|
+
const magic = buffer.toString('ascii', off, off + 5); off += 5;
|
|
439
|
+
if (magic !== SERIALIZE_MAGIC) {
|
|
440
|
+
throw new Error('Invalid SEISMIC index magic bytes');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
ensureReadable(buffer, off, 1, 'version');
|
|
444
|
+
const version = buffer.readUInt8(off); off += 1;
|
|
445
|
+
if (version !== SERIALIZE_VERSION) {
|
|
446
|
+
throw new Error(`Unsupported SEISMIC index version: ${version}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
ensureReadable(buffer, off, 4, 'block size');
|
|
450
|
+
const blockSize = buffer.readUInt32BE(off); off += 4;
|
|
451
|
+
if (!Number.isInteger(blockSize) || blockSize <= 0 || blockSize > 65536) {
|
|
452
|
+
throw new Error(`Corrupt SEISMIC index: invalid block size ${blockSize}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
ensureReadable(buffer, off, 8, 'alpha');
|
|
456
|
+
const alpha = buffer.readDoubleBE(off); off += 8;
|
|
457
|
+
if (!Number.isFinite(alpha)) {
|
|
458
|
+
throw new Error('Corrupt SEISMIC index: invalid alpha');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const index = new SeismicIndex({ blockSize, alpha });
|
|
462
|
+
|
|
463
|
+
// Read documents
|
|
464
|
+
ensureReadable(buffer, off, 4, 'document count');
|
|
465
|
+
const numDocs = buffer.readUInt32BE(off); off += 4;
|
|
466
|
+
if (numDocs > MAX_DESERIALIZE_DOCS) {
|
|
467
|
+
throw new Error(`Corrupt SEISMIC index: document count too large (${numDocs})`);
|
|
468
|
+
}
|
|
469
|
+
const sparseVectors = [];
|
|
470
|
+
|
|
471
|
+
for (let d = 0; d < numDocs; d++) {
|
|
472
|
+
ensureReadable(buffer, off, 2, `doc ${d} id length`);
|
|
473
|
+
const idLen = buffer.readUInt16BE(off); off += 2;
|
|
474
|
+
|
|
475
|
+
ensureReadable(buffer, off, idLen, `doc ${d} id`);
|
|
476
|
+
const id = buffer.toString('utf8', off, off + idLen); off += idLen;
|
|
477
|
+
|
|
478
|
+
ensureReadable(buffer, off, 4, `doc ${d} dimension count`);
|
|
479
|
+
const numDims = buffer.readUInt32BE(off); off += 4;
|
|
480
|
+
if (numDims > MAX_DESERIALIZE_DIMS_PER_DOC) {
|
|
481
|
+
throw new Error(`Corrupt SEISMIC index: dimension count too large (${numDims})`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
ensureReadable(buffer, off, numDims * 12, `doc ${d} vector payload`);
|
|
485
|
+
const vector = new Map();
|
|
486
|
+
for (let i = 0; i < numDims; i++) {
|
|
487
|
+
const dim = buffer.readUInt32BE(off); off += 4;
|
|
488
|
+
const weight = buffer.readDoubleBE(off); off += 8;
|
|
489
|
+
vector.set(dim, weight);
|
|
490
|
+
}
|
|
491
|
+
sparseVectors.push({ id, vector });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Rebuild index from documents
|
|
495
|
+
index.build(sparseVectors);
|
|
496
|
+
return index;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary Domain Barrel
|
|
3
|
+
*
|
|
4
|
+
* Re-exports public API from all vocabulary sub-modules.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// vocab-miner.js
|
|
8
|
+
export {
|
|
9
|
+
default as vocabMiner,
|
|
10
|
+
mineStructural,
|
|
11
|
+
mineSymbols,
|
|
12
|
+
mineCodeGraph,
|
|
13
|
+
mineGit,
|
|
14
|
+
mineAll,
|
|
15
|
+
} from './vocab-miner.js';
|
|
16
|
+
|
|
17
|
+
// Also re-export the barrel re-exports from vocab-miner.js
|
|
18
|
+
export {
|
|
19
|
+
splitIdentifier, STOP_WORDS, SOURCE_EXTENSIONS,
|
|
20
|
+
addTerm, mergeTerms, termsToArray, walkShallow,
|
|
21
|
+
} from './vocab-miner-utils.js';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
extractImports, extractExports, extractDefinitions, extractConstants,
|
|
25
|
+
extractNpmDeps, extractCargoDeps, extractGoDeps, extractPipDeps, extractPyprojectDeps,
|
|
26
|
+
} from './vocab-miner-extractors.js';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
mineNLContent, computeNLContentHash, extractNLText, isSecretLike,
|
|
30
|
+
detectScript, tokenizeNL, extractBigrams, extractTrigrams,
|
|
31
|
+
SECRET_PATTERNS, NL_MINING_TIMEOUT_MS,
|
|
32
|
+
} from './vocab-miner-nl.js';
|
|
33
|
+
|
|
34
|
+
// vocab-ranker.js
|
|
35
|
+
export {
|
|
36
|
+
classifyWarmMode,
|
|
37
|
+
rankIdentifiers,
|
|
38
|
+
rankCommunityPhrases,
|
|
39
|
+
rankAll,
|
|
40
|
+
_internals as rankerInternals,
|
|
41
|
+
} from './vocab-ranker.js';
|
|
42
|
+
|
|
43
|
+
// vocab-warmer.js
|
|
44
|
+
export {
|
|
45
|
+
default as vocabWarmer,
|
|
46
|
+
warmLexical,
|
|
47
|
+
warmSemantic,
|
|
48
|
+
warmHybrid,
|
|
49
|
+
warmFromCache,
|
|
50
|
+
runFullWarmup,
|
|
51
|
+
saveBinaryArtifact,
|
|
52
|
+
DATA_DIR,
|
|
53
|
+
ARTIFACT_PATHS,
|
|
54
|
+
_loadEntityMetadata,
|
|
55
|
+
} from './vocab-warmer.js';
|
|
56
|
+
|
|
57
|
+
// vocab-warmup-orchestrator.js
|
|
58
|
+
export {
|
|
59
|
+
runFullWarmup as runFullWarmupOrchestrator,
|
|
60
|
+
saveBinaryArtifact as saveBinaryArtifactOrchestrator,
|
|
61
|
+
} from './vocab-warmup-orchestrator.js';
|
|
62
|
+
|
|
63
|
+
// vocabulary-utils.js
|
|
64
|
+
export {
|
|
65
|
+
default as vocabularyUtils,
|
|
66
|
+
BinaryVocabulary,
|
|
67
|
+
mineQueryLogs,
|
|
68
|
+
extractEntities,
|
|
69
|
+
generateAllTerms,
|
|
70
|
+
callVoyageAPI,
|
|
71
|
+
migrateJsonToBinary,
|
|
72
|
+
warmupFull,
|
|
73
|
+
warmupLearn,
|
|
74
|
+
getStats,
|
|
75
|
+
} from './vocabulary-utils.js';
|
|
76
|
+
|
|
77
|
+
// vocab-constants.js
|
|
78
|
+
export {
|
|
79
|
+
DATA_DIR as vocabDataDir,
|
|
80
|
+
ARTIFACT_PATHS as vocabArtifactPaths,
|
|
81
|
+
} from './vocab-constants.js';
|
|
82
|
+
|
|
83
|
+
// vocab-prewarm-cli.js
|
|
84
|
+
export { handlePrewarmVocabCli } from './vocab-prewarm-cli.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary Prewarm Constants
|
|
3
|
+
*
|
|
4
|
+
* Shared constants for vocab-warmer.js and vocab-warmup-orchestrator.js.
|
|
5
|
+
* Extracted to break the circular dependency between those two modules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { PROJECT_ROOT } from '../infrastructure/config/index.js';
|
|
10
|
+
|
|
11
|
+
export const DATA_DIR = path.join(PROJECT_ROOT, '.sweet-search');
|
|
12
|
+
|
|
13
|
+
export const ARTIFACT_PATHS = {
|
|
14
|
+
identifiersBin: path.join(DATA_DIR, 'vocab-identifiers.bin'),
|
|
15
|
+
identifiersMeta: path.join(DATA_DIR, 'vocab-identifiers.meta.json'),
|
|
16
|
+
semanticSeedsBin: path.join(DATA_DIR, 'vocab-semantic-seeds.bin'),
|
|
17
|
+
semanticSeedsMeta: path.join(DATA_DIR, 'vocab-semantic-seeds.meta.json'),
|
|
18
|
+
communities: path.join(DATA_DIR, 'communities.json'),
|
|
19
|
+
dynamicVocab: path.join(DATA_DIR, 'vocab-dynamic.json'),
|
|
20
|
+
};
|