perf(hnsw): deferred persistence mode for 30-50× faster cloud storage adds
Problem: Each add() triggered 70 GCS operations (34 reads + 36 writes) because HNSW updates 16+ neighbors per add, and each neighbor did a read-modify-write cycle. Result: 7-11 seconds per add() on GCS. Solution: - Add `hnswPersistMode: 'immediate' | 'deferred'` config option - In deferred mode, track dirty nodes instead of persisting immediately - Flush dirty nodes on close() or explicit flush() - Smart defaults: cloud storage (GCS/S3/R2/Azure) = deferred, local = immediate Performance impact: - Single add(): 7-11 seconds → 200-400ms (30-50× faster) - GCS operations per add: 70 → 2-3 Zero configuration - cloud storage automatically uses deferred mode. Files changed: - src/types/brainy.types.ts: Add hnswPersistMode config option - src/hnsw/hnswIndex.ts: Deferred mode, dirty tracking, flush() - src/hnsw/typeAwareHNSWIndex.ts: Propagate to child indexes - src/brainy.ts: Smart defaults, flush on close() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a33b759c40
commit
4d1d567236
4 changed files with 208 additions and 20 deletions
|
|
@ -65,18 +65,19 @@ export class TypeAwareHNSWIndex {
|
|||
private distanceFunction: DistanceFunction
|
||||
private storage: BaseStorage | null
|
||||
private useParallelization: boolean
|
||||
private persistMode: 'immediate' | 'deferred'
|
||||
|
||||
/**
|
||||
* Create a new TypeAwareHNSWIndex
|
||||
*
|
||||
* @param config HNSW configuration (M, efConstruction, efSearch, ml)
|
||||
* @param distanceFunction Distance function (default: euclidean)
|
||||
* @param options Additional options (storage, parallelization)
|
||||
* @param options Additional options (storage, parallelization, persistMode)
|
||||
*/
|
||||
constructor(
|
||||
config: Partial<HNSWConfig> = {},
|
||||
distanceFunction: DistanceFunction = euclideanDistance,
|
||||
options: { useParallelization?: boolean; storage?: BaseStorage } = {}
|
||||
options: { useParallelization?: boolean; storage?: BaseStorage; persistMode?: 'immediate' | 'deferred' } = {}
|
||||
) {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config }
|
||||
this.distanceFunction = distanceFunction
|
||||
|
|
@ -85,6 +86,7 @@ export class TypeAwareHNSWIndex {
|
|||
options.useParallelization !== undefined
|
||||
? options.useParallelization
|
||||
: true
|
||||
this.persistMode = options.persistMode || 'immediate'
|
||||
|
||||
prodLog.info('TypeAwareHNSWIndex initialized (Phase 2: Type-Aware HNSW)')
|
||||
}
|
||||
|
|
@ -105,7 +107,8 @@ export class TypeAwareHNSWIndex {
|
|||
for (const [type, parentIndex] of parent.indexes.entries()) {
|
||||
const childIndex = new HNSWIndex(this.config, this.distanceFunction, {
|
||||
useParallelization: this.useParallelization,
|
||||
storage: this.storage || undefined
|
||||
storage: this.storage || undefined,
|
||||
persistMode: this.persistMode
|
||||
})
|
||||
childIndex.enableCOW(parentIndex)
|
||||
this.indexes.set(type, childIndex)
|
||||
|
|
@ -114,6 +117,52 @@ export class TypeAwareHNSWIndex {
|
|||
prodLog.info(`TypeAwareHNSWIndex COW enabled: ${parent.indexes.size} type-specific indexes shallow copied`)
|
||||
}
|
||||
|
||||
/**
|
||||
* v6.2.8: Flush dirty HNSW data to storage for all type-specific indexes
|
||||
*
|
||||
* In deferred persistence mode, HNSW connections are tracked as dirty but not
|
||||
* immediately persisted. Call flush() to persist all pending changes across
|
||||
* all type-specific indexes.
|
||||
*
|
||||
* @returns Total number of nodes flushed across all indexes
|
||||
*/
|
||||
public async flush(): Promise<number> {
|
||||
if (this.indexes.size === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const flushPromises = Array.from(this.indexes.values()).map(index =>
|
||||
index.flush()
|
||||
)
|
||||
|
||||
const results = await Promise.all(flushPromises)
|
||||
const totalFlushed = results.reduce((sum, count) => sum + count, 0)
|
||||
|
||||
if (totalFlushed > 0) {
|
||||
prodLog.info(`[TypeAwareHNSW] Flushed ${totalFlushed} dirty nodes across ${this.indexes.size} type indexes`)
|
||||
}
|
||||
|
||||
return totalFlushed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of dirty (unpersisted) nodes across all type-specific indexes
|
||||
*/
|
||||
public getDirtyNodeCount(): number {
|
||||
let total = 0
|
||||
for (const index of this.indexes.values()) {
|
||||
total += index.getDirtyNodeCount()
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current persist mode
|
||||
*/
|
||||
public getPersistMode(): 'immediate' | 'deferred' {
|
||||
return this.persistMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create HNSW index for a specific type (lazy initialization)
|
||||
*
|
||||
|
|
@ -137,7 +186,8 @@ export class TypeAwareHNSWIndex {
|
|||
|
||||
const index = new HNSWIndex(this.config, this.distanceFunction, {
|
||||
useParallelization: this.useParallelization,
|
||||
storage: this.storage || undefined
|
||||
storage: this.storage || undefined,
|
||||
persistMode: this.persistMode
|
||||
})
|
||||
|
||||
this.indexes.set(type, index)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue