family-ai-agent 1.0.6 → 1.0.8
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/.letta/settings.local.json +3 -0
- package/dist/cli/index.js +1 -1
- package/dist/database/adapters/base-adapter.d.ts +81 -0
- package/dist/database/adapters/base-adapter.d.ts.map +1 -0
- package/dist/database/adapters/base-adapter.js +105 -0
- package/dist/database/adapters/base-adapter.js.map +1 -0
- package/dist/database/adapters/index.d.ts +49 -0
- package/dist/database/adapters/index.d.ts.map +1 -0
- package/dist/database/adapters/index.js +200 -0
- package/dist/database/adapters/index.js.map +1 -0
- package/dist/database/adapters/postgres-adapter.d.ts +75 -0
- package/dist/database/adapters/postgres-adapter.d.ts.map +1 -0
- package/dist/database/adapters/postgres-adapter.js +225 -0
- package/dist/database/adapters/postgres-adapter.js.map +1 -0
- package/dist/database/adapters/sqlite-adapter.d.ts +72 -0
- package/dist/database/adapters/sqlite-adapter.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-adapter.js +368 -0
- package/dist/database/adapters/sqlite-adapter.js.map +1 -0
- package/dist/database/cache/cache-keys.d.ts +180 -0
- package/dist/database/cache/cache-keys.d.ts.map +1 -0
- package/dist/database/cache/cache-keys.js +107 -0
- package/dist/database/cache/cache-keys.js.map +1 -0
- package/dist/database/cache/index.d.ts +24 -0
- package/dist/database/cache/index.d.ts.map +1 -0
- package/dist/database/cache/index.js +34 -0
- package/dist/database/cache/index.js.map +1 -0
- package/dist/database/cache/query-cache.d.ts +67 -0
- package/dist/database/cache/query-cache.d.ts.map +1 -0
- package/dist/database/cache/query-cache.js +177 -0
- package/dist/database/cache/query-cache.js.map +1 -0
- package/dist/database/client.d.ts +63 -4
- package/dist/database/client.d.ts.map +1 -1
- package/dist/database/client.js +147 -59
- package/dist/database/client.js.map +1 -1
- package/dist/database/db-config.d.ts +104 -0
- package/dist/database/db-config.d.ts.map +1 -0
- package/dist/database/db-config.js +167 -0
- package/dist/database/db-config.js.map +1 -0
- package/dist/database/drizzle/index.d.ts +42 -0
- package/dist/database/drizzle/index.d.ts.map +1 -0
- package/dist/database/drizzle/index.js +48 -0
- package/dist/database/drizzle/index.js.map +1 -0
- package/dist/database/drizzle/schema/audit.d.ts +533 -0
- package/dist/database/drizzle/schema/audit.d.ts.map +1 -0
- package/dist/database/drizzle/schema/audit.js +71 -0
- package/dist/database/drizzle/schema/audit.js.map +1 -0
- package/dist/database/drizzle/schema/checkpoints.d.ts +665 -0
- package/dist/database/drizzle/schema/checkpoints.d.ts.map +1 -0
- package/dist/database/drizzle/schema/checkpoints.js +110 -0
- package/dist/database/drizzle/schema/checkpoints.js.map +1 -0
- package/dist/database/drizzle/schema/conversations.d.ts +449 -0
- package/dist/database/drizzle/schema/conversations.d.ts.map +1 -0
- package/dist/database/drizzle/schema/conversations.js +91 -0
- package/dist/database/drizzle/schema/conversations.js.map +1 -0
- package/dist/database/drizzle/schema/documents.d.ts +600 -0
- package/dist/database/drizzle/schema/documents.d.ts.map +1 -0
- package/dist/database/drizzle/schema/documents.js +100 -0
- package/dist/database/drizzle/schema/documents.js.map +1 -0
- package/dist/database/drizzle/schema/index.d.ts +3084 -0
- package/dist/database/drizzle/schema/index.d.ts.map +1 -0
- package/dist/database/drizzle/schema/index.js +46 -0
- package/dist/database/drizzle/schema/index.js.map +1 -0
- package/dist/database/drizzle/schema/memories.d.ts +435 -0
- package/dist/database/drizzle/schema/memories.d.ts.map +1 -0
- package/dist/database/drizzle/schema/memories.js +73 -0
- package/dist/database/drizzle/schema/memories.js.map +1 -0
- package/dist/database/drizzle/schema/tasks.d.ts +565 -0
- package/dist/database/drizzle/schema/tasks.d.ts.map +1 -0
- package/dist/database/drizzle/schema/tasks.js +84 -0
- package/dist/database/drizzle/schema/tasks.js.map +1 -0
- package/dist/database/health/circuit-breaker.d.ts +81 -0
- package/dist/database/health/circuit-breaker.d.ts.map +1 -0
- package/dist/database/health/circuit-breaker.js +184 -0
- package/dist/database/health/circuit-breaker.js.map +1 -0
- package/dist/database/health/health-monitor.d.ts +69 -0
- package/dist/database/health/health-monitor.d.ts.map +1 -0
- package/dist/database/health/health-monitor.js +174 -0
- package/dist/database/health/health-monitor.js.map +1 -0
- package/dist/database/health/index.d.ts +27 -0
- package/dist/database/health/index.d.ts.map +1 -0
- package/dist/database/health/index.js +23 -0
- package/dist/database/health/index.js.map +1 -0
- package/dist/database/index.d.ts +16 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +41 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/migrations/index.d.ts +34 -0
- package/dist/database/migrations/index.d.ts.map +1 -0
- package/dist/database/migrations/index.js +45 -0
- package/dist/database/migrations/index.js.map +1 -0
- package/dist/database/migrations/migrator.d.ts +77 -0
- package/dist/database/migrations/migrator.d.ts.map +1 -0
- package/dist/database/migrations/migrator.js +258 -0
- package/dist/database/migrations/migrator.js.map +1 -0
- package/dist/database/migrations/versions/001_initial.d.ts +9 -0
- package/dist/database/migrations/versions/001_initial.d.ts.map +1 -0
- package/dist/database/migrations/versions/001_initial.js +183 -0
- package/dist/database/migrations/versions/001_initial.js.map +1 -0
- package/dist/database/types.d.ts +255 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +8 -0
- package/dist/database/types.js.map +1 -0
- package/dist/database/vector/embedding-cache.d.ts +92 -0
- package/dist/database/vector/embedding-cache.d.ts.map +1 -0
- package/dist/database/vector/embedding-cache.js +185 -0
- package/dist/database/vector/embedding-cache.js.map +1 -0
- package/dist/database/vector/hnsw-index.d.ts +111 -0
- package/dist/database/vector/hnsw-index.d.ts.map +1 -0
- package/dist/database/vector/hnsw-index.js +337 -0
- package/dist/database/vector/hnsw-index.js.map +1 -0
- package/dist/database/vector/index.d.ts +75 -0
- package/dist/database/vector/index.d.ts.map +1 -0
- package/dist/database/vector/index.js +213 -0
- package/dist/database/vector/index.js.map +1 -0
- package/dist/database/vector/similarity.d.ts +67 -0
- package/dist/database/vector/similarity.d.ts.map +1 -0
- package/dist/database/vector/similarity.js +176 -0
- package/dist/database/vector/similarity.js.map +1 -0
- package/package.json +6 -3
- package/src/cli/index.ts +1 -1
- package/src/database/adapters/base-adapter.ts +171 -0
- package/src/database/adapters/index.ts +224 -0
- package/src/database/adapters/postgres-adapter.ts +285 -0
- package/src/database/adapters/sqlite-adapter.ts +420 -0
- package/src/database/cache/cache-keys.ts +150 -0
- package/src/database/cache/index.ts +44 -0
- package/src/database/cache/query-cache.ts +213 -0
- package/src/database/client.ts +166 -64
- package/src/database/db-config.ts +194 -0
- package/src/database/drizzle/index.ts +66 -0
- package/src/database/drizzle/schema/audit.ts +127 -0
- package/src/database/drizzle/schema/checkpoints.ts +164 -0
- package/src/database/drizzle/schema/conversations.ts +138 -0
- package/src/database/drizzle/schema/documents.ts +157 -0
- package/src/database/drizzle/schema/index.ts +139 -0
- package/src/database/drizzle/schema/memories.ts +127 -0
- package/src/database/drizzle/schema/tasks.ts +129 -0
- package/src/database/health/circuit-breaker.ts +214 -0
- package/src/database/health/health-monitor.ts +224 -0
- package/src/database/health/index.ts +41 -0
- package/src/database/index.ts +157 -0
- package/src/database/migrations/index.ts +52 -0
- package/src/database/migrations/migrator.ts +325 -0
- package/src/database/migrations/versions/001_initial.ts +198 -0
- package/src/database/types.ts +324 -0
- package/src/database/vector/embedding-cache.ts +234 -0
- package/src/database/vector/hnsw-index.ts +452 -0
- package/src/database/vector/index.ts +292 -0
- package/src/database/vector/similarity.ts +198 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HNSW (Hierarchical Navigable Small World) Index
|
|
3
|
+
*
|
|
4
|
+
* In-memory approximate nearest neighbor search for SQLite mode.
|
|
5
|
+
* Provides fast vector similarity search without pgvector.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cosineSimilarity, euclideanDistance } from './similarity.js';
|
|
9
|
+
import { createLogger } from '../../utils/logger.js';
|
|
10
|
+
|
|
11
|
+
const logger = createLogger('HNSWIndex');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HNSW node representing a vector in the graph
|
|
15
|
+
*/
|
|
16
|
+
interface HNSWNode {
|
|
17
|
+
id: string;
|
|
18
|
+
vector: number[];
|
|
19
|
+
neighbors: Map<number, Set<string>>; // level -> neighbor IDs
|
|
20
|
+
level: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Search result with similarity score
|
|
25
|
+
*/
|
|
26
|
+
export interface SearchResult {
|
|
27
|
+
id: string;
|
|
28
|
+
similarity: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* HNSW index configuration
|
|
33
|
+
*/
|
|
34
|
+
export interface HNSWConfig {
|
|
35
|
+
/** Vector dimension */
|
|
36
|
+
dimension: number;
|
|
37
|
+
/** Similarity metric: 'cosine' or 'euclidean' */
|
|
38
|
+
similarity: 'cosine' | 'euclidean';
|
|
39
|
+
/** Maximum connections per layer */
|
|
40
|
+
M: number;
|
|
41
|
+
/** Maximum connections for layer 0 */
|
|
42
|
+
M0: number;
|
|
43
|
+
/** Size of dynamic candidate list during construction */
|
|
44
|
+
efConstruction: number;
|
|
45
|
+
/** Level generation factor (1/ln(M)) */
|
|
46
|
+
mL: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default HNSW configuration
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_CONFIG: HNSWConfig = {
|
|
53
|
+
dimension: 1536,
|
|
54
|
+
similarity: 'cosine',
|
|
55
|
+
M: 16,
|
|
56
|
+
M0: 32,
|
|
57
|
+
efConstruction: 200,
|
|
58
|
+
mL: 1 / Math.log(16),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* HNSW Index for fast approximate nearest neighbor search
|
|
63
|
+
*/
|
|
64
|
+
export class HNSWIndex {
|
|
65
|
+
private nodes: Map<string, HNSWNode> = new Map();
|
|
66
|
+
private entryPoint: string | null = null;
|
|
67
|
+
private maxLevel: number = 0;
|
|
68
|
+
private config: HNSWConfig;
|
|
69
|
+
|
|
70
|
+
constructor(config: Partial<HNSWConfig> = {}) {
|
|
71
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get similarity function based on config
|
|
76
|
+
*/
|
|
77
|
+
private getSimilarity(): (a: number[], b: number[]) => number {
|
|
78
|
+
if (this.config.similarity === 'euclidean') {
|
|
79
|
+
// Convert distance to similarity (higher is better)
|
|
80
|
+
return (a, b) => 1 / (1 + euclideanDistance(a, b));
|
|
81
|
+
}
|
|
82
|
+
return cosineSimilarity;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate random level for new node
|
|
87
|
+
*/
|
|
88
|
+
private randomLevel(): number {
|
|
89
|
+
let level = 0;
|
|
90
|
+
while (Math.random() < this.config.mL && level < 32) {
|
|
91
|
+
level++;
|
|
92
|
+
}
|
|
93
|
+
return level;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get maximum connections for a level
|
|
98
|
+
*/
|
|
99
|
+
private getMaxConnections(level: number): number {
|
|
100
|
+
return level === 0 ? this.config.M0 : this.config.M;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Add a vector to the index
|
|
105
|
+
*/
|
|
106
|
+
add(id: string, vector: number[]): void {
|
|
107
|
+
if (vector.length !== this.config.dimension) {
|
|
108
|
+
throw new Error(`Expected dimension ${this.config.dimension}, got ${vector.length}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check if already exists
|
|
112
|
+
if (this.nodes.has(id)) {
|
|
113
|
+
// Update existing node
|
|
114
|
+
const existingNode = this.nodes.get(id)!;
|
|
115
|
+
existingNode.vector = vector;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const level = this.randomLevel();
|
|
120
|
+
const node: HNSWNode = {
|
|
121
|
+
id,
|
|
122
|
+
vector,
|
|
123
|
+
neighbors: new Map(),
|
|
124
|
+
level,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Initialize neighbor sets for each level
|
|
128
|
+
for (let l = 0; l <= level; l++) {
|
|
129
|
+
node.neighbors.set(l, new Set());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.nodes.set(id, node);
|
|
133
|
+
|
|
134
|
+
// First node becomes entry point
|
|
135
|
+
if (!this.entryPoint) {
|
|
136
|
+
this.entryPoint = id;
|
|
137
|
+
this.maxLevel = level;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const similarity = this.getSimilarity();
|
|
142
|
+
let currentNode = this.entryPoint;
|
|
143
|
+
|
|
144
|
+
// Traverse from top level down to level + 1
|
|
145
|
+
for (let l = this.maxLevel; l > level; l--) {
|
|
146
|
+
currentNode = this.searchLayer(vector, currentNode, 1, l, similarity)[0]?.id ?? currentNode;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Insert at each level from level down to 0
|
|
150
|
+
for (let l = Math.min(level, this.maxLevel); l >= 0; l--) {
|
|
151
|
+
const candidates = this.searchLayer(
|
|
152
|
+
vector,
|
|
153
|
+
currentNode,
|
|
154
|
+
this.config.efConstruction,
|
|
155
|
+
l,
|
|
156
|
+
similarity
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const neighbors = this.selectNeighbors(candidates, this.getMaxConnections(l));
|
|
160
|
+
|
|
161
|
+
// Connect new node to neighbors
|
|
162
|
+
for (const neighbor of neighbors) {
|
|
163
|
+
node.neighbors.get(l)!.add(neighbor.id);
|
|
164
|
+
|
|
165
|
+
// Connect neighbors back to new node (bidirectional)
|
|
166
|
+
const neighborNode = this.nodes.get(neighbor.id);
|
|
167
|
+
if (neighborNode) {
|
|
168
|
+
neighborNode.neighbors.get(l)?.add(id);
|
|
169
|
+
|
|
170
|
+
// Prune if too many connections
|
|
171
|
+
const maxConn = this.getMaxConnections(l);
|
|
172
|
+
if ((neighborNode.neighbors.get(l)?.size ?? 0) > maxConn) {
|
|
173
|
+
this.pruneConnections(neighborNode, l, maxConn, similarity);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (candidates.length > 0) {
|
|
179
|
+
currentNode = candidates[0]!.id;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Update entry point if new node has higher level
|
|
184
|
+
if (level > this.maxLevel) {
|
|
185
|
+
this.entryPoint = id;
|
|
186
|
+
this.maxLevel = level;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Search for k nearest neighbors
|
|
192
|
+
*/
|
|
193
|
+
search(query: number[], k: number, ef?: number): SearchResult[] {
|
|
194
|
+
if (query.length !== this.config.dimension) {
|
|
195
|
+
throw new Error(`Expected dimension ${this.config.dimension}, got ${query.length}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!this.entryPoint) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const effectiveEf = ef ?? Math.max(k, 100);
|
|
203
|
+
const similarity = this.getSimilarity();
|
|
204
|
+
let currentNode = this.entryPoint;
|
|
205
|
+
|
|
206
|
+
// Traverse from top level down to level 1
|
|
207
|
+
for (let l = this.maxLevel; l > 0; l--) {
|
|
208
|
+
currentNode = this.searchLayer(query, currentNode, 1, l, similarity)[0]?.id ?? currentNode;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Search at level 0 with ef candidates
|
|
212
|
+
const candidates = this.searchLayer(query, currentNode, effectiveEf, 0, similarity);
|
|
213
|
+
|
|
214
|
+
// Return top k
|
|
215
|
+
return candidates.slice(0, k);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Search within a layer
|
|
220
|
+
*/
|
|
221
|
+
private searchLayer(
|
|
222
|
+
query: number[],
|
|
223
|
+
entryId: string,
|
|
224
|
+
ef: number,
|
|
225
|
+
level: number,
|
|
226
|
+
similarity: (a: number[], b: number[]) => number
|
|
227
|
+
): SearchResult[] {
|
|
228
|
+
const entryNode = this.nodes.get(entryId);
|
|
229
|
+
if (!entryNode) return [];
|
|
230
|
+
|
|
231
|
+
const visited = new Set<string>([entryId]);
|
|
232
|
+
const candidates: SearchResult[] = [
|
|
233
|
+
{ id: entryId, similarity: similarity(query, entryNode.vector) },
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
// Priority queue (sorted by similarity descending)
|
|
237
|
+
const toExplore: SearchResult[] = [...candidates];
|
|
238
|
+
|
|
239
|
+
while (toExplore.length > 0) {
|
|
240
|
+
// Get best candidate
|
|
241
|
+
toExplore.sort((a, b) => b.similarity - a.similarity);
|
|
242
|
+
const current = toExplore.shift()!;
|
|
243
|
+
|
|
244
|
+
// Get worst in results
|
|
245
|
+
candidates.sort((a, b) => b.similarity - a.similarity);
|
|
246
|
+
const worstSimilarity = candidates.length >= ef
|
|
247
|
+
? candidates[candidates.length - 1]!.similarity
|
|
248
|
+
: -Infinity;
|
|
249
|
+
|
|
250
|
+
// Stop if current is worse than worst result
|
|
251
|
+
if (current.similarity < worstSimilarity) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Explore neighbors
|
|
256
|
+
const currentNode = this.nodes.get(current.id);
|
|
257
|
+
const neighbors = currentNode?.neighbors.get(level) ?? new Set();
|
|
258
|
+
|
|
259
|
+
for (const neighborId of neighbors) {
|
|
260
|
+
if (visited.has(neighborId)) continue;
|
|
261
|
+
visited.add(neighborId);
|
|
262
|
+
|
|
263
|
+
const neighborNode = this.nodes.get(neighborId);
|
|
264
|
+
if (!neighborNode) continue;
|
|
265
|
+
|
|
266
|
+
const sim = similarity(query, neighborNode.vector);
|
|
267
|
+
const result = { id: neighborId, similarity: sim };
|
|
268
|
+
|
|
269
|
+
if (candidates.length < ef || sim > worstSimilarity) {
|
|
270
|
+
candidates.push(result);
|
|
271
|
+
candidates.sort((a, b) => b.similarity - a.similarity);
|
|
272
|
+
if (candidates.length > ef) {
|
|
273
|
+
candidates.pop();
|
|
274
|
+
}
|
|
275
|
+
toExplore.push(result);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return candidates;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Select best neighbors from candidates
|
|
285
|
+
*/
|
|
286
|
+
private selectNeighbors(candidates: SearchResult[], maxNeighbors: number): SearchResult[] {
|
|
287
|
+
// Simple approach: take top maxNeighbors by similarity
|
|
288
|
+
return candidates
|
|
289
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
290
|
+
.slice(0, maxNeighbors);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Prune connections to maintain max connections
|
|
295
|
+
*/
|
|
296
|
+
private pruneConnections(
|
|
297
|
+
node: HNSWNode,
|
|
298
|
+
level: number,
|
|
299
|
+
maxConnections: number,
|
|
300
|
+
similarity: (a: number[], b: number[]) => number
|
|
301
|
+
): void {
|
|
302
|
+
const neighbors = node.neighbors.get(level);
|
|
303
|
+
if (!neighbors || neighbors.size <= maxConnections) return;
|
|
304
|
+
|
|
305
|
+
// Calculate similarities and keep best
|
|
306
|
+
const scored: SearchResult[] = [];
|
|
307
|
+
for (const neighborId of neighbors) {
|
|
308
|
+
const neighborNode = this.nodes.get(neighborId);
|
|
309
|
+
if (neighborNode) {
|
|
310
|
+
scored.push({
|
|
311
|
+
id: neighborId,
|
|
312
|
+
similarity: similarity(node.vector, neighborNode.vector),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
scored.sort((a, b) => b.similarity - a.similarity);
|
|
318
|
+
const keep = new Set(scored.slice(0, maxConnections).map((s) => s.id));
|
|
319
|
+
|
|
320
|
+
// Remove excess connections
|
|
321
|
+
for (const neighborId of neighbors) {
|
|
322
|
+
if (!keep.has(neighborId)) {
|
|
323
|
+
neighbors.delete(neighborId);
|
|
324
|
+
// Also remove back-link
|
|
325
|
+
this.nodes.get(neighborId)?.neighbors.get(level)?.delete(node.id);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Remove a vector from the index
|
|
332
|
+
*/
|
|
333
|
+
remove(id: string): boolean {
|
|
334
|
+
const node = this.nodes.get(id);
|
|
335
|
+
if (!node) return false;
|
|
336
|
+
|
|
337
|
+
// Remove from all neighbors
|
|
338
|
+
for (const [level, neighbors] of node.neighbors) {
|
|
339
|
+
for (const neighborId of neighbors) {
|
|
340
|
+
this.nodes.get(neighborId)?.neighbors.get(level)?.delete(id);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.nodes.delete(id);
|
|
345
|
+
|
|
346
|
+
// Update entry point if needed
|
|
347
|
+
if (this.entryPoint === id) {
|
|
348
|
+
if (this.nodes.size === 0) {
|
|
349
|
+
this.entryPoint = null;
|
|
350
|
+
this.maxLevel = 0;
|
|
351
|
+
} else {
|
|
352
|
+
// Find new entry point with highest level
|
|
353
|
+
let maxLvl = 0;
|
|
354
|
+
let newEntry: string | null = null;
|
|
355
|
+
for (const [nodeId, n] of this.nodes) {
|
|
356
|
+
if (n.level >= maxLvl) {
|
|
357
|
+
maxLvl = n.level;
|
|
358
|
+
newEntry = nodeId;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
this.entryPoint = newEntry;
|
|
362
|
+
this.maxLevel = maxLvl;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Check if an ID exists in the index
|
|
371
|
+
*/
|
|
372
|
+
has(id: string): boolean {
|
|
373
|
+
return this.nodes.has(id);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get vector by ID
|
|
378
|
+
*/
|
|
379
|
+
get(id: string): number[] | undefined {
|
|
380
|
+
return this.nodes.get(id)?.vector;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get index size
|
|
385
|
+
*/
|
|
386
|
+
get size(): number {
|
|
387
|
+
return this.nodes.size;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get index statistics
|
|
392
|
+
*/
|
|
393
|
+
getStats(): {
|
|
394
|
+
size: number;
|
|
395
|
+
dimension: number;
|
|
396
|
+
maxLevel: number;
|
|
397
|
+
avgConnections: number;
|
|
398
|
+
} {
|
|
399
|
+
let totalConnections = 0;
|
|
400
|
+
let connectionCount = 0;
|
|
401
|
+
|
|
402
|
+
for (const node of this.nodes.values()) {
|
|
403
|
+
for (const neighbors of node.neighbors.values()) {
|
|
404
|
+
totalConnections += neighbors.size;
|
|
405
|
+
connectionCount++;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
size: this.nodes.size,
|
|
411
|
+
dimension: this.config.dimension,
|
|
412
|
+
maxLevel: this.maxLevel,
|
|
413
|
+
avgConnections: connectionCount > 0 ? totalConnections / connectionCount : 0,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Clear the index
|
|
419
|
+
*/
|
|
420
|
+
clear(): void {
|
|
421
|
+
this.nodes.clear();
|
|
422
|
+
this.entryPoint = null;
|
|
423
|
+
this.maxLevel = 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get all IDs in the index
|
|
428
|
+
*/
|
|
429
|
+
getAllIds(): string[] {
|
|
430
|
+
return Array.from(this.nodes.keys());
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Rebuild index from scratch (for optimization)
|
|
435
|
+
*/
|
|
436
|
+
rebuild(): void {
|
|
437
|
+
const vectors = Array.from(this.nodes.entries()).map(([id, node]) => ({
|
|
438
|
+
id,
|
|
439
|
+
vector: node.vector,
|
|
440
|
+
}));
|
|
441
|
+
|
|
442
|
+
this.clear();
|
|
443
|
+
|
|
444
|
+
for (const { id, vector } of vectors) {
|
|
445
|
+
this.add(id, vector);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
logger.debug('Index rebuilt', { size: this.size });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export default HNSWIndex;
|