sweet-search 2.6.10 → 2.6.11

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.
@@ -15,7 +15,7 @@ import { existsSync, createWriteStream, createReadStream, statSync } from 'fs';
15
15
  import readline from 'readline';
16
16
  import path from 'path';
17
17
  import { DB_PATHS, LATE_INTERACTION_CONFIG } from '../infrastructure/config/index.js';
18
- import { wasmMaxSimF32, wasmMaxSimDequantPerToken, wasmMaxSimDequant4Bit, nativeMaxSimBatch, nativeMaxSimBatchPerToken, nativeMaxSimBatch4Bit, initWasm, isNativeMaxSimAvailable, isNativePerTokenAvailable, isNative4BitAvailable } from '../infrastructure/simd-distance.js';
18
+ import { wasmMaxSimF32, wasmMaxSimDequantPerToken, wasmMaxSimDequant4Bit, wasmMaxSimPrepareQuery, nativeMaxSimBatch, nativeMaxSimBatchPerToken, nativeMaxSimBatch4Bit, initWasm, isNativeMaxSimAvailable, isNativePerTokenAvailable, isNative4BitAvailable } from '../infrastructure/simd-distance.js';
19
19
  import { fastRotate, generateSignVector, calibrateWUSH, wushRotate } from '../infrastructure/quantization.js';
20
20
  import { poolTokens } from './late-interaction-model.js';
21
21
  import { loadBitmap, isSet } from '../infrastructure/tombstone-bitmap-reader.js';
@@ -1281,6 +1281,21 @@ export class LateInteractionIndex {
1281
1281
  }
1282
1282
 
