@seanhogg/builderforce-memory 2026.10.28 → 2026.10.29
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/dist/ingest/IngestionPipeline.d.ts +1 -8
- package/dist/ingest/IngestionPipeline.d.ts.map +1 -1
- package/dist/ingest/IngestionPipeline.js +3 -15
- package/dist/ingest/IngestionPipeline.js.map +1 -1
- package/dist/ingest/docsExtract.d.ts +30 -0
- package/dist/ingest/docsExtract.d.ts.map +1 -0
- package/dist/ingest/docsExtract.js +221 -0
- package/dist/ingest/docsExtract.js.map +1 -0
- package/dist/ingest/hash.d.ts +15 -0
- package/dist/ingest/hash.d.ts.map +1 -0
- package/dist/ingest/hash.js +22 -0
- package/dist/ingest/hash.js.map +1 -0
- package/dist/ingest/parsers.d.ts.map +1 -1
- package/dist/ingest/parsers.js +4 -0
- package/dist/ingest/parsers.js.map +1 -1
- package/dist/ingest/types.d.ts +7 -0
- package/dist/ingest/types.d.ts.map +1 -1
- package/dist/ingest/types.js.map +1 -1
- package/dist/knowledge/build.d.ts +24 -0
- package/dist/knowledge/build.d.ts.map +1 -0
- package/dist/knowledge/build.js +263 -0
- package/dist/knowledge/build.js.map +1 -0
- package/dist/knowledge/cards.d.ts +17 -0
- package/dist/knowledge/cards.d.ts.map +1 -0
- package/dist/knowledge/cards.js +118 -0
- package/dist/knowledge/cards.js.map +1 -0
- package/dist/knowledge/findings.d.ts +24 -0
- package/dist/knowledge/findings.d.ts.map +1 -0
- package/dist/knowledge/findings.js +152 -0
- package/dist/knowledge/findings.js.map +1 -0
- package/dist/knowledge/importance.d.ts +21 -0
- package/dist/knowledge/importance.d.ts.map +1 -0
- package/dist/knowledge/importance.js +70 -0
- package/dist/knowledge/importance.js.map +1 -0
- package/dist/knowledge/index.d.ts +31 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +26 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/knowledge/judge.d.ts +86 -0
- package/dist/knowledge/judge.d.ts.map +1 -0
- package/dist/knowledge/judge.js +213 -0
- package/dist/knowledge/judge.js.map +1 -0
- package/dist/knowledge/model.d.ts +333 -0
- package/dist/knowledge/model.d.ts.map +1 -0
- package/dist/knowledge/model.js +13 -0
- package/dist/knowledge/model.js.map +1 -0
- package/dist/knowledge/packs/docs.d.ts +17 -0
- package/dist/knowledge/packs/docs.d.ts.map +1 -0
- package/dist/knowledge/packs/docs.js +89 -0
- package/dist/knowledge/packs/docs.js.map +1 -0
- package/dist/knowledge/partitions.d.ts +31 -0
- package/dist/knowledge/partitions.d.ts.map +1 -0
- package/dist/knowledge/partitions.js +135 -0
- package/dist/knowledge/partitions.js.map +1 -0
- package/dist/knowledge/plan.d.ts +33 -0
- package/dist/knowledge/plan.d.ts.map +1 -0
- package/dist/knowledge/plan.js +89 -0
- package/dist/knowledge/plan.js.map +1 -0
- package/dist/knowledge/refs.d.ts +40 -0
- package/dist/knowledge/refs.d.ts.map +1 -0
- package/dist/knowledge/refs.js +102 -0
- package/dist/knowledge/refs.js.map +1 -0
- package/dist/knowledge/resolve.d.ts +35 -0
- package/dist/knowledge/resolve.d.ts.map +1 -0
- package/dist/knowledge/resolve.js +67 -0
- package/dist/knowledge/resolve.js.map +1 -0
- package/dist/knowledge/review.d.ts +23 -0
- package/dist/knowledge/review.d.ts.map +1 -0
- package/dist/knowledge/review.js +233 -0
- package/dist/knowledge/review.js.map +1 -0
- package/dist/knowledge/sources.d.ts +47 -0
- package/dist/knowledge/sources.d.ts.map +1 -0
- package/dist/knowledge/sources.js +136 -0
- package/dist/knowledge/sources.js.map +1 -0
- package/dist/knowledge/stores/localSnapshotStore.d.ts +50 -0
- package/dist/knowledge/stores/localSnapshotStore.d.ts.map +1 -0
- package/dist/knowledge/stores/localSnapshotStore.js +125 -0
- package/dist/knowledge/stores/localSnapshotStore.js.map +1 -0
- package/dist/knowledge/trace.d.ts +32 -0
- package/dist/knowledge/trace.d.ts.map +1 -0
- package/dist/knowledge/trace.js +102 -0
- package/dist/knowledge/trace.js.map +1 -0
- package/dist/knowledge/truncate.d.ts +24 -0
- package/dist/knowledge/truncate.d.ts.map +1 -0
- package/dist/knowledge/truncate.js +128 -0
- package/dist/knowledge/truncate.js.map +1 -0
- package/dist/memory/SemanticRecall.d.ts.map +1 -1
- package/dist/memory/SemanticRecall.js +3 -1
- package/dist/memory/SemanticRecall.js.map +1 -1
- package/dist/retrieval/HybridRetriever.d.ts.map +1 -1
- package/dist/retrieval/HybridRetriever.js +3 -1
- package/dist/retrieval/HybridRetriever.js.map +1 -1
- package/dist/retrieval/bm25.d.ts +3 -2
- package/dist/retrieval/bm25.d.ts.map +1 -1
- package/dist/retrieval/bm25.js +5 -3
- package/dist/retrieval/bm25.js.map +1 -1
- package/dist/retrieval/fusion.d.ts +2 -1
- package/dist/retrieval/fusion.d.ts.map +1 -1
- package/dist/retrieval/fusion.js +4 -2
- package/dist/retrieval/fusion.js.map +1 -1
- package/dist/retrieval/hnsw.d.ts +49 -11
- package/dist/retrieval/hnsw.d.ts.map +1 -1
- package/dist/retrieval/hnsw.js +57 -29
- package/dist/retrieval/hnsw.js.map +1 -1
- package/dist/retrieval/index.d.ts +7 -3
- package/dist/retrieval/index.d.ts.map +1 -1
- package/dist/retrieval/index.js +5 -2
- package/dist/retrieval/index.js.map +1 -1
- package/dist/retrieval/knowledge-index/bytes.d.ts +43 -0
- package/dist/retrieval/knowledge-index/bytes.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/bytes.js +151 -0
- package/dist/retrieval/knowledge-index/bytes.js.map +1 -0
- package/dist/retrieval/knowledge-index/cascade.d.ts +37 -0
- package/dist/retrieval/knowledge-index/cascade.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/cascade.js +111 -0
- package/dist/retrieval/knowledge-index/cascade.js.map +1 -0
- package/dist/retrieval/knowledge-index/compact.d.ts +18 -0
- package/dist/retrieval/knowledge-index/compact.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/compact.js +64 -0
- package/dist/retrieval/knowledge-index/compact.js.map +1 -0
- package/dist/retrieval/knowledge-index/csr.d.ts +41 -0
- package/dist/retrieval/knowledge-index/csr.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/csr.js +76 -0
- package/dist/retrieval/knowledge-index/csr.js.map +1 -0
- package/dist/retrieval/knowledge-index/digest.d.ts +21 -0
- package/dist/retrieval/knowledge-index/digest.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/digest.js +50 -0
- package/dist/retrieval/knowledge-index/digest.js.map +1 -0
- package/dist/retrieval/knowledge-index/embeddings.d.ts +32 -0
- package/dist/retrieval/knowledge-index/embeddings.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/embeddings.js +78 -0
- package/dist/retrieval/knowledge-index/embeddings.js.map +1 -0
- package/dist/retrieval/knowledge-index/expand.d.ts +19 -0
- package/dist/retrieval/knowledge-index/expand.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/expand.js +49 -0
- package/dist/retrieval/knowledge-index/expand.js.map +1 -0
- package/dist/retrieval/knowledge-index/index.d.ts +40 -0
- package/dist/retrieval/knowledge-index/index.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/index.js +134 -0
- package/dist/retrieval/knowledge-index/index.js.map +1 -0
- package/dist/retrieval/knowledge-index/lexical.d.ts +26 -0
- package/dist/retrieval/knowledge-index/lexical.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/lexical.js +76 -0
- package/dist/retrieval/knowledge-index/lexical.js.map +1 -0
- package/dist/retrieval/knowledge-index/pairs.d.ts +22 -0
- package/dist/retrieval/knowledge-index/pairs.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/pairs.js +51 -0
- package/dist/retrieval/knowledge-index/pairs.js.map +1 -0
- package/dist/retrieval/knowledge-index/prune.d.ts +21 -0
- package/dist/retrieval/knowledge-index/prune.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/prune.js +43 -0
- package/dist/retrieval/knowledge-index/prune.js.map +1 -0
- package/dist/retrieval/knowledge-index/quantize.d.ts +37 -0
- package/dist/retrieval/knowledge-index/quantize.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/quantize.js +77 -0
- package/dist/retrieval/knowledge-index/quantize.js.map +1 -0
- package/dist/retrieval/knowledge-index/serialize.d.ts +18 -0
- package/dist/retrieval/knowledge-index/serialize.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/serialize.js +123 -0
- package/dist/retrieval/knowledge-index/serialize.js.map +1 -0
- package/dist/retrieval/knowledge-index/sha256.d.ts +30 -0
- package/dist/retrieval/knowledge-index/sha256.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/sha256.js +130 -0
- package/dist/retrieval/knowledge-index/sha256.js.map +1 -0
- package/dist/retrieval/knowledge-index/state.d.ts +55 -0
- package/dist/retrieval/knowledge-index/state.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/state.js +67 -0
- package/dist/retrieval/knowledge-index/state.js.map +1 -0
- package/dist/retrieval/knowledge-index/tokenize.d.ts +20 -0
- package/dist/retrieval/knowledge-index/tokenize.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/tokenize.js +42 -0
- package/dist/retrieval/knowledge-index/tokenize.js.map +1 -0
- package/dist/retrieval/knowledge-index/traverse.d.ts +26 -0
- package/dist/retrieval/knowledge-index/traverse.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/traverse.js +90 -0
- package/dist/retrieval/knowledge-index/traverse.js.map +1 -0
- package/dist/retrieval/knowledge-index/types.d.ts +85 -0
- package/dist/retrieval/knowledge-index/types.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/types.js +9 -0
- package/dist/retrieval/knowledge-index/types.js.map +1 -0
- package/dist/retrieval/mmr.d.ts +11 -4
- package/dist/retrieval/mmr.d.ts.map +1 -1
- package/dist/retrieval/mmr.js +28 -8
- package/dist/retrieval/mmr.js.map +1 -1
- package/dist/retrieval/order.d.ts +41 -0
- package/dist/retrieval/order.d.ts.map +1 -0
- package/dist/retrieval/order.js +54 -0
- package/dist/retrieval/order.js.map +1 -0
- package/dist/retrieval/weightedFusion.d.ts +2 -1
- package/dist/retrieval/weightedFusion.d.ts.map +1 -1
- package/dist/retrieval/weightedFusion.js +7 -5
- package/dist/retrieval/weightedFusion.js.map +1 -1
- package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -1
- package/dist/vectorstore/MemoryVectorStore.js +2 -1
- package/dist/vectorstore/MemoryVectorStore.js.map +1 -1
- package/package.json +8 -3
- package/src/ingest/IngestionPipeline.ts +3 -15
- package/src/ingest/docsExtract.ts +219 -0
- package/src/ingest/hash.ts +22 -0
- package/src/ingest/parsers.ts +4 -0
- package/src/ingest/types.ts +7 -0
- package/src/knowledge/build.test.ts +150 -0
- package/src/knowledge/build.ts +266 -0
- package/src/knowledge/cards.ts +130 -0
- package/src/knowledge/docs.test.ts +118 -0
- package/src/knowledge/findings.ts +159 -0
- package/src/knowledge/importance.ts +72 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/judge.ts +231 -0
- package/src/knowledge/model.ts +312 -0
- package/src/knowledge/packs/docs.ts +91 -0
- package/src/knowledge/partitions.test.ts +98 -0
- package/src/knowledge/partitions.ts +119 -0
- package/src/knowledge/plan.ts +115 -0
- package/src/knowledge/refs.ts +108 -0
- package/src/knowledge/resolve.ts +78 -0
- package/src/knowledge/review.test.ts +180 -0
- package/src/knowledge/review.ts +235 -0
- package/src/knowledge/sources.ts +146 -0
- package/src/knowledge/stores/localSnapshotStore.test.ts +82 -0
- package/src/knowledge/stores/localSnapshotStore.ts +159 -0
- package/src/knowledge/testkit.test-util.ts +92 -0
- package/src/knowledge/trace.test.ts +91 -0
- package/src/knowledge/trace.ts +111 -0
- package/src/knowledge/truncate.ts +118 -0
- package/src/memory/SemanticRecall.ts +3 -1
- package/src/retrieval/HybridRetriever.ts +3 -1
- package/src/retrieval/bm25.ts +5 -3
- package/src/retrieval/fusion.ts +5 -2
- package/src/retrieval/hnsw.ts +82 -35
- package/src/retrieval/index.ts +17 -3
- package/src/retrieval/knowledge-index/bytes.ts +166 -0
- package/src/retrieval/knowledge-index/cascade.ts +125 -0
- package/src/retrieval/knowledge-index/compact.ts +68 -0
- package/src/retrieval/knowledge-index/csr.ts +102 -0
- package/src/retrieval/knowledge-index/digest.ts +54 -0
- package/src/retrieval/knowledge-index/embeddings.ts +85 -0
- package/src/retrieval/knowledge-index/expand.ts +55 -0
- package/src/retrieval/knowledge-index/index.ts +162 -0
- package/src/retrieval/knowledge-index/lexical.ts +78 -0
- package/src/retrieval/knowledge-index/pairs.ts +57 -0
- package/src/retrieval/knowledge-index/prune.ts +48 -0
- package/src/retrieval/knowledge-index/quantize.ts +78 -0
- package/src/retrieval/knowledge-index/serialize.ts +124 -0
- package/src/retrieval/knowledge-index/sha256.ts +122 -0
- package/src/retrieval/knowledge-index/state.ts +108 -0
- package/src/retrieval/knowledge-index/tokenize.ts +41 -0
- package/src/retrieval/knowledge-index/traverse.ts +95 -0
- package/src/retrieval/knowledge-index/types.ts +78 -0
- package/src/retrieval/mmr.ts +35 -10
- package/src/retrieval/order.ts +68 -0
- package/src/retrieval/weightedFusion.ts +8 -5
- package/src/vectorstore/MemoryVectorStore.ts +2 -1
package/src/retrieval/hnsw.ts
CHANGED
|
@@ -10,11 +10,19 @@
|
|
|
10
10
|
* above a threshold.
|
|
11
11
|
*
|
|
12
12
|
* Distance is cosine similarity (higher = closer); the graph stores `1 - cos`
|
|
13
|
-
* as the metric so "nearer" is "smaller".
|
|
14
|
-
*
|
|
13
|
+
* as the metric so "nearer" is "smaller".
|
|
14
|
+
*
|
|
15
|
+
* Determinism: a node's level is {@link levelOf} its id — a geometric draw from
|
|
16
|
+
* the id's sha-256, never a sequential RNG — and every ordering (candidates,
|
|
17
|
+
* pruning, results) is {@link compareScoreThenId}: score, then id. So the graph
|
|
18
|
+
* depends only on the vectors and the insertion order, and
|
|
19
|
+
* {@link HnswIndex.canonicalBuild} (id-sorted inserts) removes the latter: the
|
|
20
|
+
* same set of (id, vector) gives the same graph whatever order it arrived in.
|
|
15
21
|
*/
|
|
16
22
|
|
|
17
23
|
import { cosineSimilarity } from '../similarity/index.js';
|
|
24
|
+
import { compareScoreThenId, compareDenseHits, compareIds, DENSE_SCORE_QUANTUM } from './order.js';
|
|
25
|
+
import { sha256 } from './knowledge-index/sha256.js';
|
|
18
26
|
|
|
19
27
|
export interface HnswOptions {
|
|
20
28
|
/** Max neighbours per node on layers > 0. Default 16. */
|
|
@@ -23,7 +31,11 @@ export interface HnswOptions {
|
|
|
23
31
|
efConstruction?: number;
|
|
24
32
|
/** Default candidate-list size during search. Default 50. */
|
|
25
33
|
efSearch?: number;
|
|
26
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Retained for API compatibility. It no longer affects anything: levels come
|
|
36
|
+
* from each id's hash ({@link levelOf}), so a build does not depend on a seed
|
|
37
|
+
* or on how many inserts preceded a node.
|
|
38
|
+
*/
|
|
27
39
|
seed?: number;
|
|
28
40
|
}
|
|
29
41
|
|
|
@@ -33,20 +45,38 @@ export interface SearchHit {
|
|
|
33
45
|
score: number;
|
|
34
46
|
}
|
|
35
47
|
|
|
48
|
+
/** Any numeric vector: a float embedding, or int8 codes (cosine over codes is exact). */
|
|
49
|
+
export type HnswVector = ArrayLike<number>;
|
|
50
|
+
|
|
36
51
|
interface Node {
|
|
37
52
|
id: string;
|
|
38
|
-
vector:
|
|
53
|
+
vector: HnswVector;
|
|
39
54
|
/** neighbours[layer] = list of node indices. */
|
|
40
55
|
neighbours: number[][];
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
/** A
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
/** A read-only view of the built graph, for a consumer that stores it elsewhere (CSR). */
|
|
59
|
+
export interface HnswGraphView {
|
|
60
|
+
/** Node ids, by node index (insertion order). */
|
|
61
|
+
ids: readonly string[];
|
|
62
|
+
/** neighbours[node][layer] = node indices. */
|
|
63
|
+
neighbours: ReadonlyArray<ReadonlyArray<readonly number[]>>;
|
|
64
|
+
entry: number;
|
|
65
|
+
maxLayer: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The HNSW level of `id`: a geometric draw with P(level ≥ L) = M^-L, taken from the
|
|
70
|
+
* eight 32-bit words of sha-256(id) — each word below 2^32 / M is one more level.
|
|
71
|
+
* Integer-only, so it is the same on every platform and independent of insertion.
|
|
72
|
+
*/
|
|
73
|
+
export function levelOf(id: string, M: number): number {
|
|
74
|
+
const h = sha256(id);
|
|
75
|
+
const view = new DataView(h.buffer, h.byteOffset, h.byteLength);
|
|
76
|
+
const threshold = 0x1_0000_0000 / M;
|
|
77
|
+
let level = 0;
|
|
78
|
+
while (level < 8 && view.getUint32(level * 4) < threshold) level++;
|
|
79
|
+
return level;
|
|
50
80
|
}
|
|
51
81
|
|
|
52
82
|
export class HnswIndex {
|
|
@@ -54,8 +84,6 @@ export class HnswIndex {
|
|
|
54
84
|
private readonly _Mmax0: number;
|
|
55
85
|
private readonly _efC: number;
|
|
56
86
|
private readonly _efS: number;
|
|
57
|
-
private readonly _mL: number;
|
|
58
|
-
private readonly _rng: () => number;
|
|
59
87
|
|
|
60
88
|
private readonly _nodes: Node[] = [];
|
|
61
89
|
private readonly _idToIdx = new Map<string, number>();
|
|
@@ -67,8 +95,19 @@ export class HnswIndex {
|
|
|
67
95
|
this._Mmax0 = this._M * 2;
|
|
68
96
|
this._efC = Math.max(this._M, opts.efConstruction ?? 200);
|
|
69
97
|
this._efS = Math.max(1, opts.efSearch ?? 50);
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Builds an index by inserting `items` in id order — the canonical build. The
|
|
102
|
+
* same (id, vector) set yields the same graph regardless of the input order.
|
|
103
|
+
* Ids are unique; for a duplicated id the first in input order wins.
|
|
104
|
+
*/
|
|
105
|
+
static canonicalBuild(items: ReadonlyArray<{ id: string; vector: HnswVector }>, opts?: HnswOptions): HnswIndex {
|
|
106
|
+
const index = new HnswIndex(opts);
|
|
107
|
+
const sorted = items.map((it, i) => ({ it, i }))
|
|
108
|
+
.sort((a, b) => compareIds(a.it.id, b.it.id) || a.i - b.i);
|
|
109
|
+
for (const { it } of sorted) index.add(it.id, it.vector);
|
|
110
|
+
return index;
|
|
72
111
|
}
|
|
73
112
|
|
|
74
113
|
get size(): number {
|
|
@@ -79,18 +118,28 @@ export class HnswIndex {
|
|
|
79
118
|
return this._idToIdx.has(id);
|
|
80
119
|
}
|
|
81
120
|
|
|
82
|
-
|
|
83
|
-
|
|
121
|
+
/** The built graph, by node index — what a CSR store and pruning read. */
|
|
122
|
+
graph(): HnswGraphView {
|
|
123
|
+
return {
|
|
124
|
+
ids: this._nodes.map((n) => n.id),
|
|
125
|
+
neighbours: this._nodes.map((n) => n.neighbours),
|
|
126
|
+
entry: this._entry,
|
|
127
|
+
maxLayer: this._maxLayer,
|
|
128
|
+
};
|
|
84
129
|
}
|
|
85
130
|
|
|
86
|
-
private
|
|
87
|
-
return
|
|
131
|
+
private _dist(a: HnswVector, b: HnswVector): number {
|
|
132
|
+
return 1 - cosineSimilarity(a, b);
|
|
88
133
|
}
|
|
89
134
|
|
|
135
|
+
/** Nearest first; equal (quantized) distances by id ascending. */
|
|
136
|
+
private readonly _byDist = (a: { idx: number; dist: number }, b: { idx: number; dist: number }): number =>
|
|
137
|
+
compareScoreThenId(1 - a.dist, this._nodes[a.idx]!.id, 1 - b.dist, this._nodes[b.idx]!.id, DENSE_SCORE_QUANTUM);
|
|
138
|
+
|
|
90
139
|
/** Insert a vector. Ids are unique — re-adding a known id is ignored. */
|
|
91
|
-
add(id: string, vector:
|
|
140
|
+
add(id: string, vector: HnswVector): void {
|
|
92
141
|
if (this._idToIdx.has(id)) return;
|
|
93
|
-
const layer = this.
|
|
142
|
+
const layer = levelOf(id, this._M);
|
|
94
143
|
const node: Node = { id, vector, neighbours: Array.from({ length: layer + 1 }, () => []) };
|
|
95
144
|
const idx = this._nodes.length;
|
|
96
145
|
this._nodes.push(node);
|
|
@@ -130,8 +179,8 @@ export class HnswIndex {
|
|
|
130
179
|
}
|
|
131
180
|
}
|
|
132
181
|
|
|
133
|
-
/** Top-k nearest by cosine similarity (descending). */
|
|
134
|
-
search(query:
|
|
182
|
+
/** Top-k nearest by cosine similarity (descending; equal scores by id). */
|
|
183
|
+
search(query: HnswVector, k: number, ef = this._efS): SearchHit[] {
|
|
135
184
|
if (this._entry === -1 || k <= 0) return [];
|
|
136
185
|
let ep = this._entry;
|
|
137
186
|
for (let lc = this._maxLayer; lc > 0; lc--) {
|
|
@@ -151,7 +200,7 @@ export class HnswIndex {
|
|
|
151
200
|
// ── internals ──────────────────────────────────────────────────────────────
|
|
152
201
|
|
|
153
202
|
/** Greedy hill-climb to the single closest node to `q` on layer `lc`. */
|
|
154
|
-
private _greedyClosest(q:
|
|
203
|
+
private _greedyClosest(q: HnswVector, start: number, lc: number): number {
|
|
155
204
|
let best = start;
|
|
156
205
|
let bestDist = this._dist(q, this._nodes[best]!.vector);
|
|
157
206
|
let improved = true;
|
|
@@ -173,7 +222,7 @@ export class HnswIndex {
|
|
|
173
222
|
* ef-search on a single layer: returns candidates sorted nearest-first.
|
|
174
223
|
* A bounded best-first expansion (the standard HNSW layer search).
|
|
175
224
|
*/
|
|
176
|
-
private _searchLayer(q:
|
|
225
|
+
private _searchLayer(q: HnswVector, entry: number, ef: number, lc: number): Array<{ idx: number; dist: number }> {
|
|
177
226
|
const visited = new Set<number>([entry]);
|
|
178
227
|
const entryDist = this._dist(q, this._nodes[entry]!.vector);
|
|
179
228
|
// `candidates` is a min-heap-ish frontier; `results` keeps the best ef.
|
|
@@ -206,21 +255,21 @@ export class HnswIndex {
|
|
|
206
255
|
}
|
|
207
256
|
}
|
|
208
257
|
}
|
|
209
|
-
return results.sort(
|
|
258
|
+
return results.sort(this._byDist);
|
|
210
259
|
}
|
|
211
260
|
|
|
212
261
|
/** Keep only the `Mmax` closest neighbours of `node` on layer `lc`. */
|
|
213
262
|
private _prune(node: Node, lc: number, Mmax: number): void {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
node.neighbours[lc] = list.slice(0, Mmax);
|
|
263
|
+
const ranked = node.neighbours[lc]!.map((idx) => ({ idx, dist: this._dist(node.vector, this._nodes[idx]!.vector) }));
|
|
264
|
+
node.neighbours[lc] = ranked.sort(this._byDist).slice(0, Mmax).map((r) => r.idx);
|
|
217
265
|
}
|
|
218
266
|
}
|
|
219
267
|
|
|
220
268
|
/**
|
|
221
269
|
* Dense KNN with an exact-scan fast path: below `threshold` candidates a linear
|
|
222
|
-
* cosine scan is simpler and faster; at/above it, build an HNSW index
|
|
223
|
-
*
|
|
270
|
+
* cosine scan is simpler and faster; at/above it, build an HNSW index (the
|
|
271
|
+
* canonical, id-ordered build) and query it. Returned hits are in
|
|
272
|
+
* {@link compareDenseHits} order. Pure given its inputs, and independent of their order.
|
|
224
273
|
*/
|
|
225
274
|
export function denseSearch(
|
|
226
275
|
query: Float32Array,
|
|
@@ -233,10 +282,8 @@ export function denseSearch(
|
|
|
233
282
|
if (items.length < threshold) {
|
|
234
283
|
return items
|
|
235
284
|
.map((it) => ({ id: it.id, score: cosineSimilarity(query, it.vector) }))
|
|
236
|
-
.sort(
|
|
285
|
+
.sort(compareDenseHits)
|
|
237
286
|
.slice(0, k);
|
|
238
287
|
}
|
|
239
|
-
|
|
240
|
-
for (const it of items) index.add(it.id, it.vector);
|
|
241
|
-
return index.search(query, k);
|
|
288
|
+
return HnswIndex.canonicalBuild(items, hnsw).search(query, k);
|
|
242
289
|
}
|
package/src/retrieval/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Retrieval layer — chunking, BM25, rank and score fusion, MMR, recency decay, the
|
|
3
|
-
* similarity primitives they rest on,
|
|
3
|
+
* similarity primitives they rest on, the HybridRetriever, the ONE total order every
|
|
4
|
+
* ranker returns (`compareHits`), and the deterministic knowledge index.
|
|
4
5
|
*
|
|
5
6
|
* Zero-dependency and engine-free: this is the `@seanhogg/builderforce-memory/retrieval`
|
|
6
7
|
* subpath, which a Cloudflare Worker, Node and the browser can all import without
|
|
@@ -41,5 +42,18 @@ export type {
|
|
|
41
42
|
HybridHit,
|
|
42
43
|
} from './HybridRetriever.js';
|
|
43
44
|
|
|
44
|
-
export { HnswIndex, denseSearch } from './hnsw.js';
|
|
45
|
-
export type { HnswOptions, SearchHit } from './hnsw.js';
|
|
45
|
+
export { HnswIndex, denseSearch, levelOf } from './hnsw.js';
|
|
46
|
+
export type { HnswOptions, SearchHit, HnswVector, HnswGraphView } from './hnsw.js';
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
compareHits,
|
|
50
|
+
compareDenseHits,
|
|
51
|
+
compareScoreThenId,
|
|
52
|
+
compareIds,
|
|
53
|
+
quantizeScore,
|
|
54
|
+
DENSE_SCORE_QUANTUM,
|
|
55
|
+
LEXICAL_SCORE_QUANTUM,
|
|
56
|
+
} from './order.js';
|
|
57
|
+
export type { ScoredId } from './order.js';
|
|
58
|
+
|
|
59
|
+
export * from './knowledge-index/index.js';
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bytes.ts — the little-endian byte writer/reader that `serialize` and `digest`
|
|
3
|
+
* share, so the persisted format and the hashed canonical state encode every
|
|
4
|
+
* field the same way. DataView only: no Node `Buffer`, no host endianness.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const decoder = new TextDecoder();
|
|
9
|
+
|
|
10
|
+
export class ByteWriter {
|
|
11
|
+
private _buf = new Uint8Array(1024);
|
|
12
|
+
private _view = new DataView(this._buf.buffer);
|
|
13
|
+
private _len = 0;
|
|
14
|
+
|
|
15
|
+
private _reserve(n: number): void {
|
|
16
|
+
if (this._len + n <= this._buf.length) return;
|
|
17
|
+
let size = this._buf.length * 2;
|
|
18
|
+
while (size < this._len + n) size *= 2;
|
|
19
|
+
const next = new Uint8Array(size);
|
|
20
|
+
next.set(this._buf.subarray(0, this._len));
|
|
21
|
+
this._buf = next;
|
|
22
|
+
this._view = new DataView(next.buffer);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
u8(v: number): this {
|
|
26
|
+
this._reserve(1);
|
|
27
|
+
this._view.setUint8(this._len, v);
|
|
28
|
+
this._len += 1;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
u32(v: number): this {
|
|
33
|
+
this._reserve(4);
|
|
34
|
+
this._view.setUint32(this._len, v >>> 0, true);
|
|
35
|
+
this._len += 4;
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
i32(v: number): this {
|
|
40
|
+
this._reserve(4);
|
|
41
|
+
this._view.setInt32(this._len, v, true);
|
|
42
|
+
this._len += 4;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
f64(v: number): this {
|
|
47
|
+
this._reserve(8);
|
|
48
|
+
this._view.setFloat64(this._len, v, true);
|
|
49
|
+
this._len += 8;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Raw bytes, no length prefix. */
|
|
54
|
+
raw(bytes: Uint8Array): this {
|
|
55
|
+
this._reserve(bytes.length);
|
|
56
|
+
this._buf.set(bytes, this._len);
|
|
57
|
+
this._len += bytes.length;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Length-prefixed UTF-8. */
|
|
62
|
+
str(s: string): this {
|
|
63
|
+
const b = encoder.encode(s);
|
|
64
|
+
return this.u32(b.length).raw(b);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Count-prefixed u32 values. */
|
|
68
|
+
u32s(arr: ArrayLike<number>): this {
|
|
69
|
+
this.u32(arr.length);
|
|
70
|
+
this._reserve(arr.length * 4);
|
|
71
|
+
for (let i = 0; i < arr.length; i++) this._view.setUint32(this._len + i * 4, arr[i]! >>> 0, true);
|
|
72
|
+
this._len += arr.length * 4;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Count-prefixed f32 values. */
|
|
77
|
+
f32s(arr: ArrayLike<number>): this {
|
|
78
|
+
this.u32(arr.length);
|
|
79
|
+
this._reserve(arr.length * 4);
|
|
80
|
+
for (let i = 0; i < arr.length; i++) this._view.setFloat32(this._len + i * 4, arr[i]!, true);
|
|
81
|
+
this._len += arr.length * 4;
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Count-prefixed bytes (an Int8Array's bytes are written as-is). */
|
|
86
|
+
bytes(arr: Uint8Array | Int8Array): this {
|
|
87
|
+
this.u32(arr.length);
|
|
88
|
+
return this.raw(new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
finish(): Uint8Array {
|
|
92
|
+
return this._buf.slice(0, this._len);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class ByteReader {
|
|
97
|
+
private readonly _view: DataView;
|
|
98
|
+
private _pos = 0;
|
|
99
|
+
|
|
100
|
+
constructor(private readonly _buf: Uint8Array) {
|
|
101
|
+
this._view = new DataView(_buf.buffer, _buf.byteOffset, _buf.byteLength);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private _need(n: number): void {
|
|
105
|
+
if (this._pos + n > this._buf.length) throw new Error('knowledge-index: truncated bytes');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
u8(): number {
|
|
109
|
+
this._need(1);
|
|
110
|
+
return this._view.getUint8(this._pos++);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
u32(): number {
|
|
114
|
+
this._need(4);
|
|
115
|
+
const v = this._view.getUint32(this._pos, true);
|
|
116
|
+
this._pos += 4;
|
|
117
|
+
return v;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
i32(): number {
|
|
121
|
+
this._need(4);
|
|
122
|
+
const v = this._view.getInt32(this._pos, true);
|
|
123
|
+
this._pos += 4;
|
|
124
|
+
return v;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
f64(): number {
|
|
128
|
+
this._need(8);
|
|
129
|
+
const v = this._view.getFloat64(this._pos, true);
|
|
130
|
+
this._pos += 8;
|
|
131
|
+
return v;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
raw(n: number): Uint8Array {
|
|
135
|
+
this._need(n);
|
|
136
|
+
const out = this._buf.slice(this._pos, this._pos + n);
|
|
137
|
+
this._pos += n;
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
str(): string {
|
|
142
|
+
return decoder.decode(this.raw(this.u32()));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
u32s(): Uint32Array {
|
|
146
|
+
const n = this.u32();
|
|
147
|
+
this._need(n * 4);
|
|
148
|
+
const out = new Uint32Array(n);
|
|
149
|
+
for (let i = 0; i < n; i++) out[i] = this._view.getUint32(this._pos + i * 4, true);
|
|
150
|
+
this._pos += n * 4;
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
f32s(): Float32Array {
|
|
155
|
+
const n = this.u32();
|
|
156
|
+
this._need(n * 4);
|
|
157
|
+
const out = new Float32Array(n);
|
|
158
|
+
for (let i = 0; i < n; i++) out[i] = this._view.getFloat32(this._pos + i * 4, true);
|
|
159
|
+
this._pos += n * 4;
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
bytes(): Uint8Array {
|
|
164
|
+
return this.raw(this.u32());
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cascade.ts — the knowledge index's search: cheapest tier first.
|
|
3
|
+
*
|
|
4
|
+
* 1. exact — the folded `keys` map; a hit scores 1.0, and when the exact hits
|
|
5
|
+
* fill `k` nothing else runs;
|
|
6
|
+
* 2. lexical — BM25 over card text with the code-aware tokenizer;
|
|
7
|
+
* 3. dense — int8 graph search for `k × rerankFactor` candidates (plus the
|
|
8
|
+
* cards not yet compacted), then a float rerank over vectors
|
|
9
|
+
* recomputed through the hash-keyed LRU;
|
|
10
|
+
* 4. fuse — RRF over the lexical and dense lists; exact hits stay on top at
|
|
11
|
+
* 1.0; everything in {@link compareHits} order.
|
|
12
|
+
*
|
|
13
|
+
* Dense runs only with an embedder of the stored fingerprint — otherwise the tier
|
|
14
|
+
* is absent, never silently replaced.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { reciprocalRankFusion } from '../fusion.js';
|
|
18
|
+
import { compareDenseHits, compareHits } from '../order.js';
|
|
19
|
+
import { cosineSimilarity } from '../../similarity/index.js';
|
|
20
|
+
import type { EmbedderPort, IndexHit, IndexTier } from './types.js';
|
|
21
|
+
import { availableTiers, graphNode, type DocEntry, type IndexState } from './state.js';
|
|
22
|
+
import type { EmbeddingCache } from './embeddings.js';
|
|
23
|
+
import { dotInt8, quantize } from './quantize.js';
|
|
24
|
+
import { searchGraph } from './traverse.js';
|
|
25
|
+
import { foldKey } from './tokenize.js';
|
|
26
|
+
|
|
27
|
+
export interface SearchContext {
|
|
28
|
+
state: IndexState;
|
|
29
|
+
embedder?: EmbedderPort;
|
|
30
|
+
/** The embedder's recompute cache; present whenever `embedder` is. */
|
|
31
|
+
cache?: EmbeddingCache;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SearchOptions {
|
|
35
|
+
k?: number;
|
|
36
|
+
tiers?: IndexTier[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Exact-tier ids for `q`, ascending. */
|
|
40
|
+
function exactIds(state: IndexState, q: string): string[] {
|
|
41
|
+
const ids = state.keys.get(foldKey(q));
|
|
42
|
+
return ids ? [...ids].sort() : [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Dense candidates before the float rerank: the best `n` live graph nodes by
|
|
47
|
+
* integer dot product, plus the best `n` cards not yet in the graph. Ids, unordered
|
|
48
|
+
* between the two sources (the rerank orders them).
|
|
49
|
+
*/
|
|
50
|
+
export async function denseCandidates(ctx: SearchContext, qv: Float32Array, n: number): Promise<string[]> {
|
|
51
|
+
const { state, cache } = ctx;
|
|
52
|
+
const out = new Set<string>();
|
|
53
|
+
const c = state.compacted;
|
|
54
|
+
if (c && state.scale > 0) {
|
|
55
|
+
const qc = quantize(qv, state.scale);
|
|
56
|
+
const recomputed = new Map<number, Int8Array>();
|
|
57
|
+
const codeOf = (node: number) => c.codes[node] ?? recomputed.get(node);
|
|
58
|
+
const found = await searchGraph(c.graph, {
|
|
59
|
+
score: (node) => {
|
|
60
|
+
const code = codeOf(node);
|
|
61
|
+
return code ? dotInt8(qc, code) : -Infinity;
|
|
62
|
+
},
|
|
63
|
+
ensure: cache && state.params.lean ? (nodes) => {
|
|
64
|
+
const live = nodes.filter((node) => !codeOf(node) && graphNode(state, c.ids[node]!) === node);
|
|
65
|
+
if (live.length === 0) return undefined;
|
|
66
|
+
return cache.vectors(live.map((node) => state.docs.get(c.ids[node]!)!)).then((vecs) => {
|
|
67
|
+
live.forEach((node, i) => recomputed.set(node, quantize(vecs[i]!, state.scale)));
|
|
68
|
+
});
|
|
69
|
+
} : undefined,
|
|
70
|
+
}, Math.max(state.params.efSearch, n));
|
|
71
|
+
for (const hit of found) {
|
|
72
|
+
if (out.size >= n) break;
|
|
73
|
+
if (hit.score !== -Infinity && graphNode(state, c.ids[hit.node]!) === hit.node) out.add(c.ids[hit.node]!);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const pending: Array<{ id: string; score: number }> = [];
|
|
77
|
+
for (const doc of state.docs.values()) {
|
|
78
|
+
if (doc.vec && graphNode(state, doc.id) < 0) pending.push({ id: doc.id, score: cosineSimilarity(qv, doc.vec) });
|
|
79
|
+
}
|
|
80
|
+
for (const hit of pending.sort(compareDenseHits).slice(0, n)) out.add(hit.id);
|
|
81
|
+
return [...out];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The dense tier: candidates, then float recompute + rerank, best first. */
|
|
85
|
+
async function denseRank(ctx: SearchContext, q: string, n: number): Promise<Array<{ id: string; score: number }>> {
|
|
86
|
+
const cache = ctx.cache!;
|
|
87
|
+
const qv = await cache.query(q);
|
|
88
|
+
const ids = await denseCandidates(ctx, qv, n);
|
|
89
|
+
const docs = ids.map((id) => ctx.state.docs.get(id)!);
|
|
90
|
+
const needVec = docs.filter((d) => !d.vec);
|
|
91
|
+
const fresh = await cache.vectors(needVec);
|
|
92
|
+
const vecOf = new Map<DocEntry, Float32Array>(needVec.map((d, i) => [d, fresh[i]!]));
|
|
93
|
+
return docs
|
|
94
|
+
.map((d) => ({ id: d.id, score: cosineSimilarity(qv, d.vec ?? vecOf.get(d)!) }))
|
|
95
|
+
.sort(compareDenseHits)
|
|
96
|
+
.slice(0, n);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Runs the cascade. A pure function of the state (its digest), `q` and `opts`. */
|
|
100
|
+
export async function cascadeSearch(ctx: SearchContext, q: string, opts: SearchOptions = {}): Promise<IndexHit[]> {
|
|
101
|
+
const { state } = ctx;
|
|
102
|
+
const k = Math.max(0, opts.k ?? 10);
|
|
103
|
+
if (k === 0 || state.docs.size === 0) return [];
|
|
104
|
+
const available = availableTiers(state, ctx.embedder);
|
|
105
|
+
const run = new Set((opts.tiers ?? available).filter((t) => available.includes(t)));
|
|
106
|
+
|
|
107
|
+
const exact: IndexHit[] = run.has('exact')
|
|
108
|
+
? exactIds(state, q).map((id) => ({ id, score: 1, tier: 'exact' as const }))
|
|
109
|
+
: [];
|
|
110
|
+
if (exact.length >= k) return exact.slice(0, k);
|
|
111
|
+
const exactSet = new Set(exact.map((h) => h.id));
|
|
112
|
+
|
|
113
|
+
const n = Math.max(k, k * state.params.rerankFactor);
|
|
114
|
+
const lexical = run.has('lexical')
|
|
115
|
+
? state.lexical.search(q, n + exact.length).filter((h) => !exactSet.has(h.id)).map((h) => h.id)
|
|
116
|
+
: [];
|
|
117
|
+
const dense = run.has('dense')
|
|
118
|
+
? (await denseRank(ctx, q, n + exact.length)).filter((h) => !exactSet.has(h.id)).map((h) => h.id)
|
|
119
|
+
: [];
|
|
120
|
+
|
|
121
|
+
const lexicalSet = new Set(lexical);
|
|
122
|
+
const fused = reciprocalRankFusion([{ ids: lexical }, { ids: dense }].filter((l) => l.ids.length > 0), state.params.rrfK)
|
|
123
|
+
.map((h): IndexHit => ({ id: h.id, score: h.score, tier: lexicalSet.has(h.id) ? 'lexical' : 'dense' }));
|
|
124
|
+
return [...exact, ...fused].sort(compareHits).slice(0, k);
|
|
125
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compact.ts — the canonical rebuild.
|
|
3
|
+
*
|
|
4
|
+
* Every live card, in id order, is coded (an unchanged node keeps its code; a new
|
|
5
|
+
* or changed card is quantized from its float vector) and the graph is rebuilt
|
|
6
|
+
* from those codes. Since levels come from id hashes, inserts are id-ordered and
|
|
7
|
+
* every tie breaks on id, the same card set and fingerprint give the same bytes
|
|
8
|
+
* whatever order the upserts arrived in.
|
|
9
|
+
*
|
|
10
|
+
* The int8 scale is fitted at the first compaction and fixed after it. An embedder
|
|
11
|
+
* whose fingerprint differs from the stored one is never mixed in: compaction
|
|
12
|
+
* drops every stored vector and code and re-embeds all cards under it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { compareIds } from '../order.js';
|
|
16
|
+
import type { EmbedderPort } from './types.js';
|
|
17
|
+
import { graphNode, type DocEntry, type IndexState } from './state.js';
|
|
18
|
+
import type { EmbeddingCache } from './embeddings.js';
|
|
19
|
+
import { fitScale, quantize } from './quantize.js';
|
|
20
|
+
import { buildGraph } from './csr.js';
|
|
21
|
+
|
|
22
|
+
/** Float vectors for the docs that have neither a reusable code nor a pending vector. */
|
|
23
|
+
async function vectorsFor(cache: EmbeddingCache, docs: readonly DocEntry[]): Promise<Map<string, Float32Array>> {
|
|
24
|
+
const vecs = await cache.vectors(docs);
|
|
25
|
+
return new Map(docs.map((d, i) => [d.id, vecs[i]!]));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function compactState(state: IndexState, embedder: EmbedderPort | undefined, cache: EmbeddingCache | undefined): Promise<void> {
|
|
29
|
+
// Nothing to code with: exact and lexical need no compaction, and a stored
|
|
30
|
+
// graph of another fingerprint is kept for an embedder that matches it.
|
|
31
|
+
if (!embedder || !cache) return;
|
|
32
|
+
if (embedder.fingerprint !== state.fingerprint || embedder.dims !== state.dims) {
|
|
33
|
+
state.fingerprint = embedder.fingerprint;
|
|
34
|
+
state.dims = embedder.dims;
|
|
35
|
+
state.scale = 0;
|
|
36
|
+
state.compacted = null;
|
|
37
|
+
for (const doc of state.docs.values()) doc.vec = undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ids = [...state.docs.keys()].sort(compareIds);
|
|
41
|
+
const docs = ids.map((id) => state.docs.get(id)!);
|
|
42
|
+
if (docs.length === 0) {
|
|
43
|
+
state.compacted = null;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const old = state.compacted;
|
|
48
|
+
const reused = docs.map((d) => {
|
|
49
|
+
const node = graphNode(state, d.id);
|
|
50
|
+
return node >= 0 && state.scale > 0 ? old!.codes[node] : undefined;
|
|
51
|
+
});
|
|
52
|
+
const missing = await vectorsFor(cache, docs.filter((d, i) => !reused[i] && !d.vec));
|
|
53
|
+
const floatOf = (d: DocEntry) => d.vec ?? missing.get(d.id)!;
|
|
54
|
+
|
|
55
|
+
if (state.scale === 0) state.scale = fitScale(docs.map(floatOf));
|
|
56
|
+
const codes = docs.map((d, i) => reused[i] ?? quantize(floatOf(d), state.scale));
|
|
57
|
+
|
|
58
|
+
const { graph, hubs } = buildGraph(ids, codes, state.params);
|
|
59
|
+
state.compacted = {
|
|
60
|
+
ids,
|
|
61
|
+
hashes: docs.map((d) => d.hash),
|
|
62
|
+
codes: state.params.lean ? codes.map((code, i) => (hubs[i] ? code : undefined)) : codes,
|
|
63
|
+
hubs,
|
|
64
|
+
graph,
|
|
65
|
+
nodeOf: new Map(ids.map((id, i) => [id, i])),
|
|
66
|
+
};
|
|
67
|
+
for (const doc of docs) doc.vec = undefined;
|
|
68
|
+
}
|