sweet-search 2.6.8 → 2.6.9
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/core/graph/graph-extractor.js +24 -14
- package/core/graph/hcgs-generator.js +4 -3
- package/core/incremental-indexing/application/production-reconciler.mjs +29 -9
- package/core/incremental-indexing/domain/encoder-input.mjs +9 -4
- package/core/indexing/ast-chunker.js +65 -62
- package/core/indexing/indexer-build.js +18 -4
- package/core/indexing/indexer-phases.js +45 -24
- package/core/indexing/indexer-utils.js +96 -6
- package/core/indexing/indexing-file-policy.js +30 -7
- package/core/infrastructure/code-graph-repository.js +18 -18
- package/core/infrastructure/db-utils.js +29 -0
- package/core/infrastructure/simd-distance.js +35 -21
- package/core/infrastructure/tombstone-bitmap-reader.js +3 -1
- package/core/ranking/file-kind-ranking.js +14 -2
- package/core/ranking/late-interaction-index.js +86 -15
- package/core/ranking/mmr.js +15 -10
- package/core/search/search-anchor.js +21 -3
- package/core/search/search-boost.js +25 -13
- package/core/search/search-fusion.js +13 -5
- package/core/search/search-read-semantic.js +13 -12
- package/core/search/search-server.js +2 -1
- package/core/vector-store/binary-hnsw-index.js +13 -4
- package/core/vector-store/float-vector-store.js +9 -5
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +11 -0
- package/package.json +8 -8
|
@@ -201,6 +201,18 @@ function envFloat(name, fallback, min = 0, max = 1) {
|
|
|
201
201
|
return Number.isFinite(parsed) && parsed >= min && parsed <= max ? parsed : fallback;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// Query-term derivation is pure in `query`; memoize the last query so the
|
|
205
|
+
// per-result map in applyPostFusionBoosts doesn't re-split identical input.
|
|
206
|
+
let _lastIdTermsQuery = null;
|
|
207
|
+
let _lastIdTerms = null;
|
|
208
|
+
function identifierTermsForQuery(query) {
|
|
209
|
+
if (query !== _lastIdTermsQuery) {
|
|
210
|
+
_lastIdTermsQuery = query;
|
|
211
|
+
_lastIdTerms = new Set(splitIdentifierTerms(query));
|
|
212
|
+
}
|
|
213
|
+
return _lastIdTerms;
|
|
214
|
+
}
|
|
215
|
+
|
|
204
216
|
function splitIdentifierTerms(value) {
|
|
205
217
|
return String(value || '')
|
|
206
218
|
.replace(/_[0-9a-f]{8}(?=\.[^.]+$|$)/gi, '')
|
|
@@ -233,7 +245,7 @@ export function computeIdentifierAgreementBoost(result, query) {
|
|
|
233
245
|
const weight = envFloat('SWEET_SEARCH_IDENTIFIER_AGREEMENT_BOOST', 0.40, 0, 1);
|
|
234
246
|
if (weight === 0) return 1.0;
|
|
235
247
|
|
|
236
|
-
const queryTerms =
|
|
248
|
+
const queryTerms = identifierTermsForQuery(query);
|
|
237
249
|
if (queryTerms.size === 0) return 1.0;
|
|
238
250
|
|
|
239
251
|
const fileName = (result.file || result.path || result.metadata?.file || '')
|
|
@@ -290,22 +302,22 @@ export function computeDefinitionBoost(result, queryLower, queryTokens) {
|
|
|
290
302
|
/**
|
|
291
303
|
* Compute syntax boost (PHASE_1_FIXES helper)
|
|
292
304
|
*/
|
|
305
|
+
const DEFINITION_SIGNATURE_PATTERNS = [
|
|
306
|
+
/\b(?:public|private|protected)?\s*(?:abstract|final)?\s*class\s+(\w+)/,
|
|
307
|
+
/\b(?:public|private|protected)?\s*interface\s+(\w+)/,
|
|
308
|
+
/\b(?:public|private|protected)?\s*enum\s+(\w+)/,
|
|
309
|
+
/\bclass\s+(\w+)/,
|
|
310
|
+
/\bfunction\s+(\w+)/,
|
|
311
|
+
/\bexport\s+(?:default\s+)?(?:class|function)\s+(\w+)/,
|
|
312
|
+
/\binterface\s+(\w+)/,
|
|
313
|
+
/\btype\s+(\w+)\s*=/,
|
|
314
|
+
];
|
|
315
|
+
|
|
293
316
|
export function computeSyntaxBoost(result, queryTokens) {
|
|
294
317
|
const signature = (result.signature || '').toLowerCase();
|
|
295
318
|
if (!signature) return 1.0;
|
|
296
319
|
|
|
297
|
-
const
|
|
298
|
-
/\b(?:public|private|protected)?\s*(?:abstract|final)?\s*class\s+(\w+)/,
|
|
299
|
-
/\b(?:public|private|protected)?\s*interface\s+(\w+)/,
|
|
300
|
-
/\b(?:public|private|protected)?\s*enum\s+(\w+)/,
|
|
301
|
-
/\bclass\s+(\w+)/,
|
|
302
|
-
/\bfunction\s+(\w+)/,
|
|
303
|
-
/\bexport\s+(?:default\s+)?(?:class|function)\s+(\w+)/,
|
|
304
|
-
/\binterface\s+(\w+)/,
|
|
305
|
-
/\btype\s+(\w+)\s*=/,
|
|
306
|
-
];
|
|
307
|
-
|
|
308
|
-
for (const pattern of definitionPatterns) {
|
|
320
|
+
for (const pattern of DEFINITION_SIGNATURE_PATTERNS) {
|
|
309
321
|
const match = signature.match(pattern);
|
|
310
322
|
if (match && match[1]) {
|
|
311
323
|
const definedName = match[1].toLowerCase();
|
|
@@ -109,20 +109,24 @@ export function quantileNormalize(scores, lowQuantile = 0.05, highQuantile = 0.9
|
|
|
109
109
|
export function convexCombination(lexicalResults, semanticResults, routeType = 'mixed', options = {}) {
|
|
110
110
|
const alpha = options.alpha ?? ROUTE_ALPHAS[routeType] ?? 0.5;
|
|
111
111
|
|
|
112
|
-
// Build score maps
|
|
112
|
+
// Build score + result maps in one key derivation per result
|
|
113
113
|
const lexicalScoreMap = new Map();
|
|
114
114
|
const semanticScoreMap = new Map();
|
|
115
|
+
const lexResultMap = new Map();
|
|
116
|
+
const semResultMap = new Map();
|
|
115
117
|
const allKeys = new Set();
|
|
116
118
|
|
|
117
119
|
for (const r of lexicalResults) {
|
|
118
120
|
const key = this.getResultKey(r);
|
|
119
121
|
lexicalScoreMap.set(key, r.score);
|
|
122
|
+
lexResultMap.set(key, r);
|
|
120
123
|
allKeys.add(key);
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
for (const r of semanticResults) {
|
|
124
127
|
const key = this.getResultKey(r);
|
|
125
128
|
semanticScoreMap.set(key, r.score);
|
|
129
|
+
semResultMap.set(key, r);
|
|
126
130
|
allKeys.add(key);
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -148,8 +152,6 @@ export function convexCombination(lexicalResults, semanticResults, routeType = '
|
|
|
148
152
|
|
|
149
153
|
// Compute CC scores
|
|
150
154
|
const ccResults = [];
|
|
151
|
-
const lexResultMap = new Map(lexicalResults.map(r => [this.getResultKey(r), r]));
|
|
152
|
-
const semResultMap = new Map(semanticResults.map(r => [this.getResultKey(r), r]));
|
|
153
155
|
|
|
154
156
|
for (const key of allKeys) {
|
|
155
157
|
const lexScore = lexicalScoreMap.get(key);
|
|
@@ -253,7 +255,10 @@ export function rrfFusion(lexicalResults, semanticResults, k = 60) {
|
|
|
253
255
|
});
|
|
254
256
|
|
|
255
257
|
return [...results.values()]
|
|
256
|
-
.map(r =>
|
|
258
|
+
.map(r => {
|
|
259
|
+
const score = scores.get(this.getResultKey(r));
|
|
260
|
+
return { ...r, score, ccScore: score };
|
|
261
|
+
})
|
|
257
262
|
.sort((a, b) => b.score - a.score);
|
|
258
263
|
}
|
|
259
264
|
|
|
@@ -310,7 +315,10 @@ export function robustCCFusion(lexicalResults, semanticResults, routeType = 'mix
|
|
|
310
315
|
|
|
311
316
|
return {
|
|
312
317
|
results: [...results.values()]
|
|
313
|
-
.map(r =>
|
|
318
|
+
.map(r => {
|
|
319
|
+
const score = combinedScores.get(this.getResultKey(r));
|
|
320
|
+
return { ...r, score, ccScore: score, alpha };
|
|
321
|
+
})
|
|
314
322
|
.sort((a, b) => b.score - a.score),
|
|
315
323
|
method: 'cc_robust',
|
|
316
324
|
fallbackReason: null,
|
|
@@ -155,8 +155,7 @@ function _lateInteractionIndexPath(projectRoot, manifest) {
|
|
|
155
155
|
return path.join(_stateDirForProject(root), path.basename(DB_PATHS.lateInteraction));
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function _sourceStaleness(projectRoot, filePathRel) {
|
|
159
|
-
const manifest = _readReconcileManifest(projectRoot);
|
|
158
|
+
function _sourceStaleness(projectRoot, filePathRel, manifest = _readReconcileManifest(projectRoot)) {
|
|
160
159
|
const publishedMs = Date.parse(manifest?.publishedAt || '');
|
|
161
160
|
if (!Number.isFinite(publishedMs)) return null;
|
|
162
161
|
try {
|
|
@@ -178,9 +177,8 @@ function _sourceStaleness(projectRoot, filePathRel) {
|
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
const _repos = new Map();
|
|
181
|
-
function _getRepo(projectRoot) {
|
|
180
|
+
function _getRepo(projectRoot, manifest = _readReconcileManifest(projectRoot)) {
|
|
182
181
|
const key = _projectKey(projectRoot);
|
|
183
|
-
const manifest = _readReconcileManifest(projectRoot);
|
|
184
182
|
const dbPath = _codebasePathForProject(projectRoot, manifest);
|
|
185
183
|
const baseDbPath = _defaultCodebasePathForProject(projectRoot);
|
|
186
184
|
let entry = _repos.get(key);
|
|
@@ -202,9 +200,8 @@ let _liIndex = null;
|
|
|
202
200
|
let _liInitPromise = null;
|
|
203
201
|
let _liProjectKey = null;
|
|
204
202
|
let _liManifestEpoch = null;
|
|
205
|
-
async function _getLateInteractionIndex(projectRoot) {
|
|
203
|
+
async function _getLateInteractionIndex(projectRoot, manifest = _readReconcileManifest(projectRoot)) {
|
|
206
204
|
const projectKey = _projectKey(projectRoot);
|
|
207
|
-
const manifest = _readReconcileManifest(projectRoot);
|
|
208
205
|
const manifestEpoch = Number.isInteger(manifest?.epoch) ? manifest.epoch : null;
|
|
209
206
|
const samePin = _liProjectKey === projectKey && _liManifestEpoch === manifestEpoch;
|
|
210
207
|
if (_liIndex !== null && samePin) return _liIndex || null;
|
|
@@ -331,8 +328,8 @@ function _escapeRegex(s) {
|
|
|
331
328
|
// Candidate enumeration — load chunk metadata + per-chunk on-disk text slice
|
|
332
329
|
// ---------------------------------------------------------------------------
|
|
333
330
|
|
|
334
|
-
async function _loadFileChunks(filePathRel, projectRoot) {
|
|
335
|
-
const repo = _getRepo(projectRoot);
|
|
331
|
+
async function _loadFileChunks(filePathRel, projectRoot, manifest = _readReconcileManifest(projectRoot)) {
|
|
332
|
+
const repo = _getRepo(projectRoot, manifest);
|
|
336
333
|
if (!repo) return { chunks: [], language: null };
|
|
337
334
|
const rows = repo.getChunksByFilePath(filePathRel);
|
|
338
335
|
if (rows.length === 0) return { chunks: [], language: null };
|
|
@@ -445,9 +442,9 @@ function _scoreSymbol(chunks, queryTerms, queryRaw) {
|
|
|
445
442
|
return scores;
|
|
446
443
|
}
|
|
447
444
|
|
|
448
|
-
async function _scoreLateInteraction(chunks, query, projectRoot, lateInteractionIndexOverride = null) {
|
|
445
|
+
async function _scoreLateInteraction(chunks, query, projectRoot, lateInteractionIndexOverride = null, manifest = undefined) {
|
|
449
446
|
if (chunks.length === 0) return { scores: new Map(), ran: false };
|
|
450
|
-
const liIndex = lateInteractionIndexOverride || await _getLateInteractionIndex(projectRoot);
|
|
447
|
+
const liIndex = lateInteractionIndexOverride || await _getLateInteractionIndex(projectRoot, manifest);
|
|
451
448
|
if (!liIndex) return { scores: new Map(), ran: false };
|
|
452
449
|
|
|
453
450
|
// Only score chunks whose IDs actually appear in the LI index. Use the
|
|
@@ -636,7 +633,10 @@ async function _readSemanticUnpinned(req) {
|
|
|
636
633
|
const projectRoot = req.projectRoot || process.cwd();
|
|
637
634
|
_ensurePersistedLiModelApplied(projectRoot);
|
|
638
635
|
const filePathRel = _projectRelative(req.path, projectRoot);
|
|
639
|
-
|
|
636
|
+
// One manifest read per request — _sourceStaleness/_getRepo/_getLateInteractionIndex
|
|
637
|
+
// each re-read the same file otherwise (it cannot change mid-request usefully).
|
|
638
|
+
const reconcileManifest = _readReconcileManifest(projectRoot);
|
|
639
|
+
const staleness = _sourceStaleness(projectRoot, filePathRel, reconcileManifest);
|
|
640
640
|
|
|
641
641
|
const topK = req.topK ?? DEFAULTS.topK;
|
|
642
642
|
const threshold = req.threshold ?? DEFAULTS.threshold;
|
|
@@ -646,7 +646,7 @@ async function _readSemanticUnpinned(req) {
|
|
|
646
646
|
const verbose = !!req.verbose;
|
|
647
647
|
|
|
648
648
|
const tLoad0 = performance.now();
|
|
649
|
-
const { chunks, language, totalLines, fileText } = await _loadFileChunks(filePathRel, projectRoot);
|
|
649
|
+
const { chunks, language, totalLines, fileText } = await _loadFileChunks(filePathRel, projectRoot, reconcileManifest);
|
|
650
650
|
const tLoad1 = performance.now();
|
|
651
651
|
|
|
652
652
|
// No chunks at all → fall back to plain read so the caller still gets
|
|
@@ -692,6 +692,7 @@ async function _readSemanticUnpinned(req) {
|
|
|
692
692
|
req.query,
|
|
693
693
|
projectRoot,
|
|
694
694
|
req._lateInteractionIndex || null,
|
|
695
|
+
reconcileManifest,
|
|
695
696
|
);
|
|
696
697
|
const tLi1 = performance.now();
|
|
697
698
|
|
|
@@ -37,6 +37,8 @@ export const SEARCH_SERVER_MAX_URL_LENGTH = 16_384;
|
|
|
37
37
|
export const SEARCH_SERVER_MAX_QUERY_LENGTH = 2_000;
|
|
38
38
|
export const SEARCH_SERVER_MAX_READ_PATH_LENGTH = 8_192;
|
|
39
39
|
|
|
40
|
+
const AGENT_FORMATS = new Set(['agent', 'agent_preview', 'agent_full', 'agent_full_xl']);
|
|
41
|
+
|
|
40
42
|
function canonicalProjectRoot(root) {
|
|
41
43
|
const resolved = path.resolve(root || process.cwd());
|
|
42
44
|
try {
|
|
@@ -686,7 +688,6 @@ export async function startServer() {
|
|
|
686
688
|
|
|
687
689
|
// Agent mode: context packaging (ColGrep agent format)
|
|
688
690
|
const rawFormat = url.searchParams.get('format');
|
|
689
|
-
const AGENT_FORMATS = new Set(['agent', 'agent_preview', 'agent_full', 'agent_full_xl']);
|
|
690
691
|
const agentFormat = AGENT_FORMATS.has(rawFormat) ? rawFormat : undefined;
|
|
691
692
|
const tokenBudget = url.searchParams.has('budget')
|
|
692
693
|
? parseInt(url.searchParams.get('budget'), 10)
|
|
@@ -236,6 +236,11 @@ export class BinaryHNSWIndex {
|
|
|
236
236
|
|
|
237
237
|
// Pre-allocated visited list (generation-stamped, reused across searches)
|
|
238
238
|
this._visitedList = new VisitedList(this.maxElements);
|
|
239
|
+
// Pooled search heaps — searchLayer/searchLayerQuery are synchronous and
|
|
240
|
+
// never re-entered, so one pair (auto-growing) serves all calls. Same
|
|
241
|
+
// single-flight contract as _visitedList.
|
|
242
|
+
this._candHeap = new TypedMinHeap(64);
|
|
243
|
+
this._resHeap = new TypedMaxHeap(64);
|
|
239
244
|
|
|
240
245
|
// Asymmetric binary quantization calibration data
|
|
241
246
|
this.centroid = null; // Float32Array — dataset centroid
|
|
@@ -652,11 +657,13 @@ export class BinaryHNSWIndex {
|
|
|
652
657
|
const startDist = hammingDistance(this.vectors[startNode].binary, targetBinary);
|
|
653
658
|
|
|
654
659
|
// candidates = min-heap (explore closest first)
|
|
655
|
-
const candidates =
|
|
660
|
+
const candidates = this._candHeap;
|
|
661
|
+
candidates.clear();
|
|
656
662
|
candidates.insert(startNode, startDist);
|
|
657
663
|
|
|
658
664
|
// results = max-heap of size ef (peek furthest, replaceMax when closer found)
|
|
659
|
-
const results =
|
|
665
|
+
const results = this._resHeap;
|
|
666
|
+
results.clear();
|
|
660
667
|
results.insert(startNode, startDist);
|
|
661
668
|
|
|
662
669
|
while (candidates.size > 0) {
|
|
@@ -894,8 +901,10 @@ export class BinaryHNSWIndex {
|
|
|
894
901
|
visited.mark(startNode);
|
|
895
902
|
|
|
896
903
|
const startDist = hammingDistance(this.vectors[startNode].binary, queryBinary);
|
|
897
|
-
const candidates =
|
|
898
|
-
|
|
904
|
+
const candidates = this._candHeap;
|
|
905
|
+
candidates.clear();
|
|
906
|
+
const results = this._resHeap;
|
|
907
|
+
results.clear();
|
|
899
908
|
candidates.insert(startNode, startDist);
|
|
900
909
|
results.insert(startNode, startDist);
|
|
901
910
|
|
|
@@ -117,12 +117,16 @@ export class FloatVectorStore {
|
|
|
117
117
|
const idsPath = binPath.replace(/\.bin$/, '.ids.json');
|
|
118
118
|
if (!existsSync(idsPath)) return false;
|
|
119
119
|
|
|
120
|
-
// Read binary
|
|
120
|
+
// Read binary. Large reads return an unpooled Buffer that owns its whole
|
|
121
|
+
// ArrayBuffer — adopt it directly; only pooled (offset/short) buffers need
|
|
122
|
+
// the defensive copy.
|
|
121
123
|
const fileBuffer = await readFile(binPath);
|
|
122
|
-
this.buffer = fileBuffer.buffer.
|
|
123
|
-
fileBuffer.
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
this.buffer = (fileBuffer.byteOffset === 0 && fileBuffer.byteLength === fileBuffer.buffer.byteLength)
|
|
125
|
+
? fileBuffer.buffer
|
|
126
|
+
: fileBuffer.buffer.slice(
|
|
127
|
+
fileBuffer.byteOffset,
|
|
128
|
+
fileBuffer.byteOffset + fileBuffer.byteLength
|
|
129
|
+
);
|
|
126
130
|
|
|
127
131
|
// Parse header
|
|
128
132
|
const header = new DataView(this.buffer, 0, HEADER_SIZE);
|
|
@@ -19,6 +19,17 @@ import {
|
|
|
19
19
|
parseLineRange, looksLikeOption,
|
|
20
20
|
} from './_ss-argparse.mjs';
|
|
21
21
|
|
|
22
|
+
// Diagnostic-log isolation (agent-facing tools). The Sweet Search engine emits
|
|
23
|
+
// model/index load banners via console.log → stdout ("LateInteraction: Loaded…",
|
|
24
|
+
// "BinaryHNSW: Loaded…", "Warming up embedding…", "✓ Vocabulary loaded…"). When the
|
|
25
|
+
// engine runs IN-PROCESS in this wrapper, that stdout IS the agent's tool result, so a
|
|
26
|
+
// cold start crowds out the actual hits and the agent falls back to native tools
|
|
27
|
+
// (diagnosed root cause of the sweet≈native tie). The wrappers emit REAL results only
|
|
28
|
+
// via process.stdout.write, so redirect every console.log to stderr; the ss-* shell
|
|
29
|
+
// wrapper's `2>` suppresses it on success → the agent sees clean results, warm or cold.
|
|
30
|
+
// (Production search/grep/find already avoid this: the daemon is spawned stdio:'ignore'.)
|
|
31
|
+
console.log = (...args) => process.stderr.write(args.map(a => (typeof a === 'string' ? a : String(a))).join(' ') + '\n');
|
|
32
|
+
|
|
22
33
|
// 8-char SHA1 prefix is enough for grouping identical queries across
|
|
23
34
|
// benchmark runs without bloating artifacts.
|
|
24
35
|
function shortQueryHash(q) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sweet-search",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.9",
|
|
4
4
|
"description": "Sweet Search - SOTA Hybrid Code Search Engine with WASM CatBoost Query Router, Semantic/Lexical/Structural Search, and Multilingual Support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "core/search/sweet-search.js",
|
|
@@ -167,13 +167,13 @@
|
|
|
167
167
|
"vitest": "^4.0.16"
|
|
168
168
|
},
|
|
169
169
|
"optionalDependencies": {
|
|
170
|
-
"@sweet-search/native-darwin-arm64": "2.6.
|
|
171
|
-
"@sweet-search/native-darwin-x64": "2.6.
|
|
172
|
-
"@sweet-search/native-linux-arm64-gnu": "2.6.
|
|
173
|
-
"@sweet-search/native-linux-arm64-gnu-cuda": "2.6.
|
|
174
|
-
"@sweet-search/native-linux-x64-gnu": "2.6.
|
|
175
|
-
"@sweet-search/native-linux-x64-gnu-cuda": "2.6.
|
|
176
|
-
"@sweet-search/bg-priority": "2.6.
|
|
170
|
+
"@sweet-search/native-darwin-arm64": "2.6.9",
|
|
171
|
+
"@sweet-search/native-darwin-x64": "2.6.9",
|
|
172
|
+
"@sweet-search/native-linux-arm64-gnu": "2.6.9",
|
|
173
|
+
"@sweet-search/native-linux-arm64-gnu-cuda": "2.6.9",
|
|
174
|
+
"@sweet-search/native-linux-x64-gnu": "2.6.9",
|
|
175
|
+
"@sweet-search/native-linux-x64-gnu-cuda": "2.6.9",
|
|
176
|
+
"@sweet-search/bg-priority": "2.6.9"
|
|
177
177
|
},
|
|
178
178
|
"engines": {
|
|
179
179
|
"node": ">=18.0.0"
|