1283
1283
  _loadSegmentStaleBitmap(segmentPath) {
1284
+ // Per-query memo: scoreWithLateInteraction checks the same segment 2-3×
1285
+ // per candidate, and each uncached check costs a statSync. Within one
1286
+ // scoring pass a single freshness check per segment is equivalent — a
1287
+ // tombstone landing mid-pass races identically either way.
1288
+ const memo = this._staleQueryMemo;
1289
+ if (memo) {
1290
+ if (memo.has(segmentPath)) return memo.get(segmentPath);
1291
+ const bitmap = this._loadSegmentStaleBitmapUncached(segmentPath);
1292
+ memo.set(segmentPath, bitmap);
1293
+ return bitmap;
1294
+ }
1295
+ return this._loadSegmentStaleBitmapUncached(segmentPath);
1296
+ }
1297
+
1298
+ _loadSegmentStaleBitmapUncached(segmentPath) {
1284
1299
  const sidecarPath = segmentPath + '.stale.bin';
1285
1300
  let stat;
1286
1301
  try {
@@ -1581,6 +1596,12 @@ export class LateInteractionIndex {
1581
1596
  // don't support importance weighting, so we must use the JS-tier weighted path.
1582
1597
  const nativeScored = new Set();
1583
1598
 
1599
+ // One tombstone freshness check (statSync) per segment for this scoring
1600
+ // pass; cleared after the synchronous scoring loops below. An exception
1601
+ // path can leave it set — the next pass overwrites it, and staleness is
1602
+ // bounded by one scoring pass either way.
1603
+ this._staleQueryMemo = new Map();
1604
+
1584
1605
  // Resolve a doc-lookup ID for each candidate. Graph-expanded candidates
1585
1606
  // carry `_liChunkId` (a chunk id pointing into the LI index) while their
1586
1607
  // public `id` is the entity id from the code graph. Honouring _liChunkId
@@ -1632,6 +1653,11 @@ export class LateInteractionIndex {
1632
1653
 
1633
1654
  // Tier 2 & 3: WASM fused dequant or JS fallback for candidates not scored natively.
1634
1655
  // Try WASM fused kernels first (avoids JS-side dequant), fall back to JS dequant + wasmMaxSimF32.
1656
+ // Stage the query bytes in WASM memory once — per-candidate calls below
1657
+ // skip the redundant Q×dim×4 query memcpy when the session id matches.
1658
+ const wasmQuerySession = (useFlatPath && !this.useTokenWeights)
1659
+ ? wasmMaxSimPrepareQuery(queryFlat, effectiveQueryTokens.length, scoringDim)
1660
+ : 0;
1635
1661
  for (const candidate of toScore) {
1636
1662
  if (nativeScored.has(candidate.id)) continue;
1637
1663
  const docId = lookupDocIdOf(candidate);
@@ -1649,7 +1675,7 @@ export class LateInteractionIndex {
1649
1675
  if (doc.quantBits === 4 && doc.minArray && doc.tokenNorms) {
1650
1676
  const wasmScore = wasmMaxSimDequant4Bit(
1651
1677
  queryFlat, doc.tokens, doc.minArray, doc.scaleArray, doc.tokenNorms,
1652
- effectiveQueryTokens.length, doc.numTokens, doc.dim,
1678
+ effectiveQueryTokens.length, doc.numTokens, doc.dim, wasmQuerySession,
1653
1679
  );
1654
1680
  if (wasmScore !== null) { pushScored(candidate, wasmScore); continue; }
1655
1681
  }
@@ -1658,7 +1684,7 @@ export class LateInteractionIndex {
1658
1684
  if (doc.minArray && doc.tokenNorms && doc.quantBits !== 4) {
1659
1685
  const wasmScore = wasmMaxSimDequantPerToken(
1660
1686
  queryFlat, doc.tokens, doc.minArray, doc.scaleArray, doc.tokenNorms,
1661
- effectiveQueryTokens.length, doc.numTokens, doc.dim,
1687
+ effectiveQueryTokens.length, doc.numTokens, doc.dim, wasmQuerySession,
1662
1688
  );
1663
1689
  if (wasmScore !== null) { pushScored(candidate, wasmScore); continue; }
1664
1690
  }
@@ -1693,6 +1719,8 @@ export class LateInteractionIndex {
1693
1719
  }
1694
1720
  }
1695
1721
 
1722
+ this._staleQueryMemo = null;
1723
+
1696
1724
  for (const candidate of pruned) pushFallback(candidate, { _pruned: true });
1697
1725
 
1698
1726
  scored.sort((a, b) => b.lateInteractionScore - a.lateInteractionScore);
@@ -302,8 +302,15 @@ export async function semanticSearch3Stage(query, options = {}) {
302
302
  const validIndices = [];
303
303
  let missingInt8Count = 0;
304
304
 
305
+ // One stale-bitmap snapshot for the whole pool (getInt8Vector would stat
306
+ // the bitmap file once per candidate). Falls back per-id for injected
307
+ // index doubles without the batch method.
308
+ const poolIds = stage2Candidates.map((c) => c.id);
309
+ const poolInt8 = typeof this.binaryHnswIndex.getInt8VectorsForIds === 'function'
310
+ ? this.binaryHnswIndex.getInt8VectorsForIds(poolIds)
311
+ : poolIds.map((id) => this.binaryHnswIndex.getInt8Vector(id));
305
312
  for (let i = 0; i < stage2Candidates.length; i++) {
306
- const int8Vector = this.binaryHnswIndex.getInt8Vector(stage2Candidates[i].id);
313
+ const int8Vector = poolInt8[i];
307
314
  if (int8Vector) {
308
315
  int8Vectors.push(int8Vector);
309
316
  validIndices.push(i);
@@ -164,11 +164,20 @@ export class TypedMaxHeap {
164
164
  }
165
165
  }
166
166
 
167
- // Drain heap into sorted array (ascending by distance). Destructive.
167
+ // Drain heap into sorted order (ascending by distance). Destructive.
168
+ // The returned keys/vals are POOLED buffers reused by the next drain —
169
+ // callers must consume (or copy) them before draining again. Both call
170
+ // sites (searchLayer/searchLayerQuery) box the entries immediately, so
171
+ // this removes two typed-array allocations per query / per insert-level.
168
172
  drainSorted() {
169
173
  const n = this.size;
170
- const resultKeys = new Uint32Array(n);
171
- const resultVals = new Uint32Array(n);
174
+ if (!this._drainKeys || this._drainKeys.length < n) {
175
+ const cap = Math.max(64, n);
176
+ this._drainKeys = new Uint32Array(cap);
177
+ this._drainVals = new Uint32Array(cap);
178
+ }
179
+ const resultKeys = this._drainKeys;
180
+ const resultVals = this._drainVals;
172
181
  // Extract max repeatedly → fills from end → ascending order
173
182
  for (let i = n - 1; i >= 0; i--) {
174
183
  resultKeys[i] = this.keys[0];