2026-01-31 12:41:53 -08:00
/ * *
* Lazy Vector Loading Tests ( B2 optimization )
*
* Tests for :
* - Vectors evicted from memory after addItem ( ) in lazy mode
* - Search returns correct results after vector eviction
* - Memory mode retains vectors ( default behavior )
* - Lazy mode requires storage adapter
* /
import { describe , it , expect , beforeEach } from 'vitest'
import { v4 as uuidv4 } from 'uuid'
2026-06-09 13:07:56 -07:00
import { JsHnswVectorIndex } from '../../../src/hnsw/hnswIndex.js'
2026-01-31 12:41:53 -08:00
import { euclideanDistance } from '../../../src/utils/index.js'
import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js'
// Helper: generate a random vector of given dimension
function randomVector ( dim : number ) : number [ ] {
return Array . from ( { length : dim } , ( ) = > Math . random ( ) * 2 - 1 )
}
// Helper: save a vector to storage so lazy loading can retrieve it
// Both noun (vector) and metadata must be saved for getNounVector() to work
async function saveVector ( storage : MemoryStorage , id : string , vector : number [ ] ) : Promise < void > {
await storage . saveNoun ( {
id ,
vector ,
connections : new Map ( ) ,
level : 0
} )
await storage . saveNounMetadata ( id , {
noun : 'thing' ,
createdAt : Date.now ( ) ,
updatedAt : Date.now ( )
} )
}
describe ( 'Lazy Vector Loading (B2)' , ( ) = > {
const dim = 32
// =================================================================
// 1. LAZY MODE: VECTOR EVICTION
// =================================================================
describe ( 'vector eviction in lazy mode' , ( ) = > {
2026-06-09 13:07:56 -07:00
let index : JsHnswVectorIndex
2026-01-31 12:41:53 -08:00
let storage : MemoryStorage
beforeEach ( async ( ) = > {
storage = new MemoryStorage ( )
2026-06-09 13:07:56 -07:00
index = new JsHnswVectorIndex (
2026-01-31 12:41:53 -08:00
{
M : 8 ,
efConstruction : 100 ,
efSearch : 50 ,
ml : 8 ,
vectorStorage : 'lazy'
} ,
euclideanDistance ,
{ useParallelization : false , storage }
)
} )
it ( 'should still be searchable after vector eviction' , async ( ) = > {
const targetId = uuidv4 ( )
const target = randomVector ( dim )
// Store noun in storage so lazy loading can find it
await saveVector ( storage , targetId , target )
await index . addItem ( { id : targetId , vector : target } )
// Add more entities
for ( let i = 0 ; i < 20 ; i ++ ) {
const id = uuidv4 ( )
const v = randomVector ( dim )
await saveVector ( storage , id , v )
await index . addItem ( { id , vector : v } )
}
// Search should still find the target
const results = await index . search ( target , 5 )
expect ( results . length ) . toBeGreaterThan ( 0 )
// The closest result should be the target (distance ~0)
const targetResult = results . find ( ( [ id ] ) = > id === targetId )
expect ( targetResult ) . toBeDefined ( )
expect ( targetResult ! [ 1 ] ) . toBeCloseTo ( 0 , 1 )
} )
it ( 'should return correct top-k results in lazy mode' , async ( ) = > {
2026-02-01 16:23:49 -08:00
for ( let i = 0 ; i < 50 ; i ++ ) {
2026-01-31 12:41:53 -08:00
const id = uuidv4 ( )
const v = randomVector ( dim )
await saveVector ( storage , id , v )
await index . addItem ( { id , vector : v } )
}
const query = randomVector ( dim )
const results = await index . search ( query , 10 )
feat: enforce data/metadata separation, numeric range queries, improved docs
- Store data opaquely in add() and update() instead of spreading object
properties into top-level metadata. data is for semantic search (HNSW),
metadata is for structured where-filter queries (MetadataIndex).
- Fix numeric range queries in MetadataIndex — use numeric-aware comparison
instead of lexicographic string comparison for normalized values.
- Add data field to RelateParams and Relation types for relationship content.
- Add where.type → where.noun alias in metadata-only find() path.
- Rewrite README: focused ~350 lines from 791, quick start first, feature
showcase with mini-snippets, organized doc links, no version callouts.
- Add DATA_MODEL.md and QUERY_OPERATORS.md reference docs.
- Remove 10 outdated/redundant doc files consolidated into API reference.
- Improve JSDoc on Entity, Relation, AddParams, FindParams, and core methods.
- Fix tests asserting data properties appear in metadata (data model violation).
- Deprecate verb.source/target in favor of from/to (public) and sourceId/targetId (storage).
2026-02-09 12:06:59 -08:00
// With lazy mode and small graph (50 items), HNSW may return slightly fewer
// than k if some vectors are evicted and unreachable during graph traversal
expect ( results . length ) . toBeGreaterThanOrEqual ( 8 )
expect ( results . length ) . toBeLessThanOrEqual ( 10 )
2026-01-31 12:41:53 -08:00
// Results should be sorted by distance
for ( let i = 1 ; i < results . length ; i ++ ) {
expect ( results [ i ] [ 1 ] ) . toBeGreaterThanOrEqual ( results [ i - 1 ] [ 1 ] )
}
} )
} )
// =================================================================
// 2. MEMORY MODE: VECTORS RETAINED (DEFAULT)
// =================================================================
describe ( 'memory mode retains vectors (default)' , ( ) = > {
it ( 'should keep vectors in memory by default' , async ( ) = > {
const storage = new MemoryStorage ( )
2026-06-09 13:07:56 -07:00
const index = new JsHnswVectorIndex (
2026-01-31 12:41:53 -08:00
{ M : 4 , efConstruction : 50 , efSearch : 20 } ,
euclideanDistance ,
{ useParallelization : false , storage }
)
const id = uuidv4 ( )
const v = randomVector ( dim )
await index . addItem ( { id , vector : v } )
// Search should work without needing storage
const results = await index . search ( v , 1 )
expect ( results . length ) . toBe ( 1 )
expect ( results [ 0 ] [ 0 ] ) . toBe ( id )
expect ( results [ 0 ] [ 1 ] ) . toBeCloseTo ( 0 , 5 )
} )
it ( 'should work without storage adapter in memory mode' , async ( ) = > {
// No storage adapter provided
2026-06-09 13:07:56 -07:00
const index = new JsHnswVectorIndex (
2026-01-31 12:41:53 -08:00
{ M : 4 , efConstruction : 50 , efSearch : 20 } ,
euclideanDistance ,
{ useParallelization : false }
)
const id = uuidv4 ( )
const v = randomVector ( dim )
await index . addItem ( { id , vector : v } )
const results = await index . search ( v , 1 )
expect ( results . length ) . toBe ( 1 )
expect ( results [ 0 ] [ 0 ] ) . toBe ( id )
} )
} )
// =================================================================
// 3. LAZY + NO STORAGE: GRACEFUL BEHAVIOR
// =================================================================
describe ( 'lazy mode without storage' , ( ) = > {
it ( 'should not evict vectors when no storage adapter is configured' , async ( ) = > {
// vectorStorage: 'lazy' but no storage — vectors should stay in memory
2026-06-09 13:07:56 -07:00
const index = new JsHnswVectorIndex (
2026-01-31 12:41:53 -08:00
{
M : 4 ,
efConstruction : 50 ,
efSearch : 20 ,
vectorStorage : 'lazy'
} ,
euclideanDistance ,
{ useParallelization : false }
)
const id = uuidv4 ( )
const v = randomVector ( dim )
await index . addItem ( { id , vector : v } )
// Should still work because vectors aren't evicted without storage
const results = await index . search ( v , 1 )
expect ( results . length ) . toBe ( 1 )
expect ( results [ 0 ] [ 0 ] ) . toBe ( id )
} )
} )
// =================================================================
2026-06-15 10:08:51 -07:00
// 4. MULTIPLE SEARCHES IN LAZY MODE
2026-01-31 12:41:53 -08:00
// =================================================================
describe ( 'multiple searches in lazy mode' , ( ) = > {
it ( 'should handle repeated searches correctly' , async ( ) = > {
const storage = new MemoryStorage ( )
2026-06-09 13:07:56 -07:00
const index = new JsHnswVectorIndex (
2026-01-31 12:41:53 -08:00
{
M : 8 ,
efConstruction : 100 ,
efSearch : 50 ,
ml : 8 ,
vectorStorage : 'lazy'
} ,
euclideanDistance ,
{ useParallelization : false , storage }
)
const entries : Array < { id : string ; vector : number [ ] } > = [ ]
for ( let i = 0 ; i < 25 ; i ++ ) {
const id = uuidv4 ( )
const v = randomVector ( dim )
entries . push ( { id , vector : v } )
await saveVector ( storage , id , v )
await index . addItem ( { id , vector : v } )
}
// Run multiple searches — each should return results and be consistent
for ( let q = 0 ; q < 5 ; q ++ ) {
const entry = entries [ q * 5 ]
const results = await index . search ( entry . vector , 5 )
expect ( results . length ) . toBe ( 5 )
// Results should be sorted by distance
for ( let i = 1 ; i < results . length ; i ++ ) {
expect ( results [ i ] [ 1 ] ) . toBeGreaterThanOrEqual ( results [ i - 1 ] [ 1 ] )
}
// At least the closest result should have a reasonably small distance
expect ( results [ 0 ] [ 1 ] ) . toBeLessThan ( 5 )
}
} )
} )
} )