feat: add SQ8 vector quantization, lazy loading, and two-phase rerank to HNSW

- SQ8 scalar quantization (8-bit) for 4x vector storage reduction
- Lazy vector loading: evict float32 vectors after graph construction,
  load on-demand from storage via UnifiedCache
- Two-phase search: over-retrieve with SQ8 approximate distances,
  rerank top candidates with exact float32 distances
- Configuration surface: hnsw.quantization and hnsw.vectorStorage in BrainyConfig
- All features disabled by default (zero behavior change for existing users)
- 27 new tests covering quantization accuracy, lazy loading, reranking
- Remove GitHub Actions CI (build locally, cortex CI handles native builds)
This commit is contained in:
David Snelling 2026-01-31 12:41:53 -08:00
parent e384afcdac
commit 0f3a88429d
9 changed files with 1138 additions and 222 deletions

View file

@ -256,21 +256,22 @@ export class TypeAwareHNSWIndex {
queryVector: Vector,
k: number = 10,
type?: NounType | NounType[],
filter?: (id: string) => Promise<boolean>
filter?: (id: string) => Promise<boolean>,
options?: { rerank?: { multiplier: number } }
): Promise<Array<[string, number]>> {
// Single-type search (fast path)
if (type && typeof type === 'string') {
const index = this.getIndexForType(type)
return await index.search(queryVector, k, filter)
return await index.search(queryVector, k, filter, options)
}
// Multi-type search (handle empty array edge case)
if (type && Array.isArray(type) && type.length > 0) {
return await this.searchMultipleTypes(queryVector, k, type, filter)
return await this.searchMultipleTypes(queryVector, k, type, filter, options)
}
// All-types search (slowest path + empty array fallback)
return await this.searchAllTypes(queryVector, k, filter)
return await this.searchAllTypes(queryVector, k, filter, options)
}
/**
@ -286,7 +287,8 @@ export class TypeAwareHNSWIndex {
queryVector: Vector,
k: number,
types: NounType[],
filter?: (id: string) => Promise<boolean>
filter?: (id: string) => Promise<boolean>,
options?: { rerank?: { multiplier: number } }
): Promise<Array<[string, number]>> {
const allResults: Array<[string, number]> = []
@ -294,7 +296,7 @@ export class TypeAwareHNSWIndex {
for (const type of types) {
if (this.indexes.has(type)) {
const index = this.indexes.get(type)!
const results = await index.search(queryVector, k, filter)
const results = await index.search(queryVector, k, filter, options)
allResults.push(...results)
}
}
@ -320,13 +322,14 @@ export class TypeAwareHNSWIndex {
private async searchAllTypes(
queryVector: Vector,
k: number,
filter?: (id: string) => Promise<boolean>
filter?: (id: string) => Promise<boolean>,
options?: { rerank?: { multiplier: number } }
): Promise<Array<[string, number]>> {
const allResults: Array<[string, number]> = []
// Search each type's graph
for (const [type, index] of this.indexes.entries()) {
const results = await index.search(queryVector, k, filter)
const results = await index.search(queryVector, k, filter, options)
allResults.push(...results)
}