hrr-memory 0.3.2 → 0.5.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/README.md +179 -179
- package/package.json +56 -56
- package/src/bucket.js +55 -55
- package/src/index.js +26 -25
- package/src/memory.js +89 -2
- package/src/observation.js +2 -0
- package/src/ops.js +107 -107
- package/src/symbols.js +49 -49
- package/src/traversal.js +151 -0
- package/types/index.d.ts +257 -257
package/src/memory.js
CHANGED
|
@@ -24,6 +24,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
|
24
24
|
import { bind, unbind, similarity } from './ops.js';
|
|
25
25
|
import { SymbolTable } from './symbols.js';
|
|
26
26
|
import { Bucket, MAX_BUCKET_SIZE } from './bucket.js';
|
|
27
|
+
import { buildAdjacency, dijkstra, neighbors as getNeighbors } from './traversal.js';
|
|
27
28
|
|
|
28
29
|
const STOP_WORDS = new Set([
|
|
29
30
|
'what', 'is', 'the', 'a', 'an', 'does', 'do', 'where', 'who', 'how',
|
|
@@ -37,6 +38,9 @@ export class HRRMemory {
|
|
|
37
38
|
this.symbols = new SymbolTable(d);
|
|
38
39
|
this.buckets = new Map();
|
|
39
40
|
this.routing = new Map();
|
|
41
|
+
this._relIndex = new Map(); // "subject\0relation" → Set<bucket_id>
|
|
42
|
+
this._adjDirty = true;
|
|
43
|
+
this._adjacency = null;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
_activeBucket(subject) {
|
|
@@ -80,7 +84,18 @@ export class HRRMemory {
|
|
|
80
84
|
const r = this.symbols.get(relation);
|
|
81
85
|
const o = this.symbols.get(object);
|
|
82
86
|
const association = bind(bind(s, r), o);
|
|
83
|
-
this._activeBucket(subject)
|
|
87
|
+
const bucket = this._activeBucket(subject);
|
|
88
|
+
bucket.storeVector(association, triple);
|
|
89
|
+
|
|
90
|
+
// Invalidate adjacency cache
|
|
91
|
+
this._adjDirty = true;
|
|
92
|
+
|
|
93
|
+
// Update relation → shard index
|
|
94
|
+
const indexKey = triple.subject + '\0' + triple.relation;
|
|
95
|
+
let set = this._relIndex.get(indexKey);
|
|
96
|
+
if (!set) { set = new Set(); this._relIndex.set(indexKey, set); }
|
|
97
|
+
set.add(bucket.name);
|
|
98
|
+
|
|
84
99
|
return true;
|
|
85
100
|
}
|
|
86
101
|
|
|
@@ -98,17 +113,42 @@ export class HRRMemory {
|
|
|
98
113
|
if (idx === -1) continue;
|
|
99
114
|
bucket.triples.splice(idx, 1);
|
|
100
115
|
bucket.rebuild(this.symbols);
|
|
116
|
+
|
|
117
|
+
// Update relation index: remove bucket if it no longer has this relation
|
|
118
|
+
const indexKey = s + '\0' + r;
|
|
119
|
+
const set = this._relIndex.get(indexKey);
|
|
120
|
+
if (set) {
|
|
121
|
+
const stillHas = bucket.triples.some(t => t.relation === r);
|
|
122
|
+
if (!stillHas) {
|
|
123
|
+
set.delete(ids[i]);
|
|
124
|
+
if (set.size === 0) this._relIndex.delete(indexKey);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
if (bucket.count === 0 && ids[i].includes('#')) {
|
|
102
129
|
this.buckets.delete(ids[i]);
|
|
103
130
|
ids.splice(i, 1);
|
|
104
131
|
}
|
|
132
|
+
|
|
133
|
+
// Invalidate adjacency cache
|
|
134
|
+
this._adjDirty = true;
|
|
135
|
+
|
|
105
136
|
return true;
|
|
106
137
|
}
|
|
107
138
|
return false;
|
|
108
139
|
}
|
|
109
140
|
|
|
110
141
|
query(subject, relation) {
|
|
111
|
-
const
|
|
142
|
+
const s = subject.toLowerCase().trim();
|
|
143
|
+
const r = relation.toLowerCase().trim();
|
|
144
|
+
|
|
145
|
+
// Fast-path: use relation index to scan only relevant shards
|
|
146
|
+
const indexKey = s + '\0' + r;
|
|
147
|
+
const indexed = this._relIndex.get(indexKey);
|
|
148
|
+
const buckets = indexed
|
|
149
|
+
? [...indexed].map(id => this.buckets.get(id)).filter(Boolean)
|
|
150
|
+
: this._subjectBuckets(subject); // fallback for pre-index data
|
|
151
|
+
|
|
112
152
|
if (buckets.length === 0) return { match: null, score: 0, confident: false, bucket: null };
|
|
113
153
|
const probe = bind(this.symbols.get(subject), this.symbols.get(relation));
|
|
114
154
|
let bestName = null, bestScore = -1, bestBucket = null;
|
|
@@ -179,6 +219,42 @@ export class HRRMemory {
|
|
|
179
219
|
return { type: 'miss', query: question };
|
|
180
220
|
}
|
|
181
221
|
|
|
222
|
+
// ── Graph Traversal ──────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Build or return cached adjacency list.
|
|
226
|
+
* @returns {Map<string, Array<{relation: string, object: string, weight: number}>>}
|
|
227
|
+
*/
|
|
228
|
+
_getAdjacency() {
|
|
229
|
+
if (this._adjDirty || !this._adjacency) {
|
|
230
|
+
this._adjacency = buildAdjacency(this);
|
|
231
|
+
this._adjDirty = false;
|
|
232
|
+
}
|
|
233
|
+
return this._adjacency;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Find shortest path between two entities using Dijkstra's algorithm.
|
|
238
|
+
* @param {string} from - Starting entity
|
|
239
|
+
* @param {string} to - Target entity
|
|
240
|
+
* @param {{ maxHops?: number, relations?: string[] }} [options]
|
|
241
|
+
* @returns {{ found: boolean, path: Array<{subject: string, relation: string, object: string, weight: number}>, totalWeight: number, hops: number } | null}
|
|
242
|
+
*/
|
|
243
|
+
traverse(from, to, options = {}) {
|
|
244
|
+
const adj = this._getAdjacency();
|
|
245
|
+
return dijkstra(adj, from, to, options);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get direct neighbors of a subject in the knowledge graph.
|
|
250
|
+
* @param {string} subject
|
|
251
|
+
* @returns {Array<{relation: string, object: string, weight: number}>}
|
|
252
|
+
*/
|
|
253
|
+
neighbors(subject) {
|
|
254
|
+
const adj = this._getAdjacency();
|
|
255
|
+
return getNeighbors(adj, subject);
|
|
256
|
+
}
|
|
257
|
+
|
|
182
258
|
stats() {
|
|
183
259
|
let totalFacts = 0;
|
|
184
260
|
const bucketInfo = [];
|
|
@@ -219,6 +295,17 @@ export class HRRMemory {
|
|
|
219
295
|
for (const [k, v] of Object.entries(data.routing || {})) {
|
|
220
296
|
mem.routing.set(k, Array.isArray(v) ? v : [v]);
|
|
221
297
|
}
|
|
298
|
+
|
|
299
|
+
// Rebuild relation index from deserialized triples
|
|
300
|
+
for (const [bucketId, bucket] of mem.buckets) {
|
|
301
|
+
for (const t of bucket.triples) {
|
|
302
|
+
const indexKey = t.subject + '\0' + t.relation;
|
|
303
|
+
let set = mem._relIndex.get(indexKey);
|
|
304
|
+
if (!set) { set = new Set(); mem._relIndex.set(indexKey, set); }
|
|
305
|
+
set.add(bucketId);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
222
309
|
return mem;
|
|
223
310
|
}
|
|
224
311
|
|
package/src/observation.js
CHANGED
|
@@ -49,6 +49,8 @@ export class ObservationMemory {
|
|
|
49
49
|
search(relation, object) { return this._hrr.search(relation, object); }
|
|
50
50
|
ask(question) { return this._hrr.ask(question); }
|
|
51
51
|
stats() { return this._hrr.stats(); }
|
|
52
|
+
traverse(from, to, options) { return this._hrr.traverse(from, to, options); }
|
|
53
|
+
neighbors(subject) { return this._hrr.neighbors(subject); }
|
|
52
54
|
|
|
53
55
|
// ── Store (async) ──────────────────────────────────
|
|
54
56
|
|
package/src/ops.js
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core HRR operations — circular convolution, correlation, similarity.
|
|
3
|
-
* All vectors are Float32Array. FFT computed in Float64 for precision.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/** Generate a random unit vector of dimension d (Gaussian, normalized) */
|
|
7
|
-
export function randomVector(d) {
|
|
8
|
-
const v = new Float32Array(d);
|
|
9
|
-
for (let i = 0; i < d; i += 2) {
|
|
10
|
-
const u1 = Math.random(), u2 = Math.random();
|
|
11
|
-
const r = Math.sqrt(-2 * Math.log(u1));
|
|
12
|
-
v[i] = r * Math.cos(2 * Math.PI * u2);
|
|
13
|
-
if (i + 1 < d) v[i + 1] = r * Math.sin(2 * Math.PI * u2);
|
|
14
|
-
}
|
|
15
|
-
return normalize(v);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Normalize vector to unit length */
|
|
19
|
-
export function normalize(v) {
|
|
20
|
-
let norm = 0;
|
|
21
|
-
for (let i = 0; i < v.length; i++) norm += v[i] * v[i];
|
|
22
|
-
norm = Math.sqrt(norm);
|
|
23
|
-
if (norm === 0) return v;
|
|
24
|
-
const out = new Float32Array(v.length);
|
|
25
|
-
for (let i = 0; i < v.length; i++) out[i] = v[i] / norm;
|
|
26
|
-
return out;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Cosine similarity between two vectors */
|
|
30
|
-
export function similarity(a, b) {
|
|
31
|
-
let dot = 0, na = 0, nb = 0;
|
|
32
|
-
for (let i = 0; i < a.length; i++) {
|
|
33
|
-
dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i];
|
|
34
|
-
}
|
|
35
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-10);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── FFT (Cooley-Tukey radix-2) ──
|
|
39
|
-
|
|
40
|
-
function fft(re, im, inverse) {
|
|
41
|
-
const n = re.length;
|
|
42
|
-
for (let i = 1, j = 0; i < n; i++) {
|
|
43
|
-
let bit = n >> 1;
|
|
44
|
-
for (; j & bit; bit >>= 1) j ^= bit;
|
|
45
|
-
j ^= bit;
|
|
46
|
-
if (i < j) {
|
|
47
|
-
[re[i], re[j]] = [re[j], re[i]];
|
|
48
|
-
[im[i], im[j]] = [im[j], im[i]];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
for (let len = 2; len <= n; len <<= 1) {
|
|
52
|
-
const ang = (inverse ? -1 : 1) * 2 * Math.PI / len;
|
|
53
|
-
const wRe = Math.cos(ang), wIm = Math.sin(ang);
|
|
54
|
-
for (let i = 0; i < n; i += len) {
|
|
55
|
-
let curRe = 1, curIm = 0;
|
|
56
|
-
for (let j = 0; j < len / 2; j++) {
|
|
57
|
-
const uRe = re[i+j], uIm = im[i+j];
|
|
58
|
-
const vRe = re[i+j+len/2]*curRe - im[i+j+len/2]*curIm;
|
|
59
|
-
const vIm = re[i+j+len/2]*curIm + im[i+j+len/2]*curRe;
|
|
60
|
-
re[i+j] = uRe+vRe; im[i+j] = uIm+vIm;
|
|
61
|
-
re[i+j+len/2] = uRe-vRe; im[i+j+len/2] = uIm-vIm;
|
|
62
|
-
const nr = curRe*wRe - curIm*wIm;
|
|
63
|
-
curIm = curRe*wIm + curIm*wRe;
|
|
64
|
-
curRe = nr;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (inverse) {
|
|
69
|
-
for (let i = 0; i < n; i++) { re[i] /= n; im[i] /= n; }
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function nextPow2(n) { let p = 1; while (p < n) p <<= 1; return p; }
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Circular convolution (binding): a ⊛ b
|
|
77
|
-
* Creates an association between two vectors.
|
|
78
|
-
* Computed in Float64, returned as Float32.
|
|
79
|
-
*/
|
|
80
|
-
export function bind(a, b) {
|
|
81
|
-
const d = a.length, n = nextPow2(d);
|
|
82
|
-
const aR = new Float64Array(n), aI = new Float64Array(n);
|
|
83
|
-
const bR = new Float64Array(n), bI = new Float64Array(n);
|
|
84
|
-
for (let i = 0; i < d; i++) { aR[i] = a[i]; bR[i] = b[i]; }
|
|
85
|
-
fft(aR, aI, false); fft(bR, bI, false);
|
|
86
|
-
const cR = new Float64Array(n), cI = new Float64Array(n);
|
|
87
|
-
for (let i = 0; i < n; i++) {
|
|
88
|
-
cR[i] = aR[i]*bR[i] - aI[i]*bI[i];
|
|
89
|
-
cI[i] = aR[i]*bI[i] + aI[i]*bR[i];
|
|
90
|
-
}
|
|
91
|
-
fft(cR, cI, true);
|
|
92
|
-
const out = new Float32Array(d);
|
|
93
|
-
for (let i = 0; i < d; i++) out[i] = cR[i];
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Circular correlation (unbinding): retrieve from association.
|
|
99
|
-
* unbind(key, memory) ≈ the value that was bound with key.
|
|
100
|
-
*/
|
|
101
|
-
export function unbind(key, memory) {
|
|
102
|
-
const d = key.length;
|
|
103
|
-
const inv = new Float32Array(d);
|
|
104
|
-
inv[0] = key[0];
|
|
105
|
-
for (let i = 1; i < d; i++) inv[i] = key[d - i];
|
|
106
|
-
return bind(inv, memory);
|
|
107
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Core HRR operations — circular convolution, correlation, similarity.
|
|
3
|
+
* All vectors are Float32Array. FFT computed in Float64 for precision.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Generate a random unit vector of dimension d (Gaussian, normalized) */
|
|
7
|
+
export function randomVector(d) {
|
|
8
|
+
const v = new Float32Array(d);
|
|
9
|
+
for (let i = 0; i < d; i += 2) {
|
|
10
|
+
const u1 = Math.random(), u2 = Math.random();
|
|
11
|
+
const r = Math.sqrt(-2 * Math.log(u1));
|
|
12
|
+
v[i] = r * Math.cos(2 * Math.PI * u2);
|
|
13
|
+
if (i + 1 < d) v[i + 1] = r * Math.sin(2 * Math.PI * u2);
|
|
14
|
+
}
|
|
15
|
+
return normalize(v);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Normalize vector to unit length */
|
|
19
|
+
export function normalize(v) {
|
|
20
|
+
let norm = 0;
|
|
21
|
+
for (let i = 0; i < v.length; i++) norm += v[i] * v[i];
|
|
22
|
+
norm = Math.sqrt(norm);
|
|
23
|
+
if (norm === 0) return v;
|
|
24
|
+
const out = new Float32Array(v.length);
|
|
25
|
+
for (let i = 0; i < v.length; i++) out[i] = v[i] / norm;
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Cosine similarity between two vectors */
|
|
30
|
+
export function similarity(a, b) {
|
|
31
|
+
let dot = 0, na = 0, nb = 0;
|
|
32
|
+
for (let i = 0; i < a.length; i++) {
|
|
33
|
+
dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i];
|
|
34
|
+
}
|
|
35
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-10);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── FFT (Cooley-Tukey radix-2) ──
|
|
39
|
+
|
|
40
|
+
function fft(re, im, inverse) {
|
|
41
|
+
const n = re.length;
|
|
42
|
+
for (let i = 1, j = 0; i < n; i++) {
|
|
43
|
+
let bit = n >> 1;
|
|
44
|
+
for (; j & bit; bit >>= 1) j ^= bit;
|
|
45
|
+
j ^= bit;
|
|
46
|
+
if (i < j) {
|
|
47
|
+
[re[i], re[j]] = [re[j], re[i]];
|
|
48
|
+
[im[i], im[j]] = [im[j], im[i]];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (let len = 2; len <= n; len <<= 1) {
|
|
52
|
+
const ang = (inverse ? -1 : 1) * 2 * Math.PI / len;
|
|
53
|
+
const wRe = Math.cos(ang), wIm = Math.sin(ang);
|
|
54
|
+
for (let i = 0; i < n; i += len) {
|
|
55
|
+
let curRe = 1, curIm = 0;
|
|
56
|
+
for (let j = 0; j < len / 2; j++) {
|
|
57
|
+
const uRe = re[i+j], uIm = im[i+j];
|
|
58
|
+
const vRe = re[i+j+len/2]*curRe - im[i+j+len/2]*curIm;
|
|
59
|
+
const vIm = re[i+j+len/2]*curIm + im[i+j+len/2]*curRe;
|
|
60
|
+
re[i+j] = uRe+vRe; im[i+j] = uIm+vIm;
|
|
61
|
+
re[i+j+len/2] = uRe-vRe; im[i+j+len/2] = uIm-vIm;
|
|
62
|
+
const nr = curRe*wRe - curIm*wIm;
|
|
63
|
+
curIm = curRe*wIm + curIm*wRe;
|
|
64
|
+
curRe = nr;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (inverse) {
|
|
69
|
+
for (let i = 0; i < n; i++) { re[i] /= n; im[i] /= n; }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function nextPow2(n) { let p = 1; while (p < n) p <<= 1; return p; }
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Circular convolution (binding): a ⊛ b
|
|
77
|
+
* Creates an association between two vectors.
|
|
78
|
+
* Computed in Float64, returned as Float32.
|
|
79
|
+
*/
|
|
80
|
+
export function bind(a, b) {
|
|
81
|
+
const d = a.length, n = nextPow2(d);
|
|
82
|
+
const aR = new Float64Array(n), aI = new Float64Array(n);
|
|
83
|
+
const bR = new Float64Array(n), bI = new Float64Array(n);
|
|
84
|
+
for (let i = 0; i < d; i++) { aR[i] = a[i]; bR[i] = b[i]; }
|
|
85
|
+
fft(aR, aI, false); fft(bR, bI, false);
|
|
86
|
+
const cR = new Float64Array(n), cI = new Float64Array(n);
|
|
87
|
+
for (let i = 0; i < n; i++) {
|
|
88
|
+
cR[i] = aR[i]*bR[i] - aI[i]*bI[i];
|
|
89
|
+
cI[i] = aR[i]*bI[i] + aI[i]*bR[i];
|
|
90
|
+
}
|
|
91
|
+
fft(cR, cI, true);
|
|
92
|
+
const out = new Float32Array(d);
|
|
93
|
+
for (let i = 0; i < d; i++) out[i] = cR[i];
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Circular correlation (unbinding): retrieve from association.
|
|
99
|
+
* unbind(key, memory) ≈ the value that was bound with key.
|
|
100
|
+
*/
|
|
101
|
+
export function unbind(key, memory) {
|
|
102
|
+
const d = key.length;
|
|
103
|
+
const inv = new Float32Array(d);
|
|
104
|
+
inv[0] = key[0];
|
|
105
|
+
for (let i = 1; i < d; i++) inv[i] = key[d - i];
|
|
106
|
+
return bind(inv, memory);
|
|
107
|
+
}
|
package/src/symbols.js
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared symbol table — maps string names to random vectors.
|
|
3
|
-
* Shared across all buckets to save memory.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { randomVector, similarity } from './ops.js';
|
|
7
|
-
|
|
8
|
-
export class SymbolTable {
|
|
9
|
-
constructor(d = 2048) {
|
|
10
|
-
this.d = d;
|
|
11
|
-
this.symbols = new Map();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
get(name) {
|
|
15
|
-
const key = name.toLowerCase().trim();
|
|
16
|
-
if (!this.symbols.has(key)) this.symbols.set(key, randomVector(this.d));
|
|
17
|
-
return this.symbols.get(key);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
has(name) { return this.symbols.has(name.toLowerCase().trim()); }
|
|
21
|
-
|
|
22
|
-
get size() { return this.symbols.size; }
|
|
23
|
-
|
|
24
|
-
get bytes() { return this.symbols.size * this.d * 4; }
|
|
25
|
-
|
|
26
|
-
nearest(vec, candidates = null, topK = 1) {
|
|
27
|
-
const source = candidates || this.symbols;
|
|
28
|
-
const results = [];
|
|
29
|
-
for (const [name, svec] of source) {
|
|
30
|
-
results.push({ name, score: similarity(vec, svec) });
|
|
31
|
-
}
|
|
32
|
-
results.sort((a, b) => b.score - a.score);
|
|
33
|
-
return topK === 1 ? results[0] || null : results.slice(0, topK);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
toJSON() {
|
|
37
|
-
const out = {};
|
|
38
|
-
for (const [k, v] of this.symbols) out[k] = Array.from(v);
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
static fromJSON(data, d) {
|
|
43
|
-
const st = new SymbolTable(d);
|
|
44
|
-
for (const [k, v] of Object.entries(data)) {
|
|
45
|
-
st.symbols.set(k, new Float32Array(v));
|
|
46
|
-
}
|
|
47
|
-
return st;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared symbol table — maps string names to random vectors.
|
|
3
|
+
* Shared across all buckets to save memory.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomVector, similarity } from './ops.js';
|
|
7
|
+
|
|
8
|
+
export class SymbolTable {
|
|
9
|
+
constructor(d = 2048) {
|
|
10
|
+
this.d = d;
|
|
11
|
+
this.symbols = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get(name) {
|
|
15
|
+
const key = name.toLowerCase().trim();
|
|
16
|
+
if (!this.symbols.has(key)) this.symbols.set(key, randomVector(this.d));
|
|
17
|
+
return this.symbols.get(key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
has(name) { return this.symbols.has(name.toLowerCase().trim()); }
|
|
21
|
+
|
|
22
|
+
get size() { return this.symbols.size; }
|
|
23
|
+
|
|
24
|
+
get bytes() { return this.symbols.size * this.d * 4; }
|
|
25
|
+
|
|
26
|
+
nearest(vec, candidates = null, topK = 1) {
|
|
27
|
+
const source = candidates || this.symbols;
|
|
28
|
+
const results = [];
|
|
29
|
+
for (const [name, svec] of source) {
|
|
30
|
+
results.push({ name, score: similarity(vec, svec) });
|
|
31
|
+
}
|
|
32
|
+
results.sort((a, b) => b.score - a.score);
|
|
33
|
+
return topK === 1 ? results[0] || null : results.slice(0, topK);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toJSON() {
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const [k, v] of this.symbols) out[k] = Array.from(v);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static fromJSON(data, d) {
|
|
43
|
+
const st = new SymbolTable(d);
|
|
44
|
+
for (const [k, v] of Object.entries(data)) {
|
|
45
|
+
st.symbols.set(k, new Float32Array(v));
|
|
46
|
+
}
|
|
47
|
+
return st;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/traversal.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph traversal for HRR Memory using Dijkstra's algorithm.
|
|
3
|
+
*
|
|
4
|
+
* Builds an adjacency list from stored triples and finds shortest paths
|
|
5
|
+
* between entities. Edge weights are derived from HRR confidence scores:
|
|
6
|
+
* weight = 1 - score, so high-confidence edges are cheaper to traverse.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build adjacency list from all triples in HRRMemory.
|
|
11
|
+
* @param {import('./memory.js').HRRMemory} hrr
|
|
12
|
+
* @returns {Map<string, Array<{relation: string, object: string, weight: number}>>}
|
|
13
|
+
*/
|
|
14
|
+
export function buildAdjacency(hrr) {
|
|
15
|
+
const adj = new Map();
|
|
16
|
+
|
|
17
|
+
for (const bucket of hrr.buckets.values()) {
|
|
18
|
+
for (const t of bucket.triples) {
|
|
19
|
+
// Forward edge: subject → object
|
|
20
|
+
if (!adj.has(t.subject)) adj.set(t.subject, []);
|
|
21
|
+
adj.get(t.subject).push({
|
|
22
|
+
relation: t.relation,
|
|
23
|
+
object: t.object,
|
|
24
|
+
weight: edgeWeight(hrr, t.subject, t.relation),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Ensure object exists as a node (even if it has no outgoing edges)
|
|
28
|
+
if (!adj.has(t.object)) adj.set(t.object, []);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return adj;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute edge weight from HRR query confidence.
|
|
37
|
+
* Falls back to 0.5 (neutral) if query returns no confidence.
|
|
38
|
+
*/
|
|
39
|
+
function edgeWeight(hrr, subject, relation) {
|
|
40
|
+
const result = hrr.query(subject, relation);
|
|
41
|
+
if (result && result.confident) {
|
|
42
|
+
return Math.max(0, 1 - result.score);
|
|
43
|
+
}
|
|
44
|
+
// For low-confidence edges, still use score but with a floor
|
|
45
|
+
if (result && result.score > 0) {
|
|
46
|
+
return 1 - result.score;
|
|
47
|
+
}
|
|
48
|
+
return 0.5;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Dijkstra's shortest path between two entities.
|
|
53
|
+
*
|
|
54
|
+
* @param {Map<string, Array<{relation: string, object: string, weight: number}>>} adjacency
|
|
55
|
+
* @param {string} from - Starting entity
|
|
56
|
+
* @param {string} to - Target entity
|
|
57
|
+
* @param {object} [options]
|
|
58
|
+
* @param {number} [options.maxHops=10] - Maximum hops to search
|
|
59
|
+
* @param {string[]} [options.relations] - Only follow these relation types
|
|
60
|
+
* @returns {{ found: boolean, path: Array<{subject: string, relation: string, object: string, weight: number}>, totalWeight: number, hops: number } | null}
|
|
61
|
+
*/
|
|
62
|
+
export function dijkstra(adjacency, from, to, options = {}) {
|
|
63
|
+
const maxHops = options.maxHops ?? 10;
|
|
64
|
+
const relFilter = options.relations
|
|
65
|
+
? new Set(options.relations.map(r => r.toLowerCase().trim()))
|
|
66
|
+
: null;
|
|
67
|
+
|
|
68
|
+
if (!adjacency.has(from) || !adjacency.has(to)) return null;
|
|
69
|
+
if (from === to) return { found: true, path: [], totalWeight: 0, hops: 0 };
|
|
70
|
+
|
|
71
|
+
// dist.get(node) = best known total weight to reach node
|
|
72
|
+
const dist = new Map();
|
|
73
|
+
// prev.get(node) = { from, relation, weight } describing the edge used
|
|
74
|
+
const prev = new Map();
|
|
75
|
+
// hopCount.get(node) = number of hops to reach node
|
|
76
|
+
const hopCount = new Map();
|
|
77
|
+
|
|
78
|
+
// Simple priority queue using sorted array (sufficient for knowledge graph sizes)
|
|
79
|
+
const pq = [];
|
|
80
|
+
|
|
81
|
+
dist.set(from, 0);
|
|
82
|
+
hopCount.set(from, 0);
|
|
83
|
+
pq.push({ node: from, cost: 0 });
|
|
84
|
+
|
|
85
|
+
while (pq.length > 0) {
|
|
86
|
+
// Extract minimum
|
|
87
|
+
pq.sort((a, b) => a.cost - b.cost);
|
|
88
|
+
const { node: current, cost: currentCost } = pq.shift();
|
|
89
|
+
|
|
90
|
+
// Skip stale entries
|
|
91
|
+
if (currentCost > (dist.get(current) ?? Infinity)) continue;
|
|
92
|
+
|
|
93
|
+
// Found target
|
|
94
|
+
if (current === to) break;
|
|
95
|
+
|
|
96
|
+
// Check hop limit
|
|
97
|
+
const hops = hopCount.get(current) ?? 0;
|
|
98
|
+
if (hops >= maxHops) continue;
|
|
99
|
+
|
|
100
|
+
// Explore neighbors
|
|
101
|
+
const neighbors = adjacency.get(current) || [];
|
|
102
|
+
for (const edge of neighbors) {
|
|
103
|
+
if (relFilter && !relFilter.has(edge.relation)) continue;
|
|
104
|
+
|
|
105
|
+
const newDist = currentCost + edge.weight;
|
|
106
|
+
const prevDist = dist.get(edge.object) ?? Infinity;
|
|
107
|
+
|
|
108
|
+
if (newDist < prevDist) {
|
|
109
|
+
dist.set(edge.object, newDist);
|
|
110
|
+
prev.set(edge.object, { from: current, relation: edge.relation, weight: edge.weight });
|
|
111
|
+
hopCount.set(edge.object, hops + 1);
|
|
112
|
+
pq.push({ node: edge.object, cost: newDist });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!dist.has(to) || dist.get(to) === Infinity) return null;
|
|
118
|
+
|
|
119
|
+
// Reconstruct path
|
|
120
|
+
const path = [];
|
|
121
|
+
let current = to;
|
|
122
|
+
while (current !== from) {
|
|
123
|
+
const step = prev.get(current);
|
|
124
|
+
if (!step) return null;
|
|
125
|
+
path.unshift({
|
|
126
|
+
subject: step.from,
|
|
127
|
+
relation: step.relation,
|
|
128
|
+
object: current,
|
|
129
|
+
weight: step.weight,
|
|
130
|
+
});
|
|
131
|
+
current = step.from;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
found: true,
|
|
136
|
+
path,
|
|
137
|
+
totalWeight: Math.round(dist.get(to) * 1000) / 1000,
|
|
138
|
+
hops: path.length,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get direct neighbors of a subject.
|
|
144
|
+
*
|
|
145
|
+
* @param {Map<string, Array<{relation: string, object: string, weight: number}>>} adjacency
|
|
146
|
+
* @param {string} subject
|
|
147
|
+
* @returns {Array<{relation: string, object: string, weight: number}>}
|
|
148
|
+
*/
|
|
149
|
+
export function neighbors(adjacency, subject) {
|
|
150
|
+
return adjacency.get(subject.toLowerCase().trim()) || [];
|
|
151
|
+
}
|