UNPKG

@n2flowjs/nbase

Version:

Neural Vector Database for efficient similarity search

296 lines (242 loc) 11.2 kB
import { expect } from 'chai'; import { VectorDB } from '../src'; import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; const mkdtemp = promisify(fs.mkdtemp); const rimraf = promisify(fs.rm); describe('VectorDB', () => { let tempDir: string; before(async () => { tempDir = await mkdtemp(path.join(__dirname, 'vector_db_test_')); }); after(async () => { await rimraf(tempDir, { recursive: true, force: true }); }); describe('Constructor', () => { it('should create a VectorDB instance with default parameters', () => { const db = new VectorDB(); expect(db).to.be.instanceOf(VectorDB); expect(db.defaultVectorSize).to.be.null; expect(db.memoryStorage.size).to.equal(0); }); it('should create a VectorDB with specified vector size', () => { const db = new VectorDB(128); expect(db.defaultVectorSize).to.equal(128); }); }); describe('Vector Operations', () => { let db: VectorDB; beforeEach(() => { db = new VectorDB(3); }); it('should add a vector with auto-generated ID', () => { const vector = new Float32Array([0.1, 0.2, 0.3]); const id = db.addVector(undefined, vector); expect(id).to.equal(1); expect(db.getVector(id)).to.deep.equal(vector); }); it('should add a vector with specified numeric ID', () => { const vector = new Float32Array([0.4, 0.5, 0.6]); const id = db.addVector(5, vector); expect(id).to.equal(5); expect(db.getVector(id)).to.deep.equal(vector); expect(db.getIdCounter()).to.equal(6); // Should update idCounter }); it('should add a vector with specified string ID', () => { const vector = new Float32Array([0.7, 0.8, 0.9]); const id = db.addVector('test-id', vector); expect(id).to.equal('test-id'); expect(db.getVector(id)).to.deep.equal(vector); }); it('should overwrite existing vector with same ID', () => { const vector1 = new Float32Array([0.1, 0.2, 0.3]); const vector2 = new Float32Array([0.4, 0.5, 0.6]); db.addVector(1, vector1); db.addVector(1, vector2); expect(db.getVector(1)).to.deep.equal(vector2); }); it('should check if vector exists', () => { db.addVector(1, [0.1, 0.2, 0.3]); expect(db.hasVector(1)).to.be.true; expect(db.hasVector(2)).to.be.false; }); it('should delete a vector', () => { db.addVector(1, [0.1, 0.2, 0.3]); expect(db.deleteVector(1)).to.be.true; expect(db.hasVector(1)).to.be.false; expect(db.deleteVector(1)).to.be.false; // Already deleted }); it('should get vector dimension', () => { db.addVector(1, [0.1, 0.2, 0.3]); db.addVector(2, [0.1, 0.2, 0.3, 0.4]); expect(db.getVectorDimension(1)).to.equal(3); expect(db.getVectorDimension(2)).to.equal(4); expect(db.getVectorDimension(3)).to.be.null; }); it('should bulk add vectors', () => { const vectors = [ { id: 1, vector: [0.1, 0.2, 0.3] }, { id: 2, vector: [0.4, 0.5, 0.6] }, { id: 'test', vector: [0.7, 0.8, 0.9] } ]; const added = db.bulkAdd(vectors); expect(added).to.equal(3); expect(db.getVector(1)).to.deep.equal(new Float32Array([0.1, 0.2, 0.3])); expect(db.getVector(2)).to.deep.equal(new Float32Array([0.4, 0.5, 0.6])); expect(db.getVector('test')).to.deep.equal(new Float32Array([0.7, 0.8, 0.9])); }); }); describe('Metadata Operations', () => { let db: VectorDB; beforeEach(() => { db = new VectorDB(3); db.addVector(1, [0.1, 0.2, 0.3]); }); it('should add metadata to a vector', () => { db.addMetadata(1, { source: 'test', tags: ['tag1', 'tag2'] }); expect(db.getMetadata(1)).to.deep.equal({ source: 'test', tags: ['tag1', 'tag2'] }); }); it('should throw when adding metadata to non-existent vector', () => { expect(() => db.addMetadata(999, { source: 'test' })).to.throw(); }); it('should update metadata with object', () => { db.addMetadata(1, { source: 'test', count: 5 }); const updated = db.updateMetadata(1, { count: 10, newField: 'value' }); expect(updated).to.be.true; expect(db.getMetadata(1)).to.deep.equal({ source: 'test', count: 10, newField: 'value' }); }); it('should update metadata with function', () => { db.addMetadata(1, { counter: 5 }); const updated = db.updateMetadata(1, (current) => { return { counter: current ? current.counter + 1 : 1 }; }); expect(updated).to.be.true; expect(db.getMetadata(1)).to.deep.equal({ counter: 6 }); }); it('should return false when updating non-existent vector metadata', () => { const updated = db.updateMetadata(999, { field: 'value' }); expect(updated).to.be.false; }); }); describe('Vector Search', () => { let db: VectorDB; beforeEach(() => { db = new VectorDB(3); db.addVector(1, [1, 0, 0], { category: 'x' }); db.addVector(2, [0, 1, 0], { category: 'y' }); db.addVector(3, [0, 0, 1], { category: 'z' }); db.addVector(4, [0.7, 0.7, 0], { category: 'xy' }); }); it('should find nearest vectors using euclidean distance', () => { const results = db.findNearest([0.8, 0.8, 0], 2); expect(results.length).to.equal(2); expect(results[0].id).to.equal(4); // Vector 4 should be closest expect(results[1].id).to.be.oneOf([1, 2]); // Either vector 1 or 2 }); it('should find nearest vectors using cosine distance', () => { const results = db.findNearest([0.5, 0.5, 0], 2, { metric: 'cosine' }); expect(results.length).to.equal(2); expect(results[0].id).to.equal(4); // Vector 4 should be closest }); it('should apply filter when searching', () => { const results = db.findNearest([0.8, 0.8, 0], 3, { filter: (id, metadata) => metadata?.category === 'xy' }); expect(results.length).to.equal(1); expect(results[0].id).to.equal(4); }); }); describe('Stats and Lifecycle', () => { let db: VectorDB; beforeEach(() => { db = new VectorDB(3); db.addVector(1, [1, 0, 0], { category: 'x' }); db.addVector(2, [0, 1, 0], { category: 'y' }); db.addVector(3, [0, 0, 1], { category: 'z' }); }); it('should return correct vector count', () => { expect(db.getVectorCount()).to.equal(3); }); it('should return vector statistics', () => { const stats = db.getStats(); expect(stats.vectorCount).to.equal(3); expect(stats.metadataCount).to.equal(3); expect(stats.defaultVectorSize).to.equal(3); expect(stats.dimensions.unique).to.equal(1); expect(stats.dimensions.counts).to.deep.equal({ '3': 3 }); }); it('should clear data on close', async () => { await db.close(); expect(db.getVectorCount()).to.equal(0); }); }); describe('Persistence', () => { let db: VectorDB; let dbPath: string; beforeEach(() => { dbPath = path.join(tempDir, 'test_db'); db = new VectorDB(3, dbPath); }); afterEach(async () => { await db.close(); }); it('should save and load database', async () => { // Add vectors and metadata db.addVector(1, [0.1, 0.2, 0.3], { tag: 'test1' }); db.addVector(2, [0.4, 0.5, 0.6], { tag: 'test2' }); // Save to disk await db.save(); // Create a new instance that loads from disk const loadedDb = new VectorDB(null, dbPath); // Wait a bit for async loading await new Promise(resolve => setTimeout(resolve, 100)); // Verify data is loaded expect(loadedDb.getVectorCount()).to.equal(2); expect(loadedDb.getVector(1)).to.deep.equal(new Float32Array([0.1, 0.2, 0.3])); expect(loadedDb.getMetadata(1)).to.deep.equal({ tag: 'test1' }); }); it('should save and load with compression', async () => { // Create DB with compression const compressedDbPath = path.join(tempDir, 'compressed_db'); const compressedDb = new VectorDB(3, compressedDbPath, { useCompression: true }); // Add vectors compressedDb.addVector(1, [0.1, 0.2, 0.3]); // Save to disk await compressedDb.save(); // Create a new instance with compression that loads from disk const loadedDb = new VectorDB(null, compressedDbPath, { useCompression: true }); // Wait a bit for async loading await new Promise(resolve => setTimeout(resolve, 100)); // Verify data is loaded expect(loadedDb.getVectorCount()).to.equal(1); expect(loadedDb.getVector(1)).to.deep.equal(new Float32Array([0.1, 0.2, 0.3])); await compressedDb.close(); }); }); describe('Events', () => { it('should emit events on vector operations', (done) => { const db = new VectorDB(3); db.on('vector:add', (data) => { expect(data.id).to.equal(1); expect(data.dimensions).to.equal(3); done(); }); db.addVector(1, [0.1, 0.2, 0.3]); }); it('should emit events on metadata operations', (done) => { const db = new VectorDB(3); db.addVector(1, [0.1, 0.2, 0.3]); db.on('metadata:add', (data) => { expect(data.id).to.equal(1); expect(data.metadata).to.deep.equal({ test: 'value' }); done(); }); db.addMetadata(1, { test: 'value' }); }); }); });