@claude-flow/memory 3.0.0-alpha.1
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/.agentic-flow/intelligence.json +16 -0
- package/README.md +249 -0
- package/__tests__/coverage/base.css +224 -0
- package/__tests__/coverage/block-navigation.js +87 -0
- package/__tests__/coverage/coverage-final.json +19 -0
- package/__tests__/coverage/favicon.png +0 -0
- package/__tests__/coverage/index.html +206 -0
- package/__tests__/coverage/lcov-report/base.css +224 -0
- package/__tests__/coverage/lcov-report/block-navigation.js +87 -0
- package/__tests__/coverage/lcov-report/favicon.png +0 -0
- package/__tests__/coverage/lcov-report/index.html +206 -0
- package/__tests__/coverage/lcov-report/prettify.css +1 -0
- package/__tests__/coverage/lcov-report/prettify.js +2 -0
- package/__tests__/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/__tests__/coverage/lcov-report/sorter.js +210 -0
- package/__tests__/coverage/lcov-report/src/agentdb-adapter.ts.html +2737 -0
- package/__tests__/coverage/lcov-report/src/agentdb-backend.ts.html +3130 -0
- package/__tests__/coverage/lcov-report/src/application/commands/delete-memory.command.ts.html +601 -0
- package/__tests__/coverage/lcov-report/src/application/commands/index.html +131 -0
- package/__tests__/coverage/lcov-report/src/application/commands/store-memory.command.ts.html +394 -0
- package/__tests__/coverage/lcov-report/src/application/queries/index.html +116 -0
- package/__tests__/coverage/lcov-report/src/application/queries/search-memory.query.ts.html +796 -0
- package/__tests__/coverage/lcov-report/src/application/services/index.html +116 -0
- package/__tests__/coverage/lcov-report/src/application/services/memory-application-service.ts.html +793 -0
- package/__tests__/coverage/lcov-report/src/cache-manager.ts.html +1633 -0
- package/__tests__/coverage/lcov-report/src/database-provider.ts.html +1618 -0
- package/__tests__/coverage/lcov-report/src/domain/entities/index.html +116 -0
- package/__tests__/coverage/lcov-report/src/domain/entities/memory-entry.ts.html +952 -0
- package/__tests__/coverage/lcov-report/src/domain/services/index.html +116 -0
- package/__tests__/coverage/lcov-report/src/domain/services/memory-domain-service.ts.html +1294 -0
- package/__tests__/coverage/lcov-report/src/hnsw-index.ts.html +3124 -0
- package/__tests__/coverage/lcov-report/src/hybrid-backend.ts.html +2167 -0
- package/__tests__/coverage/lcov-report/src/index.html +266 -0
- package/__tests__/coverage/lcov-report/src/infrastructure/repositories/hybrid-memory-repository.ts.html +1633 -0
- package/__tests__/coverage/lcov-report/src/infrastructure/repositories/index.html +116 -0
- package/__tests__/coverage/lcov-report/src/migration.ts.html +2092 -0
- package/__tests__/coverage/lcov-report/src/query-builder.ts.html +1711 -0
- package/__tests__/coverage/lcov-report/src/sqlite-backend.ts.html +2281 -0
- package/__tests__/coverage/lcov-report/src/sqljs-backend.ts.html +2374 -0
- package/__tests__/coverage/lcov-report/src/types.ts.html +2266 -0
- package/__tests__/coverage/lcov.info +10238 -0
- package/__tests__/coverage/prettify.css +1 -0
- package/__tests__/coverage/prettify.js +2 -0
- package/__tests__/coverage/sort-arrow-sprite.png +0 -0
- package/__tests__/coverage/sorter.js +210 -0
- package/__tests__/coverage/src/agentdb-adapter.ts.html +2737 -0
- package/__tests__/coverage/src/agentdb-backend.ts.html +3130 -0
- package/__tests__/coverage/src/application/commands/delete-memory.command.ts.html +601 -0
- package/__tests__/coverage/src/application/commands/index.html +131 -0
- package/__tests__/coverage/src/application/commands/store-memory.command.ts.html +394 -0
- package/__tests__/coverage/src/application/queries/index.html +116 -0
- package/__tests__/coverage/src/application/queries/search-memory.query.ts.html +796 -0
- package/__tests__/coverage/src/application/services/index.html +116 -0
- package/__tests__/coverage/src/application/services/memory-application-service.ts.html +793 -0
- package/__tests__/coverage/src/cache-manager.ts.html +1633 -0
- package/__tests__/coverage/src/database-provider.ts.html +1618 -0
- package/__tests__/coverage/src/domain/entities/index.html +116 -0
- package/__tests__/coverage/src/domain/entities/memory-entry.ts.html +952 -0
- package/__tests__/coverage/src/domain/services/index.html +116 -0
- package/__tests__/coverage/src/domain/services/memory-domain-service.ts.html +1294 -0
- package/__tests__/coverage/src/hnsw-index.ts.html +3124 -0
- package/__tests__/coverage/src/hybrid-backend.ts.html +2167 -0
- package/__tests__/coverage/src/index.html +266 -0
- package/__tests__/coverage/src/infrastructure/repositories/hybrid-memory-repository.ts.html +1633 -0
- package/__tests__/coverage/src/infrastructure/repositories/index.html +116 -0
- package/__tests__/coverage/src/migration.ts.html +2092 -0
- package/__tests__/coverage/src/query-builder.ts.html +1711 -0
- package/__tests__/coverage/src/sqlite-backend.ts.html +2281 -0
- package/__tests__/coverage/src/sqljs-backend.ts.html +2374 -0
- package/__tests__/coverage/src/types.ts.html +2266 -0
- package/benchmarks/cache-hit-rate.bench.ts +535 -0
- package/benchmarks/hnsw-indexing.bench.ts +552 -0
- package/benchmarks/memory-write.bench.ts +469 -0
- package/benchmarks/vector-search.bench.ts +449 -0
- package/dist/agentdb-adapter.d.ts +146 -0
- package/dist/agentdb-adapter.d.ts.map +1 -0
- package/dist/agentdb-adapter.js +679 -0
- package/dist/agentdb-adapter.js.map +1 -0
- package/dist/agentdb-backend.d.ts +214 -0
- package/dist/agentdb-backend.d.ts.map +1 -0
- package/dist/agentdb-backend.js +827 -0
- package/dist/agentdb-backend.js.map +1 -0
- package/dist/agentdb-backend.test.d.ts +7 -0
- package/dist/agentdb-backend.test.d.ts.map +1 -0
- package/dist/agentdb-backend.test.js +258 -0
- package/dist/agentdb-backend.test.js.map +1 -0
- package/dist/application/commands/delete-memory.command.d.ts +65 -0
- package/dist/application/commands/delete-memory.command.d.ts.map +1 -0
- package/dist/application/commands/delete-memory.command.js +129 -0
- package/dist/application/commands/delete-memory.command.js.map +1 -0
- package/dist/application/commands/store-memory.command.d.ts +48 -0
- package/dist/application/commands/store-memory.command.d.ts.map +1 -0
- package/dist/application/commands/store-memory.command.js +72 -0
- package/dist/application/commands/store-memory.command.js.map +1 -0
- package/dist/application/index.d.ts +12 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +15 -0
- package/dist/application/index.js.map +1 -0
- package/dist/application/queries/search-memory.query.d.ts +72 -0
- package/dist/application/queries/search-memory.query.d.ts.map +1 -0
- package/dist/application/queries/search-memory.query.js +143 -0
- package/dist/application/queries/search-memory.query.js.map +1 -0
- package/dist/application/services/memory-application-service.d.ts +121 -0
- package/dist/application/services/memory-application-service.d.ts.map +1 -0
- package/dist/application/services/memory-application-service.js +190 -0
- package/dist/application/services/memory-application-service.js.map +1 -0
- package/dist/cache-manager.d.ts +134 -0
- package/dist/cache-manager.d.ts.map +1 -0
- package/dist/cache-manager.js +407 -0
- package/dist/cache-manager.js.map +1 -0
- package/dist/database-provider.d.ts +86 -0
- package/dist/database-provider.d.ts.map +1 -0
- package/dist/database-provider.js +385 -0
- package/dist/database-provider.js.map +1 -0
- package/dist/database-provider.test.d.ts +7 -0
- package/dist/database-provider.test.d.ts.map +1 -0
- package/dist/database-provider.test.js +285 -0
- package/dist/database-provider.test.js.map +1 -0
- package/dist/domain/entities/memory-entry.d.ts +143 -0
- package/dist/domain/entities/memory-entry.d.ts.map +1 -0
- package/dist/domain/entities/memory-entry.js +226 -0
- package/dist/domain/entities/memory-entry.js.map +1 -0
- package/dist/domain/index.d.ts +11 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +12 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/repositories/memory-repository.interface.d.ts +102 -0
- package/dist/domain/repositories/memory-repository.interface.d.ts.map +1 -0
- package/dist/domain/repositories/memory-repository.interface.js +11 -0
- package/dist/domain/repositories/memory-repository.interface.js.map +1 -0
- package/dist/domain/services/memory-domain-service.d.ts +105 -0
- package/dist/domain/services/memory-domain-service.d.ts.map +1 -0
- package/dist/domain/services/memory-domain-service.js +297 -0
- package/dist/domain/services/memory-domain-service.js.map +1 -0
- package/dist/hnsw-index.d.ts +111 -0
- package/dist/hnsw-index.d.ts.map +1 -0
- package/dist/hnsw-index.js +781 -0
- package/dist/hnsw-index.js.map +1 -0
- package/dist/hybrid-backend.d.ts +217 -0
- package/dist/hybrid-backend.d.ts.map +1 -0
- package/dist/hybrid-backend.js +491 -0
- package/dist/hybrid-backend.js.map +1 -0
- package/dist/hybrid-backend.test.d.ts +8 -0
- package/dist/hybrid-backend.test.d.ts.map +1 -0
- package/dist/hybrid-backend.test.js +320 -0
- package/dist/hybrid-backend.test.js.map +1 -0
- package/dist/index.d.ts +188 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +345 -0
- package/dist/index.js.map +1 -0
- package/dist/infrastructure/index.d.ts +17 -0
- package/dist/infrastructure/index.d.ts.map +1 -0
- package/dist/infrastructure/index.js +16 -0
- package/dist/infrastructure/index.js.map +1 -0
- package/dist/infrastructure/repositories/hybrid-memory-repository.d.ts +66 -0
- package/dist/infrastructure/repositories/hybrid-memory-repository.d.ts.map +1 -0
- package/dist/infrastructure/repositories/hybrid-memory-repository.js +409 -0
- package/dist/infrastructure/repositories/hybrid-memory-repository.js.map +1 -0
- package/dist/migration.d.ts +68 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +513 -0
- package/dist/migration.js.map +1 -0
- package/dist/query-builder.d.ts +211 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +438 -0
- package/dist/query-builder.js.map +1 -0
- package/dist/sqlite-backend.d.ts +121 -0
- package/dist/sqlite-backend.d.ts.map +1 -0
- package/dist/sqlite-backend.js +564 -0
- package/dist/sqlite-backend.js.map +1 -0
- package/dist/sqljs-backend.d.ts +128 -0
- package/dist/sqljs-backend.d.ts.map +1 -0
- package/dist/sqljs-backend.js +598 -0
- package/dist/sqljs-backend.js.map +1 -0
- package/dist/types.d.ts +481 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +58 -0
- package/dist/types.js.map +1 -0
- package/docs/AGENTDB-INTEGRATION.md +388 -0
- package/docs/CROSS_PLATFORM.md +505 -0
- package/docs/WINDOWS_SUPPORT.md +422 -0
- package/examples/agentdb-example.ts +345 -0
- package/examples/cross-platform-usage.ts +326 -0
- package/framework/benchmark.ts +112 -0
- package/package.json +31 -0
- package/src/agentdb-adapter.ts +884 -0
- package/src/agentdb-backend.test.ts +339 -0
- package/src/agentdb-backend.ts +1016 -0
- package/src/application/commands/delete-memory.command.ts +172 -0
- package/src/application/commands/store-memory.command.ts +103 -0
- package/src/application/index.ts +36 -0
- package/src/application/queries/search-memory.query.ts +237 -0
- package/src/application/services/memory-application-service.ts +236 -0
- package/src/cache-manager.ts +516 -0
- package/src/database-provider.test.ts +364 -0
- package/src/database-provider.ts +511 -0
- package/src/domain/entities/memory-entry.ts +289 -0
- package/src/domain/index.ts +35 -0
- package/src/domain/repositories/memory-repository.interface.ts +120 -0
- package/src/domain/services/memory-domain-service.ts +403 -0
- package/src/hnsw-index.ts +1013 -0
- package/src/hybrid-backend.test.ts +399 -0
- package/src/hybrid-backend.ts +694 -0
- package/src/index.ts +515 -0
- package/src/infrastructure/index.ts +23 -0
- package/src/infrastructure/repositories/hybrid-memory-repository.ts +516 -0
- package/src/migration.ts +669 -0
- package/src/query-builder.ts +542 -0
- package/src/sqlite-backend.ts +732 -0
- package/src/sqljs-backend.ts +763 -0
- package/src/types.ts +727 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/verify-cross-platform.ts +170 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HNSW Indexing Benchmark
|
|
3
|
+
*
|
|
4
|
+
* Target: <10ms for index operations
|
|
5
|
+
*
|
|
6
|
+
* Measures HNSW index construction, updates, and maintenance performance.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { benchmark, BenchmarkRunner, formatTime, meetsTarget } from '../framework/benchmark.js';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// HNSW Implementation
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
interface HNSWConfig {
|
|
16
|
+
dimensions: number;
|
|
17
|
+
maxElements: number;
|
|
18
|
+
M: number; // Max connections per node
|
|
19
|
+
efConstruction: number; // Size of dynamic candidate list during construction
|
|
20
|
+
mL: number; // Level generation parameter
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface HNSWNode {
|
|
24
|
+
id: number;
|
|
25
|
+
vector: Float32Array;
|
|
26
|
+
level: number;
|
|
27
|
+
connections: Map<number, number[]>; // level -> neighbors
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* HNSW Index implementation for benchmarking
|
|
32
|
+
*/
|
|
33
|
+
class HNSWIndex {
|
|
34
|
+
private nodes: Map<number, HNSWNode> = new Map();
|
|
35
|
+
private entryPoint: number | null = null;
|
|
36
|
+
private maxLevel = 0;
|
|
37
|
+
private config: HNSWConfig;
|
|
38
|
+
|
|
39
|
+
constructor(config: Partial<HNSWConfig> = {}) {
|
|
40
|
+
this.config = {
|
|
41
|
+
dimensions: 384,
|
|
42
|
+
maxElements: 100000,
|
|
43
|
+
M: 16,
|
|
44
|
+
efConstruction: 200,
|
|
45
|
+
mL: 1 / Math.log(16),
|
|
46
|
+
...config,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate random level for new node
|
|
52
|
+
*/
|
|
53
|
+
private randomLevel(): number {
|
|
54
|
+
let level = 0;
|
|
55
|
+
while (Math.random() < this.config.mL && level < Math.log2(this.config.maxElements)) {
|
|
56
|
+
level++;
|
|
57
|
+
}
|
|
58
|
+
return level;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Calculate distance between two vectors (using cosine similarity)
|
|
63
|
+
*/
|
|
64
|
+
private distance(a: Float32Array, b: Float32Array): number {
|
|
65
|
+
let dot = 0;
|
|
66
|
+
for (let i = 0; i < a.length; i++) {
|
|
67
|
+
dot += a[i]! * b[i]!;
|
|
68
|
+
}
|
|
69
|
+
return 1 - dot; // Convert similarity to distance
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Add a vector to the index
|
|
74
|
+
*/
|
|
75
|
+
add(id: number, vector: Float32Array): void {
|
|
76
|
+
const level = this.randomLevel();
|
|
77
|
+
const node: HNSWNode = {
|
|
78
|
+
id,
|
|
79
|
+
vector,
|
|
80
|
+
level,
|
|
81
|
+
connections: new Map(),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (let l = 0; l <= level; l++) {
|
|
85
|
+
node.connections.set(l, []);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.entryPoint === null) {
|
|
89
|
+
this.entryPoint = id;
|
|
90
|
+
this.maxLevel = level;
|
|
91
|
+
this.nodes.set(id, node);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Find entry point for this level
|
|
96
|
+
let currentNode = this.nodes.get(this.entryPoint)!;
|
|
97
|
+
let currentLevel = this.maxLevel;
|
|
98
|
+
|
|
99
|
+
// Greedy search down to target level
|
|
100
|
+
while (currentLevel > level) {
|
|
101
|
+
const neighbors = currentNode.connections.get(currentLevel) || [];
|
|
102
|
+
let closest = currentNode;
|
|
103
|
+
let closestDist = this.distance(vector, currentNode.vector);
|
|
104
|
+
|
|
105
|
+
for (const neighborId of neighbors) {
|
|
106
|
+
const neighbor = this.nodes.get(neighborId)!;
|
|
107
|
+
const dist = this.distance(vector, neighbor.vector);
|
|
108
|
+
if (dist < closestDist) {
|
|
109
|
+
closest = neighbor;
|
|
110
|
+
closestDist = dist;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (closest === currentNode) {
|
|
115
|
+
currentLevel--;
|
|
116
|
+
} else {
|
|
117
|
+
currentNode = closest;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Insert at each level
|
|
122
|
+
for (let l = Math.min(level, this.maxLevel); l >= 0; l--) {
|
|
123
|
+
// Find neighbors at this level (simplified)
|
|
124
|
+
const candidates = this.searchLayer(vector, currentNode.id, l, this.config.efConstruction);
|
|
125
|
+
const neighbors = candidates.slice(0, this.config.M);
|
|
126
|
+
|
|
127
|
+
// Connect node to neighbors
|
|
128
|
+
node.connections.set(l, neighbors.map((n) => n.id));
|
|
129
|
+
|
|
130
|
+
// Add reverse connections
|
|
131
|
+
for (const { id: neighborId } of neighbors) {
|
|
132
|
+
const neighbor = this.nodes.get(neighborId)!;
|
|
133
|
+
const neighborConnections = neighbor.connections.get(l) || [];
|
|
134
|
+
if (neighborConnections.length < this.config.M) {
|
|
135
|
+
neighborConnections.push(id);
|
|
136
|
+
neighbor.connections.set(l, neighborConnections);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.nodes.set(id, node);
|
|
142
|
+
|
|
143
|
+
if (level > this.maxLevel) {
|
|
144
|
+
this.maxLevel = level;
|
|
145
|
+
this.entryPoint = id;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Search at a specific layer
|
|
151
|
+
*/
|
|
152
|
+
private searchLayer(
|
|
153
|
+
query: Float32Array,
|
|
154
|
+
entryId: number,
|
|
155
|
+
level: number,
|
|
156
|
+
ef: number
|
|
157
|
+
): Array<{ id: number; distance: number }> {
|
|
158
|
+
const visited = new Set<number>([entryId]);
|
|
159
|
+
const entryNode = this.nodes.get(entryId)!;
|
|
160
|
+
const candidates = [{ id: entryId, distance: this.distance(query, entryNode.vector) }];
|
|
161
|
+
const results = [...candidates];
|
|
162
|
+
|
|
163
|
+
while (candidates.length > 0) {
|
|
164
|
+
candidates.sort((a, b) => a.distance - b.distance);
|
|
165
|
+
const current = candidates.shift()!;
|
|
166
|
+
|
|
167
|
+
if (results.length >= ef && current.distance > results[results.length - 1]!.distance) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const currentNode = this.nodes.get(current.id)!;
|
|
172
|
+
const neighbors = currentNode.connections.get(level) || [];
|
|
173
|
+
|
|
174
|
+
for (const neighborId of neighbors) {
|
|
175
|
+
if (visited.has(neighborId)) continue;
|
|
176
|
+
visited.add(neighborId);
|
|
177
|
+
|
|
178
|
+
const neighbor = this.nodes.get(neighborId)!;
|
|
179
|
+
const dist = this.distance(query, neighbor.vector);
|
|
180
|
+
|
|
181
|
+
if (results.length < ef || dist < results[results.length - 1]!.distance) {
|
|
182
|
+
results.push({ id: neighborId, distance: dist });
|
|
183
|
+
candidates.push({ id: neighborId, distance: dist });
|
|
184
|
+
results.sort((a, b) => a.distance - b.distance);
|
|
185
|
+
if (results.length > ef) {
|
|
186
|
+
results.pop();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Search for k nearest neighbors
|
|
197
|
+
*/
|
|
198
|
+
search(query: Float32Array, k: number, ef = 50): Array<{ id: number; distance: number }> {
|
|
199
|
+
if (this.entryPoint === null) return [];
|
|
200
|
+
|
|
201
|
+
let currentId = this.entryPoint;
|
|
202
|
+
const currentNode = this.nodes.get(currentId)!;
|
|
203
|
+
|
|
204
|
+
// Greedy descent to level 0
|
|
205
|
+
for (let level = this.maxLevel; level > 0; level--) {
|
|
206
|
+
const results = this.searchLayer(query, currentId, level, 1);
|
|
207
|
+
if (results.length > 0) {
|
|
208
|
+
currentId = results[0]!.id;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Search at level 0
|
|
213
|
+
const results = this.searchLayer(query, currentId, 0, Math.max(ef, k));
|
|
214
|
+
return results.slice(0, k);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Remove a vector from the index
|
|
219
|
+
*/
|
|
220
|
+
remove(id: number): boolean {
|
|
221
|
+
const node = this.nodes.get(id);
|
|
222
|
+
if (!node) return false;
|
|
223
|
+
|
|
224
|
+
// Remove all connections to this node
|
|
225
|
+
for (const [level, neighbors] of node.connections) {
|
|
226
|
+
for (const neighborId of neighbors) {
|
|
227
|
+
const neighbor = this.nodes.get(neighborId);
|
|
228
|
+
if (neighbor) {
|
|
229
|
+
const neighborConns = neighbor.connections.get(level);
|
|
230
|
+
if (neighborConns) {
|
|
231
|
+
const idx = neighborConns.indexOf(id);
|
|
232
|
+
if (idx >= 0) {
|
|
233
|
+
neighborConns.splice(idx, 1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.nodes.delete(id);
|
|
241
|
+
|
|
242
|
+
// Update entry point if needed
|
|
243
|
+
if (this.entryPoint === id) {
|
|
244
|
+
this.entryPoint = this.nodes.size > 0 ? this.nodes.keys().next().value : null;
|
|
245
|
+
this.maxLevel = this.entryPoint !== null
|
|
246
|
+
? this.nodes.get(this.entryPoint)!.level
|
|
247
|
+
: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
get size(): number {
|
|
254
|
+
return this.nodes.size;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ============================================================================
|
|
259
|
+
// Helper Functions
|
|
260
|
+
// ============================================================================
|
|
261
|
+
|
|
262
|
+
function generateVector(dim: number): Float32Array {
|
|
263
|
+
const v = new Float32Array(dim);
|
|
264
|
+
let norm = 0;
|
|
265
|
+
for (let i = 0; i < dim; i++) {
|
|
266
|
+
v[i] = Math.random() * 2 - 1;
|
|
267
|
+
norm += v[i]! * v[i]!;
|
|
268
|
+
}
|
|
269
|
+
norm = Math.sqrt(norm);
|
|
270
|
+
for (let i = 0; i < dim; i++) {
|
|
271
|
+
v[i]! /= norm;
|
|
272
|
+
}
|
|
273
|
+
return v;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Benchmark Suite
|
|
278
|
+
// ============================================================================
|
|
279
|
+
|
|
280
|
+
export async function runHNSWIndexingBenchmarks(): Promise<void> {
|
|
281
|
+
const runner = new BenchmarkRunner('HNSW Indexing');
|
|
282
|
+
|
|
283
|
+
console.log('\n--- HNSW Indexing Benchmarks ---\n');
|
|
284
|
+
|
|
285
|
+
const dimensions = 384;
|
|
286
|
+
|
|
287
|
+
// Benchmark 1: Single Vector Insert
|
|
288
|
+
const singleInsertResult = await runner.run(
|
|
289
|
+
'single-vector-insert',
|
|
290
|
+
async () => {
|
|
291
|
+
const index = new HNSWIndex({ dimensions });
|
|
292
|
+
const vector = generateVector(dimensions);
|
|
293
|
+
index.add(0, vector);
|
|
294
|
+
},
|
|
295
|
+
{ iterations: 500 }
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
console.log(`Single Vector Insert: ${formatTime(singleInsertResult.mean)}`);
|
|
299
|
+
const insertTarget = meetsTarget('hnsw-indexing', singleInsertResult.mean);
|
|
300
|
+
console.log(` Target (<10ms): ${insertTarget.met ? 'PASS' : 'FAIL'}`);
|
|
301
|
+
|
|
302
|
+
// Benchmark 2: Batch Insert (100 vectors)
|
|
303
|
+
const batch100Result = await runner.run(
|
|
304
|
+
'batch-insert-100',
|
|
305
|
+
async () => {
|
|
306
|
+
const index = new HNSWIndex({ dimensions });
|
|
307
|
+
const vectors = Array.from({ length: 100 }, () => generateVector(dimensions));
|
|
308
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
309
|
+
index.add(i, vectors[i]!);
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
{ iterations: 20 }
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
console.log(`Batch Insert (100 vectors): ${formatTime(batch100Result.mean)}`);
|
|
316
|
+
console.log(` Per vector: ${formatTime(batch100Result.mean / 100)}`);
|
|
317
|
+
|
|
318
|
+
// Benchmark 3: Batch Insert (1000 vectors)
|
|
319
|
+
const batch1000Result = await runner.run(
|
|
320
|
+
'batch-insert-1000',
|
|
321
|
+
async () => {
|
|
322
|
+
const index = new HNSWIndex({ dimensions });
|
|
323
|
+
const vectors = Array.from({ length: 1000 }, () => generateVector(dimensions));
|
|
324
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
325
|
+
index.add(i, vectors[i]!);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{ iterations: 5 }
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
console.log(`Batch Insert (1000 vectors): ${formatTime(batch1000Result.mean)}`);
|
|
332
|
+
console.log(` Per vector: ${formatTime(batch1000Result.mean / 1000)}`);
|
|
333
|
+
|
|
334
|
+
// Create pre-built index for search benchmarks
|
|
335
|
+
const prebuiltIndex = new HNSWIndex({ dimensions });
|
|
336
|
+
const prebuiltVectors = Array.from({ length: 1000 }, () => generateVector(dimensions));
|
|
337
|
+
for (let i = 0; i < prebuiltVectors.length; i++) {
|
|
338
|
+
prebuiltIndex.add(i, prebuiltVectors[i]!);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Benchmark 4: Search on 1000-vector index
|
|
342
|
+
const query = generateVector(dimensions);
|
|
343
|
+
|
|
344
|
+
const search1000Result = await runner.run(
|
|
345
|
+
'search-1000-vectors',
|
|
346
|
+
async () => {
|
|
347
|
+
prebuiltIndex.search(query, 10, 50);
|
|
348
|
+
},
|
|
349
|
+
{ iterations: 500 }
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
console.log(`Search (1000 vectors, k=10): ${formatTime(search1000Result.mean)}`);
|
|
353
|
+
|
|
354
|
+
// Benchmark 5: Vector Removal
|
|
355
|
+
const removeResult = await runner.run(
|
|
356
|
+
'vector-removal',
|
|
357
|
+
async () => {
|
|
358
|
+
// Create a small index for removal testing
|
|
359
|
+
const index = new HNSWIndex({ dimensions });
|
|
360
|
+
for (let i = 0; i < 100; i++) {
|
|
361
|
+
index.add(i, generateVector(dimensions));
|
|
362
|
+
}
|
|
363
|
+
// Remove middle element
|
|
364
|
+
index.remove(50);
|
|
365
|
+
},
|
|
366
|
+
{ iterations: 100 }
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
console.log(`Vector Removal (from 100): ${formatTime(removeResult.mean)}`);
|
|
370
|
+
|
|
371
|
+
// Benchmark 6: Index Update (remove + add)
|
|
372
|
+
const updateResult = await runner.run(
|
|
373
|
+
'index-update',
|
|
374
|
+
async () => {
|
|
375
|
+
const index = new HNSWIndex({ dimensions });
|
|
376
|
+
for (let i = 0; i < 100; i++) {
|
|
377
|
+
index.add(i, generateVector(dimensions));
|
|
378
|
+
}
|
|
379
|
+
// Update: remove and re-add
|
|
380
|
+
index.remove(50);
|
|
381
|
+
index.add(50, generateVector(dimensions));
|
|
382
|
+
},
|
|
383
|
+
{ iterations: 100 }
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
console.log(`Index Update (remove + add): ${formatTime(updateResult.mean)}`);
|
|
387
|
+
|
|
388
|
+
// Benchmark 7: Different M values
|
|
389
|
+
const m8Index = new HNSWIndex({ dimensions, M: 8 });
|
|
390
|
+
const m8Vectors = Array.from({ length: 500 }, () => generateVector(dimensions));
|
|
391
|
+
|
|
392
|
+
const m8BuildResult = await runner.run(
|
|
393
|
+
'build-m8-500',
|
|
394
|
+
async () => {
|
|
395
|
+
const index = new HNSWIndex({ dimensions, M: 8 });
|
|
396
|
+
for (let i = 0; i < 500; i++) {
|
|
397
|
+
index.add(i, m8Vectors[i]!);
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
{ iterations: 10 }
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
console.log(`Build (M=8, 500 vectors): ${formatTime(m8BuildResult.mean)}`);
|
|
404
|
+
|
|
405
|
+
const m32BuildResult = await runner.run(
|
|
406
|
+
'build-m32-500',
|
|
407
|
+
async () => {
|
|
408
|
+
const index = new HNSWIndex({ dimensions, M: 32 });
|
|
409
|
+
for (let i = 0; i < 500; i++) {
|
|
410
|
+
index.add(i, m8Vectors[i]!);
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
{ iterations: 10 }
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
console.log(`Build (M=32, 500 vectors): ${formatTime(m32BuildResult.mean)}`);
|
|
417
|
+
|
|
418
|
+
// Benchmark 8: Different ef_construction values
|
|
419
|
+
const ef100Result = await runner.run(
|
|
420
|
+
'build-ef100-500',
|
|
421
|
+
async () => {
|
|
422
|
+
const index = new HNSWIndex({ dimensions, efConstruction: 100 });
|
|
423
|
+
for (let i = 0; i < 500; i++) {
|
|
424
|
+
index.add(i, m8Vectors[i]!);
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
{ iterations: 10 }
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
console.log(`Build (ef=100, 500 vectors): ${formatTime(ef100Result.mean)}`);
|
|
431
|
+
|
|
432
|
+
const ef400Result = await runner.run(
|
|
433
|
+
'build-ef400-500',
|
|
434
|
+
async () => {
|
|
435
|
+
const index = new HNSWIndex({ dimensions, efConstruction: 400 });
|
|
436
|
+
for (let i = 0; i < 500; i++) {
|
|
437
|
+
index.add(i, m8Vectors[i]!);
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
{ iterations: 10 }
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
console.log(`Build (ef=400, 500 vectors): ${formatTime(ef400Result.mean)}`);
|
|
444
|
+
|
|
445
|
+
// Summary
|
|
446
|
+
console.log('\n--- Summary ---');
|
|
447
|
+
console.log(`Single insert: ${formatTime(singleInsertResult.mean)}`);
|
|
448
|
+
console.log(`Per-vector cost at 1000: ${formatTime(batch1000Result.mean / 1000)}`);
|
|
449
|
+
console.log(`Search (1000 vectors): ${formatTime(search1000Result.mean)}`);
|
|
450
|
+
console.log(`M=8 vs M=32: ${(m32BuildResult.mean / m8BuildResult.mean).toFixed(2)}x slower`);
|
|
451
|
+
console.log(`ef=100 vs ef=400: ${(ef400Result.mean / ef100Result.mean).toFixed(2)}x slower`);
|
|
452
|
+
|
|
453
|
+
// Print full results
|
|
454
|
+
runner.printResults();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ============================================================================
|
|
458
|
+
// HNSW Indexing Optimization Strategies
|
|
459
|
+
// ============================================================================
|
|
460
|
+
|
|
461
|
+
export const hnswOptimizations = {
|
|
462
|
+
/**
|
|
463
|
+
* Optimal M selection based on dimension
|
|
464
|
+
*/
|
|
465
|
+
optimalM: {
|
|
466
|
+
description: 'Choose M based on vector dimensions (M = 2 * log2(dimensions))',
|
|
467
|
+
expectedImprovement: '10-30%',
|
|
468
|
+
implementation: `
|
|
469
|
+
function optimalM(dimensions: number): number {
|
|
470
|
+
return Math.round(2 * Math.log2(dimensions));
|
|
471
|
+
}
|
|
472
|
+
// For 384 dimensions: M = 17
|
|
473
|
+
`,
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Parallel index construction
|
|
478
|
+
*/
|
|
479
|
+
parallelConstruction: {
|
|
480
|
+
description: 'Build index using multiple worker threads',
|
|
481
|
+
expectedImprovement: '2-4x',
|
|
482
|
+
implementation: `
|
|
483
|
+
async function parallelBuild(vectors: Float32Array[]): Promise<HNSWIndex> {
|
|
484
|
+
const workers = os.cpus().length;
|
|
485
|
+
const chunks = chunkArray(vectors, workers);
|
|
486
|
+
|
|
487
|
+
const partialIndices = await Promise.all(
|
|
488
|
+
chunks.map((chunk, i) => buildInWorker(chunk, i))
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
return mergeIndices(partialIndices);
|
|
492
|
+
}
|
|
493
|
+
`,
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Incremental updates
|
|
498
|
+
*/
|
|
499
|
+
incrementalUpdates: {
|
|
500
|
+
description: 'Batch updates and apply incrementally',
|
|
501
|
+
expectedImprovement: '20-50%',
|
|
502
|
+
implementation: `
|
|
503
|
+
class IncrementalHNSW {
|
|
504
|
+
private pendingUpdates: Update[] = [];
|
|
505
|
+
private updateThreshold = 100;
|
|
506
|
+
|
|
507
|
+
add(id: number, vector: Float32Array): void {
|
|
508
|
+
this.pendingUpdates.push({ type: 'add', id, vector });
|
|
509
|
+
if (this.pendingUpdates.length >= this.updateThreshold) {
|
|
510
|
+
this.flush();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private flush(): void {
|
|
515
|
+
// Apply all updates in batch
|
|
516
|
+
for (const update of this.pendingUpdates) {
|
|
517
|
+
this.applyUpdate(update);
|
|
518
|
+
}
|
|
519
|
+
this.pendingUpdates = [];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
`,
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Memory-mapped storage
|
|
527
|
+
*/
|
|
528
|
+
mmapStorage: {
|
|
529
|
+
description: 'Use memory-mapped files for large indices',
|
|
530
|
+
expectedImprovement: '30-50% memory, 10-20% speed',
|
|
531
|
+
implementation: `
|
|
532
|
+
import mmap from 'mmap-io';
|
|
533
|
+
|
|
534
|
+
class MmapHNSW {
|
|
535
|
+
private fd: number;
|
|
536
|
+
private buffer: Buffer;
|
|
537
|
+
|
|
538
|
+
constructor(filePath: string, size: number) {
|
|
539
|
+
this.fd = fs.openSync(filePath, 'r+');
|
|
540
|
+
this.buffer = mmap.map(size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED, this.fd);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
`,
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// Run if executed directly
|
|
548
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
549
|
+
runHNSWIndexingBenchmarks().catch(console.error);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export default runHNSWIndexingBenchmarks;
|