diff --git a/src/state/vector-index.ts b/src/state/vector-index.ts index 03c8248e..b37c2cb8 100644 --- a/src/state/vector-index.ts +++ b/src/state/vector-index.ts @@ -1,9 +1,10 @@ function float32ToBase64(arr: Float32Array): string { - return Buffer.from(arr.buffer).toString("base64"); + return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString("base64"); } function base64ToFloat32(b64: string): Float32Array { - return new Float32Array(Buffer.from(b64, "base64").buffer); + const buf = Buffer.from(b64, "base64"); + return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4); } function cosineSimilarity(a: Float32Array, b: Float32Array): number { diff --git a/test/vector-index.test.ts b/test/vector-index.test.ts index 7415e557..8decde32 100644 --- a/test/vector-index.test.ts +++ b/test/vector-index.test.ts @@ -76,4 +76,30 @@ describe("VectorIndex", () => { const results = index.search(new Float32Array([1, 0, 0])); expect(results[0].score).toBe(0); }); + + it("serialize round-trip preserves dimension and values for pooled-buffer sizes", () => { + // 384 floats = 1536 bytes, small enough for Node to pool the decoded Buffer. + // Without the byteOffset/byteLength fix, deserialized length becomes 2048 + // and values read from pool offset 0 instead of the slice. + const DIM = 384; + const vecs = Array.from({ length: 5 }, (_, n) => { + const v = new Float32Array(DIM); + for (let i = 0; i < DIM; i++) v[i] = n * 1000 + i; + return v; + }); + + vecs.forEach((v, n) => index.add(`obs_${n}`, "ses_1", v)); + const restored = VectorIndex.deserialize(index.serialize()); + + expect(restored.size).toBe(5); + // Dimension must be exactly DIM, not 2048 (Buffer.poolSize / 4). + const { mismatches } = restored.validateDimensions(DIM); + expect(mismatches).toEqual([]); + // Each vector must be its own nearest neighbour after reload. + for (let n = 0; n < 5; n++) { + const results = restored.search(vecs[n], 1); + expect(results[0].obsId).toBe(`obs_${n}`); + expect(results[0].score).toBeCloseTo(1.0, 4); + } + }); });