@n2flowjs/nbase
Version:
Neural Vector Database for efficient similarity search
184 lines (149 loc) • 5.61 kB
text/typescript
import { expect } from 'chai';
import express from 'express';
import { beforeEach, describe, it } from 'mocha';
import * as sinon from 'sinon';
import request from 'supertest';
import { searchRoutes } from '../../src/server/routes/search';
import { ApiContext } from '../../src/types';
describe('Search Routes', () => {
let app: express.Application;
let context: ApiContext;
let mockDatabase: any;
let mockTimer: any;
let mockCreateFilterFunction: any;
let originalConsoleError: any;
beforeEach(() => {
// Save original console.error to restore later
originalConsoleError = console.error;
console.error = sinon.stub();
// Create mock database
mockDatabase = {
findNearest: sinon.stub().resolves([
{ id: '1', score: 0.95, metadata: { title: 'Test 1' } },
{ id: '2', score: 0.85, metadata: { title: 'Test 2' } },
]),
};
// Create mock timer
mockTimer = {
start: sinon.stub().returns(undefined),
stop: sinon.stub().returns({ total: 123 }),
};
// Create mock filter function
mockCreateFilterFunction = sinon.stub().returns(() => true);
// Create mock context
context = {
database: mockDatabase,
timer: mockTimer,
createFilterFunction: mockCreateFilterFunction,
} as any;
// Setup express app with search routes
app = express();
app.use(express.json());
app.use('/search', searchRoutes(context));
});
afterEach(() => {
console.error = originalConsoleError;
sinon.restore();
});
it('should return 400 when query is missing', async () => {
const response = await request(app).post('/search').send({});
expect(response.status).to.equal(400);
expect(response.body.error).to.equal('Invalid request: query vector array is required');
});
it('should return 400 when query is not an array', async () => {
const response = await request(app).post('/search').send({ query: 'not an array' });
expect(response.status).to.equal(400);
expect(response.body.error).to.equal('Invalid request: query vector array is required');
});
it('should return search results with default options', async () => {
const response = await request(app)
.post('/search')
.send({ query: [0.1, 0.2, 0.3], k: 2 });
expect(response.status).to.equal(200);
expect(response.body.results).to.be.an('array').with.lengthOf(2);
expect(response.body.count).to.equal(2);
expect(response.body.duration).to.equal(123);
expect(response.body.searchOptions).to.deep.include({
k: 2,
method: 'clustered',
partitionsSearched: 'all',
filtersApplied: false,
cacheUsed: true,
});
expect(mockTimer.start.calledWith('search')).to.be.true;
expect(mockTimer.stop.calledWith('search')).to.be.true;
expect(mockDatabase.findNearest.calledOnce).to.be.true;
});
it('should handle search with HNSW method', async () => {
const response = await request(app)
.post('/search')
.send({
query: [0.1, 0.2, 0.3],
k: 5,
method: 'hnsw',
efSearch: 100,
});
expect(response.status).to.equal(200);
expect(response.body.searchOptions.method).to.equal('hnsw');
// Verify correct options were passed to database.findNearest
const searchOptionsArg = mockDatabase.findNearest.args[0][2];
expect(searchOptionsArg.useHNSW).to.be.true;
expect(searchOptionsArg.efSearch).to.equal(100);
});
it('should handle search with filters', async () => {
const filters = { category: 'test' };
const response = await request(app)
.post('/search')
.send({
query: [0.1, 0.2, 0.3],
filters: filters,
});
expect(response.status).to.equal(200);
expect(response.body.searchOptions.filtersApplied).to.be.true;
expect(mockCreateFilterFunction.calledWith(filters)).to.be.true;
});
it('should handle search with array filters', async () => {
const filters = [{ field: 'category', value: 'test' }];
const response = await request(app)
.post('/search')
.send({
query: [0.1, 0.2, 0.3],
filters: filters,
});
expect(response.status).to.equal(200);
expect(response.body.searchOptions.filtersApplied).to.be.true;
expect(mockCreateFilterFunction.calledWith(filters)).to.be.true;
});
it('should handle search with partitionIds', async () => {
const partitionIds = ['partition1', 'partition2'];
const response = await request(app)
.post('/search')
.send({
query: [0.1, 0.2, 0.3],
partitionIds: partitionIds,
});
expect(response.status).to.equal(200);
expect(response.body.searchOptions.partitionsSearched).to.equal(2);
});
it('should handle database errors', async () => {
const errorMessage = 'Database error';
mockDatabase.findNearest.rejects(new Error(errorMessage));
const response = await request(app)
.post('/search')
.send({ query: [0.1, 0.2, 0.3] });
expect(response.status).to.equal(500);
expect(response.body.error).to.equal(errorMessage);
expect(response.body.duration).to.equal(123);
});
it('should include stack trace in development mode', async () => {
process.env.NODE_ENV = 'development';
mockDatabase.findNearest.rejects(new Error('Test error'));
const response = await request(app)
.post('/search')
.send({ query: [0.1, 0.2, 0.3] });
expect(response.status).to.equal(500);
expect(response.body.stack).to.be.a('string');
// Reset NODE_ENV
delete process.env.NODE_ENV;
});
});