verso-db 0.1.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/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/BinaryHeap.d.ts +25 -0
- package/dist/BinaryHeap.d.ts.map +1 -0
- package/dist/Collection.d.ts +156 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/HNSWIndex.d.ts +357 -0
- package/dist/HNSWIndex.d.ts.map +1 -0
- package/dist/MaxBinaryHeap.d.ts +63 -0
- package/dist/MaxBinaryHeap.d.ts.map +1 -0
- package/dist/Storage.d.ts +54 -0
- package/dist/Storage.d.ts.map +1 -0
- package/dist/VectorDB.d.ts +44 -0
- package/dist/VectorDB.d.ts.map +1 -0
- package/dist/backends/DistanceBackend.d.ts +5 -0
- package/dist/backends/DistanceBackend.d.ts.map +1 -0
- package/dist/backends/JsDistanceBackend.d.ts +37 -0
- package/dist/backends/JsDistanceBackend.d.ts.map +1 -0
- package/dist/encoding/DeltaEncoder.d.ts +61 -0
- package/dist/encoding/DeltaEncoder.d.ts.map +1 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3732 -0
- package/dist/presets.d.ts +91 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/quantization/ScalarQuantizer.d.ts +114 -0
- package/dist/quantization/ScalarQuantizer.d.ts.map +1 -0
- package/dist/storage/BatchWriter.d.ts +104 -0
- package/dist/storage/BatchWriter.d.ts.map +1 -0
- package/dist/storage/BunStorageBackend.d.ts +58 -0
- package/dist/storage/BunStorageBackend.d.ts.map +1 -0
- package/dist/storage/MemoryBackend.d.ts +44 -0
- package/dist/storage/MemoryBackend.d.ts.map +1 -0
- package/dist/storage/OPFSBackend.d.ts +59 -0
- package/dist/storage/OPFSBackend.d.ts.map +1 -0
- package/dist/storage/StorageBackend.d.ts +66 -0
- package/dist/storage/StorageBackend.d.ts.map +1 -0
- package/dist/storage/WriteAheadLog.d.ts +111 -0
- package/dist/storage/WriteAheadLog.d.ts.map +1 -0
- package/dist/storage/createStorageBackend.d.ts +40 -0
- package/dist/storage/createStorageBackend.d.ts.map +1 -0
- package/dist/storage/index.d.ts +30 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/package.json +98 -0
- package/src/BinaryHeap.ts +131 -0
- package/src/Collection.ts +695 -0
- package/src/HNSWIndex.ts +1839 -0
- package/src/MaxBinaryHeap.ts +175 -0
- package/src/Storage.ts +435 -0
- package/src/VectorDB.ts +109 -0
- package/src/backends/DistanceBackend.ts +17 -0
- package/src/backends/JsDistanceBackend.ts +227 -0
- package/src/encoding/DeltaEncoder.ts +217 -0
- package/src/errors.ts +110 -0
- package/src/index.ts +138 -0
- package/src/presets.ts +229 -0
- package/src/quantization/ScalarQuantizer.ts +383 -0
- package/src/storage/BatchWriter.ts +336 -0
- package/src/storage/BunStorageBackend.ts +161 -0
- package/src/storage/MemoryBackend.ts +120 -0
- package/src/storage/OPFSBackend.ts +250 -0
- package/src/storage/StorageBackend.ts +74 -0
- package/src/storage/WriteAheadLog.ts +326 -0
- package/src/storage/createStorageBackend.ts +137 -0
- package/src/storage/index.ts +53 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface DistanceBackend {
|
|
2
|
+
batchL2(
|
|
3
|
+
base: Float32Array,
|
|
4
|
+
dim: number,
|
|
5
|
+
ids: Uint32Array,
|
|
6
|
+
query: Float32Array,
|
|
7
|
+
out: Float32Array
|
|
8
|
+
): void;
|
|
9
|
+
|
|
10
|
+
batchDot(
|
|
11
|
+
base: Float32Array,
|
|
12
|
+
dim: number,
|
|
13
|
+
ids: Uint32Array,
|
|
14
|
+
query: Float32Array,
|
|
15
|
+
out: Float32Array
|
|
16
|
+
): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { DistanceBackend } from './DistanceBackend';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optimized JavaScript distance backend with SIMD-style 4-wide unrolling.
|
|
5
|
+
* This provides ~1.5-2x speedup over naive loops by reducing loop overhead
|
|
6
|
+
* and enabling better CPU pipelining.
|
|
7
|
+
*/
|
|
8
|
+
export class JsDistanceBackend implements DistanceBackend {
|
|
9
|
+
batchL2(base: Float32Array, dim: number, ids: Uint32Array, query: Float32Array, out: Float32Array): void {
|
|
10
|
+
const len = ids.length;
|
|
11
|
+
for (let i = 0; i < len; i++) {
|
|
12
|
+
const off = ids[i] * dim;
|
|
13
|
+
out[i] = this.l2SquaredUnrolled(base, off, query, dim);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
batchDot(base: Float32Array, dim: number, ids: Uint32Array, query: Float32Array, out: Float32Array): void {
|
|
18
|
+
const len = ids.length;
|
|
19
|
+
for (let i = 0; i < len; i++) {
|
|
20
|
+
const off = ids[i] * dim;
|
|
21
|
+
out[i] = this.dotProductUnrolled(base, off, query, dim);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compute dot product with 4-wide manual unrolling for better performance.
|
|
27
|
+
* Uses separate accumulators to enable instruction-level parallelism.
|
|
28
|
+
*/
|
|
29
|
+
private dotProductUnrolled(base: Float32Array, offset: number, query: Float32Array, dim: number): number {
|
|
30
|
+
let sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0;
|
|
31
|
+
let d = 0;
|
|
32
|
+
|
|
33
|
+
// Process 4 elements at a time
|
|
34
|
+
const limit = dim - 3;
|
|
35
|
+
for (; d < limit; d += 4) {
|
|
36
|
+
sum0 += base[offset + d] * query[d];
|
|
37
|
+
sum1 += base[offset + d + 1] * query[d + 1];
|
|
38
|
+
sum2 += base[offset + d + 2] * query[d + 2];
|
|
39
|
+
sum3 += base[offset + d + 3] * query[d + 3];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Handle remaining elements
|
|
43
|
+
for (; d < dim; d++) {
|
|
44
|
+
sum0 += base[offset + d] * query[d];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return sum0 + sum1 + sum2 + sum3;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compute L2 squared distance with 4-wide manual unrolling.
|
|
52
|
+
*/
|
|
53
|
+
private l2SquaredUnrolled(base: Float32Array, offset: number, query: Float32Array, dim: number): number {
|
|
54
|
+
let sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0;
|
|
55
|
+
let d = 0;
|
|
56
|
+
|
|
57
|
+
// Process 4 elements at a time
|
|
58
|
+
const limit = dim - 3;
|
|
59
|
+
for (; d < limit; d += 4) {
|
|
60
|
+
const diff0 = base[offset + d] - query[d];
|
|
61
|
+
const diff1 = base[offset + d + 1] - query[d + 1];
|
|
62
|
+
const diff2 = base[offset + d + 2] - query[d + 2];
|
|
63
|
+
const diff3 = base[offset + d + 3] - query[d + 3];
|
|
64
|
+
sum0 += diff0 * diff0;
|
|
65
|
+
sum1 += diff1 * diff1;
|
|
66
|
+
sum2 += diff2 * diff2;
|
|
67
|
+
sum3 += diff3 * diff3;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle remaining elements
|
|
71
|
+
for (; d < dim; d++) {
|
|
72
|
+
const diff = base[offset + d] - query[d];
|
|
73
|
+
sum0 += diff * diff;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return sum0 + sum1 + sum2 + sum3;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fast inline distance functions for single-vector comparisons.
|
|
82
|
+
* These avoid the batch interface overhead for point-to-point distance calculations.
|
|
83
|
+
*/
|
|
84
|
+
export function dotProductFast(a: Float32Array, b: Float32Array): number {
|
|
85
|
+
const len = a.length;
|
|
86
|
+
// Use 8 accumulators for better ILP (instruction-level parallelism)
|
|
87
|
+
let sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0;
|
|
88
|
+
let sum4 = 0, sum5 = 0, sum6 = 0, sum7 = 0;
|
|
89
|
+
let i = 0;
|
|
90
|
+
|
|
91
|
+
// 8-wide unrolling for high-dimensional vectors (768D, 1536D)
|
|
92
|
+
const limit8 = len - 7;
|
|
93
|
+
for (; i < limit8; i += 8) {
|
|
94
|
+
sum0 += a[i] * b[i];
|
|
95
|
+
sum1 += a[i + 1] * b[i + 1];
|
|
96
|
+
sum2 += a[i + 2] * b[i + 2];
|
|
97
|
+
sum3 += a[i + 3] * b[i + 3];
|
|
98
|
+
sum4 += a[i + 4] * b[i + 4];
|
|
99
|
+
sum5 += a[i + 5] * b[i + 5];
|
|
100
|
+
sum6 += a[i + 6] * b[i + 6];
|
|
101
|
+
sum7 += a[i + 7] * b[i + 7];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle remaining elements
|
|
105
|
+
for (; i < len; i++) {
|
|
106
|
+
sum0 += a[i] * b[i];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return sum0 + sum1 + sum2 + sum3 + sum4 + sum5 + sum6 + sum7;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function l2SquaredFast(a: Float32Array, b: Float32Array): number {
|
|
113
|
+
const len = a.length;
|
|
114
|
+
// Use 8 accumulators for better ILP
|
|
115
|
+
let sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0;
|
|
116
|
+
let sum4 = 0, sum5 = 0, sum6 = 0, sum7 = 0;
|
|
117
|
+
let i = 0;
|
|
118
|
+
|
|
119
|
+
// 8-wide unrolling for high-dimensional vectors
|
|
120
|
+
const limit8 = len - 7;
|
|
121
|
+
for (; i < limit8; i += 8) {
|
|
122
|
+
const d0 = a[i] - b[i];
|
|
123
|
+
const d1 = a[i + 1] - b[i + 1];
|
|
124
|
+
const d2 = a[i + 2] - b[i + 2];
|
|
125
|
+
const d3 = a[i + 3] - b[i + 3];
|
|
126
|
+
const d4 = a[i + 4] - b[i + 4];
|
|
127
|
+
const d5 = a[i + 5] - b[i + 5];
|
|
128
|
+
const d6 = a[i + 6] - b[i + 6];
|
|
129
|
+
const d7 = a[i + 7] - b[i + 7];
|
|
130
|
+
sum0 += d0 * d0;
|
|
131
|
+
sum1 += d1 * d1;
|
|
132
|
+
sum2 += d2 * d2;
|
|
133
|
+
sum3 += d3 * d3;
|
|
134
|
+
sum4 += d4 * d4;
|
|
135
|
+
sum5 += d5 * d5;
|
|
136
|
+
sum6 += d6 * d6;
|
|
137
|
+
sum7 += d7 * d7;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle remaining elements
|
|
141
|
+
for (; i < len; i++) {
|
|
142
|
+
const d = a[i] - b[i];
|
|
143
|
+
sum0 += d * d;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return sum0 + sum1 + sum2 + sum3 + sum4 + sum5 + sum6 + sum7;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Normalize a vector in place.
|
|
151
|
+
* Uses 8-wide unrolling for better ILP.
|
|
152
|
+
*/
|
|
153
|
+
export function normalizeInPlace(v: Float32Array): number {
|
|
154
|
+
const len = v.length;
|
|
155
|
+
let s0 = 0, s1 = 0, s2 = 0, s3 = 0;
|
|
156
|
+
let s4 = 0, s5 = 0, s6 = 0, s7 = 0;
|
|
157
|
+
let i = 0;
|
|
158
|
+
const limit8 = len - 7;
|
|
159
|
+
|
|
160
|
+
for (; i < limit8; i += 8) {
|
|
161
|
+
s0 += v[i] * v[i];
|
|
162
|
+
s1 += v[i + 1] * v[i + 1];
|
|
163
|
+
s2 += v[i + 2] * v[i + 2];
|
|
164
|
+
s3 += v[i + 3] * v[i + 3];
|
|
165
|
+
s4 += v[i + 4] * v[i + 4];
|
|
166
|
+
s5 += v[i + 5] * v[i + 5];
|
|
167
|
+
s6 += v[i + 6] * v[i + 6];
|
|
168
|
+
s7 += v[i + 7] * v[i + 7];
|
|
169
|
+
}
|
|
170
|
+
for (; i < len; i++) {
|
|
171
|
+
s0 += v[i] * v[i];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const norm = Math.sqrt(s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7);
|
|
175
|
+
if (norm > 0) {
|
|
176
|
+
const invNorm = 1 / norm;
|
|
177
|
+
for (let j = 0; j < len; j++) {
|
|
178
|
+
v[j] *= invNorm;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return norm;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Compute cosine distance between two vectors.
|
|
186
|
+
* For pre-normalized vectors, this is simply 1 - dot(a, b).
|
|
187
|
+
* Uses 8-wide unrolling for norm computation.
|
|
188
|
+
*/
|
|
189
|
+
export function cosineDistanceFast(a: Float32Array, b: Float32Array, aIsNormalized = false, bIsNormalized = false): number {
|
|
190
|
+
const dot = dotProductFast(a, b);
|
|
191
|
+
|
|
192
|
+
if (aIsNormalized && bIsNormalized) {
|
|
193
|
+
// Both vectors are normalized, cosine distance = 1 - dot
|
|
194
|
+
const dist = 1 - dot;
|
|
195
|
+
return dist < 1e-10 ? 0 : dist;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Need to compute norms with 8-wide unrolling
|
|
199
|
+
const len = a.length;
|
|
200
|
+
let nA0 = 0, nA1 = 0, nA2 = 0, nA3 = 0;
|
|
201
|
+
let nB0 = 0, nB1 = 0, nB2 = 0, nB3 = 0;
|
|
202
|
+
let i = 0;
|
|
203
|
+
const limit8 = len - 7;
|
|
204
|
+
|
|
205
|
+
for (; i < limit8; i += 8) {
|
|
206
|
+
nA0 += a[i] * a[i] + a[i + 4] * a[i + 4];
|
|
207
|
+
nA1 += a[i + 1] * a[i + 1] + a[i + 5] * a[i + 5];
|
|
208
|
+
nA2 += a[i + 2] * a[i + 2] + a[i + 6] * a[i + 6];
|
|
209
|
+
nA3 += a[i + 3] * a[i + 3] + a[i + 7] * a[i + 7];
|
|
210
|
+
nB0 += b[i] * b[i] + b[i + 4] * b[i + 4];
|
|
211
|
+
nB1 += b[i + 1] * b[i + 1] + b[i + 5] * b[i + 5];
|
|
212
|
+
nB2 += b[i + 2] * b[i + 2] + b[i + 6] * b[i + 6];
|
|
213
|
+
nB3 += b[i + 3] * b[i + 3] + b[i + 7] * b[i + 7];
|
|
214
|
+
}
|
|
215
|
+
for (; i < len; i++) {
|
|
216
|
+
nA0 += a[i] * a[i];
|
|
217
|
+
nB0 += b[i] * b[i];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const normA = nA0 + nA1 + nA2 + nA3;
|
|
221
|
+
const normB = nB0 + nB1 + nB2 + nB3;
|
|
222
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
223
|
+
if (magnitude === 0) return 1;
|
|
224
|
+
|
|
225
|
+
const dist = 1 - (dot / magnitude);
|
|
226
|
+
return dist < 1e-10 ? 0 : dist;
|
|
227
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delta Encoding with Varint for Neighbor Lists
|
|
3
|
+
*
|
|
4
|
+
* Implements delta-encoded neighbor lists as used by Qdrant for ~38% storage reduction.
|
|
5
|
+
* Neighbor IDs are sorted, then stored as deltas with variable-length encoding.
|
|
6
|
+
*
|
|
7
|
+
* Format:
|
|
8
|
+
* - First ID stored as full uint32
|
|
9
|
+
* - Subsequent IDs stored as varint deltas from previous ID
|
|
10
|
+
*
|
|
11
|
+
* Varint encoding (like Protocol Buffers):
|
|
12
|
+
* - Values 0-127: 1 byte
|
|
13
|
+
* - Values 128-16383: 2 bytes
|
|
14
|
+
* - Values 16384-2097151: 3 bytes
|
|
15
|
+
* - Values 2097152-268435455: 4 bytes
|
|
16
|
+
* - Larger: 5 bytes
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Encode an unsigned integer as a varint
|
|
21
|
+
* Returns the number of bytes written
|
|
22
|
+
*/
|
|
23
|
+
export function encodeVarint(value: number, buffer: Uint8Array, offset: number): number {
|
|
24
|
+
let v = value >>> 0; // Ensure unsigned
|
|
25
|
+
let bytesWritten = 0;
|
|
26
|
+
|
|
27
|
+
while (v >= 0x80) {
|
|
28
|
+
buffer[offset + bytesWritten] = (v & 0x7f) | 0x80;
|
|
29
|
+
v >>>= 7;
|
|
30
|
+
bytesWritten++;
|
|
31
|
+
}
|
|
32
|
+
buffer[offset + bytesWritten] = v;
|
|
33
|
+
return bytesWritten + 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decode a varint from buffer
|
|
38
|
+
* Returns [value, bytesRead]
|
|
39
|
+
*/
|
|
40
|
+
export function decodeVarint(buffer: Uint8Array, offset: number): [number, number] {
|
|
41
|
+
let result = 0;
|
|
42
|
+
let shift = 0;
|
|
43
|
+
let bytesRead = 0;
|
|
44
|
+
|
|
45
|
+
while (offset + bytesRead < buffer.length) {
|
|
46
|
+
const byte = buffer[offset + bytesRead];
|
|
47
|
+
result |= (byte & 0x7f) << shift;
|
|
48
|
+
bytesRead++;
|
|
49
|
+
|
|
50
|
+
if ((byte & 0x80) === 0) {
|
|
51
|
+
return [result >>> 0, bytesRead];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
shift += 7;
|
|
55
|
+
if (shift > 35) {
|
|
56
|
+
throw new Error('Varint too long');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error('Unexpected end of buffer');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Calculate the number of bytes needed to encode a varint
|
|
65
|
+
*/
|
|
66
|
+
export function varintSize(value: number): number {
|
|
67
|
+
let v = value >>> 0;
|
|
68
|
+
let size = 1;
|
|
69
|
+
while (v >= 0x80) {
|
|
70
|
+
v >>>= 7;
|
|
71
|
+
size++;
|
|
72
|
+
}
|
|
73
|
+
return size;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Delta-encode a sorted array of neighbor IDs
|
|
78
|
+
* Returns the encoded buffer
|
|
79
|
+
*/
|
|
80
|
+
export function deltaEncodeNeighbors(neighbors: number[]): Uint8Array {
|
|
81
|
+
if (neighbors.length === 0) {
|
|
82
|
+
return new Uint8Array(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Sort neighbors for optimal delta encoding
|
|
86
|
+
const sorted = neighbors.slice().sort((a, b) => a - b);
|
|
87
|
+
|
|
88
|
+
// Calculate required buffer size
|
|
89
|
+
let size = 4; // First ID as uint32
|
|
90
|
+
let prev = sorted[0];
|
|
91
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
92
|
+
const delta = sorted[i] - prev;
|
|
93
|
+
size += varintSize(delta);
|
|
94
|
+
prev = sorted[i];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Encode
|
|
98
|
+
const buffer = new Uint8Array(size);
|
|
99
|
+
const view = new DataView(buffer.buffer);
|
|
100
|
+
|
|
101
|
+
// First ID as full uint32 (little-endian)
|
|
102
|
+
view.setUint32(0, sorted[0], true);
|
|
103
|
+
let offset = 4;
|
|
104
|
+
|
|
105
|
+
// Remaining as deltas
|
|
106
|
+
prev = sorted[0];
|
|
107
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
108
|
+
const delta = sorted[i] - prev;
|
|
109
|
+
offset += encodeVarint(delta, buffer, offset);
|
|
110
|
+
prev = sorted[i];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return buffer;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Decode a delta-encoded neighbor list
|
|
118
|
+
* Returns the original neighbor IDs (sorted)
|
|
119
|
+
*/
|
|
120
|
+
export function deltaDecodeNeighbors(buffer: Uint8Array, count: number): number[] {
|
|
121
|
+
if (count === 0 || buffer.length === 0) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
126
|
+
const neighbors = new Array<number>(count);
|
|
127
|
+
|
|
128
|
+
// First ID as full uint32
|
|
129
|
+
neighbors[0] = view.getUint32(0, true);
|
|
130
|
+
let offset = 4;
|
|
131
|
+
|
|
132
|
+
// Remaining as deltas
|
|
133
|
+
for (let i = 1; i < count; i++) {
|
|
134
|
+
const [delta, bytesRead] = decodeVarint(buffer, offset);
|
|
135
|
+
neighbors[i] = neighbors[i - 1] + delta;
|
|
136
|
+
offset += bytesRead;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return neighbors;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Calculate the encoded size for a neighbor list without actually encoding
|
|
144
|
+
* Useful for calculating total buffer size before serialization
|
|
145
|
+
*/
|
|
146
|
+
export function deltaEncodedSize(neighbors: number[]): number {
|
|
147
|
+
if (neighbors.length === 0) {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const sorted = neighbors.slice().sort((a, b) => a - b);
|
|
152
|
+
|
|
153
|
+
let size = 4; // First ID as uint32
|
|
154
|
+
let prev = sorted[0];
|
|
155
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
156
|
+
const delta = sorted[i] - prev;
|
|
157
|
+
size += varintSize(delta);
|
|
158
|
+
prev = sorted[i];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return size;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Batch encode multiple neighbor lists efficiently
|
|
166
|
+
* Returns a single buffer with all encoded lists concatenated
|
|
167
|
+
* Also returns offsets for each list
|
|
168
|
+
*/
|
|
169
|
+
export function deltaEncodeBatch(neighborLists: number[][]): {
|
|
170
|
+
buffer: Uint8Array;
|
|
171
|
+
offsets: number[];
|
|
172
|
+
sizes: number[];
|
|
173
|
+
} {
|
|
174
|
+
// Calculate total size and individual sizes
|
|
175
|
+
const sizes = neighborLists.map(list => deltaEncodedSize(list));
|
|
176
|
+
const totalSize = sizes.reduce((a, b) => a + b, 0);
|
|
177
|
+
|
|
178
|
+
// Allocate single buffer
|
|
179
|
+
const buffer = new Uint8Array(totalSize);
|
|
180
|
+
const offsets: number[] = [];
|
|
181
|
+
|
|
182
|
+
let currentOffset = 0;
|
|
183
|
+
for (let i = 0; i < neighborLists.length; i++) {
|
|
184
|
+
offsets.push(currentOffset);
|
|
185
|
+
|
|
186
|
+
if (neighborLists[i].length > 0) {
|
|
187
|
+
const encoded = deltaEncodeNeighbors(neighborLists[i]);
|
|
188
|
+
buffer.set(encoded, currentOffset);
|
|
189
|
+
currentOffset += encoded.length;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { buffer, offsets, sizes };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Decode a batch of neighbor lists from a single buffer
|
|
198
|
+
*/
|
|
199
|
+
export function deltaDecodeBatch(
|
|
200
|
+
buffer: Uint8Array,
|
|
201
|
+
offsets: number[],
|
|
202
|
+
sizes: number[],
|
|
203
|
+
counts: number[]
|
|
204
|
+
): number[][] {
|
|
205
|
+
const results: number[][] = [];
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < offsets.length; i++) {
|
|
208
|
+
if (counts[i] === 0 || sizes[i] === 0) {
|
|
209
|
+
results.push([]);
|
|
210
|
+
} else {
|
|
211
|
+
const slice = buffer.subarray(offsets[i], offsets[i] + sizes[i]);
|
|
212
|
+
results.push(deltaDecodeNeighbors(slice, counts[i]));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return results;
|
|
217
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all VectorDB errors
|
|
3
|
+
*/
|
|
4
|
+
export class VectorDBError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
|
|
7
|
+
constructor(message: string, code: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'VectorDBError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Thrown when vector dimensions don't match expected dimensions
|
|
16
|
+
*/
|
|
17
|
+
export class DimensionMismatchError extends VectorDBError {
|
|
18
|
+
readonly expected: number;
|
|
19
|
+
readonly actual: number;
|
|
20
|
+
|
|
21
|
+
constructor(expected: number, actual: number, context?: string) {
|
|
22
|
+
const message = context
|
|
23
|
+
? `${context}: expected dimension ${expected}, got ${actual}`
|
|
24
|
+
: `Dimension mismatch: expected ${expected}, got ${actual}`;
|
|
25
|
+
super(message, 'DIMENSION_MISMATCH');
|
|
26
|
+
this.name = 'DimensionMismatchError';
|
|
27
|
+
this.expected = expected;
|
|
28
|
+
this.actual = actual;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Thrown when attempting to add a vector with an ID that already exists
|
|
34
|
+
*/
|
|
35
|
+
export class DuplicateVectorError extends VectorDBError {
|
|
36
|
+
readonly ids: string[];
|
|
37
|
+
|
|
38
|
+
constructor(ids: string[]) {
|
|
39
|
+
const message = ids.length === 1
|
|
40
|
+
? `Vector with ID '${ids[0]}' already exists`
|
|
41
|
+
: `Vectors with IDs already exist: ${ids.join(', ')}`;
|
|
42
|
+
super(message, 'DUPLICATE_VECTOR');
|
|
43
|
+
this.name = 'DuplicateVectorError';
|
|
44
|
+
this.ids = ids;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Thrown when a requested collection does not exist
|
|
50
|
+
*/
|
|
51
|
+
export class CollectionNotFoundError extends VectorDBError {
|
|
52
|
+
readonly collectionName: string;
|
|
53
|
+
|
|
54
|
+
constructor(collectionName: string) {
|
|
55
|
+
super(`Collection '${collectionName}' does not exist`, 'COLLECTION_NOT_FOUND');
|
|
56
|
+
this.name = 'CollectionNotFoundError';
|
|
57
|
+
this.collectionName = collectionName;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Thrown when attempting to create a collection that already exists
|
|
63
|
+
*/
|
|
64
|
+
export class CollectionExistsError extends VectorDBError {
|
|
65
|
+
readonly collectionName: string;
|
|
66
|
+
|
|
67
|
+
constructor(collectionName: string) {
|
|
68
|
+
super(`Collection '${collectionName}' already exists`, 'COLLECTION_EXISTS');
|
|
69
|
+
this.name = 'CollectionExistsError';
|
|
70
|
+
this.collectionName = collectionName;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Thrown when a storage operation fails
|
|
76
|
+
*/
|
|
77
|
+
export class StorageError extends VectorDBError {
|
|
78
|
+
readonly operation: string;
|
|
79
|
+
readonly path?: string;
|
|
80
|
+
|
|
81
|
+
constructor(operation: string, message: string, path?: string) {
|
|
82
|
+
super(`Storage ${operation} failed: ${message}`, 'STORAGE_ERROR');
|
|
83
|
+
this.name = 'StorageError';
|
|
84
|
+
this.operation = operation;
|
|
85
|
+
this.path = path;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Thrown when quantization operations fail
|
|
91
|
+
*/
|
|
92
|
+
export class QuantizationError extends VectorDBError {
|
|
93
|
+
constructor(message: string) {
|
|
94
|
+
super(message, 'QUANTIZATION_ERROR');
|
|
95
|
+
this.name = 'QuantizationError';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Thrown when a vector is not found in the index
|
|
101
|
+
*/
|
|
102
|
+
export class VectorNotFoundError extends VectorDBError {
|
|
103
|
+
readonly vectorId: string | number;
|
|
104
|
+
|
|
105
|
+
constructor(vectorId: string | number) {
|
|
106
|
+
super(`Vector '${vectorId}' not found`, 'VECTOR_NOT_FOUND');
|
|
107
|
+
this.name = 'VectorNotFoundError';
|
|
108
|
+
this.vectorId = vectorId;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verso - High-performance vector search with HNSW indexing
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - HNSW algorithm for approximate nearest neighbor search
|
|
6
|
+
* - Multiple distance metrics: cosine, euclidean, dot product
|
|
7
|
+
* - Int8 scalar quantization for 4x memory reduction
|
|
8
|
+
* - Batch query support for improved throughput
|
|
9
|
+
* - Parameter presets for different use cases
|
|
10
|
+
* - Multi-platform: Bun (file system) and Browser (OPFS)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { VectorDB, getRecommendedPreset } from 'verso';
|
|
15
|
+
*
|
|
16
|
+
* const db = new VectorDB();
|
|
17
|
+
* const preset = getRecommendedPreset(768);
|
|
18
|
+
*
|
|
19
|
+
* const collection = await db.createCollection('my-vectors', {
|
|
20
|
+
* dimension: 768,
|
|
21
|
+
* metric: 'cosine',
|
|
22
|
+
* M: preset.M,
|
|
23
|
+
* efConstruction: preset.efConstruction
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* await collection.add({
|
|
27
|
+
* ids: ['doc1', 'doc2'],
|
|
28
|
+
* vectors: [new Float32Array(768), new Float32Array(768)]
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* const results = await collection.query({
|
|
32
|
+
* queryVector: new Float32Array(768),
|
|
33
|
+
* k: 10,
|
|
34
|
+
* efSearch: preset.efSearch
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @packageDocumentation
|
|
39
|
+
* @module verso
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// Core components
|
|
43
|
+
export { VectorDB } from './VectorDB';
|
|
44
|
+
export type { VectorDBConfig } from './VectorDB';
|
|
45
|
+
export { Collection } from './Collection';
|
|
46
|
+
export type { AddConfig, QueryConfig, QueryResult } from './Collection';
|
|
47
|
+
export { HNSWIndex } from './HNSWIndex';
|
|
48
|
+
export type { DistanceMetric, Node } from './HNSWIndex';
|
|
49
|
+
|
|
50
|
+
// Distance backends
|
|
51
|
+
export type { DistanceBackend } from './backends/DistanceBackend';
|
|
52
|
+
export {
|
|
53
|
+
JsDistanceBackend,
|
|
54
|
+
dotProductFast,
|
|
55
|
+
l2SquaredFast,
|
|
56
|
+
normalizeInPlace,
|
|
57
|
+
cosineDistanceFast
|
|
58
|
+
} from './backends/JsDistanceBackend';
|
|
59
|
+
|
|
60
|
+
// Data structures
|
|
61
|
+
export { BinaryHeap } from './BinaryHeap';
|
|
62
|
+
export { MaxBinaryHeap } from './MaxBinaryHeap';
|
|
63
|
+
|
|
64
|
+
// Scalar Quantization (Int8 - 4x reduction)
|
|
65
|
+
export {
|
|
66
|
+
ScalarQuantizer,
|
|
67
|
+
QuantizedVectorStore,
|
|
68
|
+
dotProductInt8,
|
|
69
|
+
l2SquaredInt8,
|
|
70
|
+
cosineDistanceInt8
|
|
71
|
+
} from './quantization/ScalarQuantizer';
|
|
72
|
+
export type { QuantizationParams } from './quantization/ScalarQuantizer';
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Parameter presets
|
|
76
|
+
export type { HNSWPreset } from './presets';
|
|
77
|
+
export {
|
|
78
|
+
PRESET_LOW_DIM,
|
|
79
|
+
PRESET_MEDIUM_DIM,
|
|
80
|
+
PRESET_HIGH_DIM,
|
|
81
|
+
PRESET_VERY_HIGH_DIM,
|
|
82
|
+
PRESET_SMALL_DATASET,
|
|
83
|
+
PRESET_LARGE_DATASET,
|
|
84
|
+
PRESET_MAX_RECALL,
|
|
85
|
+
PRESET_LOW_LATENCY,
|
|
86
|
+
PRESETS,
|
|
87
|
+
getRecommendedPreset,
|
|
88
|
+
getPreset,
|
|
89
|
+
getRAGPreset
|
|
90
|
+
} from './presets';
|
|
91
|
+
|
|
92
|
+
// Storage backends (Bun, Browser, Memory)
|
|
93
|
+
export type { StorageBackend, StorageOptions } from './storage/StorageBackend';
|
|
94
|
+
export { BunStorageBackend } from './storage/BunStorageBackend';
|
|
95
|
+
export { MemoryBackend } from './storage/MemoryBackend';
|
|
96
|
+
export { OPFSBackend } from './storage/OPFSBackend';
|
|
97
|
+
export {
|
|
98
|
+
createStorageBackend,
|
|
99
|
+
getRecommendedStorageType,
|
|
100
|
+
isStorageTypeAvailable,
|
|
101
|
+
type StorageType,
|
|
102
|
+
type CreateStorageOptions,
|
|
103
|
+
} from './storage/createStorageBackend';
|
|
104
|
+
|
|
105
|
+
// Write-ahead log for incremental updates
|
|
106
|
+
export {
|
|
107
|
+
WriteAheadLog,
|
|
108
|
+
WALOperationType,
|
|
109
|
+
type WALEntry,
|
|
110
|
+
} from './storage/WriteAheadLog';
|
|
111
|
+
|
|
112
|
+
// Batch write coalescing for reduced I/O
|
|
113
|
+
export {
|
|
114
|
+
BatchWriter,
|
|
115
|
+
createBatchWriter,
|
|
116
|
+
type BatchWriterOptions,
|
|
117
|
+
} from './storage/BatchWriter';
|
|
118
|
+
|
|
119
|
+
// Delta encoding for compact neighbor lists
|
|
120
|
+
export {
|
|
121
|
+
deltaEncodeNeighbors,
|
|
122
|
+
deltaDecodeNeighbors,
|
|
123
|
+
deltaEncodedSize,
|
|
124
|
+
encodeVarint,
|
|
125
|
+
decodeVarint,
|
|
126
|
+
} from './encoding/DeltaEncoder';
|
|
127
|
+
|
|
128
|
+
// Error classes for better error handling
|
|
129
|
+
export {
|
|
130
|
+
VectorDBError,
|
|
131
|
+
DimensionMismatchError,
|
|
132
|
+
DuplicateVectorError,
|
|
133
|
+
CollectionNotFoundError,
|
|
134
|
+
CollectionExistsError,
|
|
135
|
+
StorageError,
|
|
136
|
+
QuantizationError,
|
|
137
|
+
VectorNotFoundError,
|
|
138
|
+
} from './errors';
|