brainy/dist/hnsw/hnswIndexOptimized.js
David Snelling f8c45f2d8d Initial commit: Brainy - Multi-Dimensional AI Database
Open source vector database with HNSW indexing, graph relationships,
and metadata facets. Features CLI with professional augmentation registry
integration for discovering extensions and capabilities.
2025-08-18 17:35:06 -07:00

471 lines
No EOL
17 KiB
JavaScript

/**
* Optimized HNSW (Hierarchical Navigable Small World) Index implementation
* Extends the base HNSW implementation with support for large datasets
* Uses product quantization for dimensionality reduction and disk-based storage when needed
*/
import { HNSWIndex } from './hnswIndex.js';
// Default configuration for the optimized HNSW index
const DEFAULT_OPTIMIZED_CONFIG = {
M: 16,
efConstruction: 200,
efSearch: 50,
ml: 16,
memoryThreshold: 1024 * 1024 * 1024, // 1GB default threshold
productQuantization: {
enabled: false,
numSubvectors: 16,
numCentroids: 256
},
useDiskBasedIndex: false
};
/**
* Product Quantization implementation
* Reduces vector dimensionality by splitting vectors into subvectors
* and quantizing each subvector to the nearest centroid
*/
class ProductQuantizer {
constructor(numSubvectors = 16, numCentroids = 256) {
this.centroids = [];
this.subvectorSize = 0;
this.initialized = false;
this.dimension = 0;
this.numSubvectors = numSubvectors;
this.numCentroids = numCentroids;
}
/**
* Initialize the product quantizer with training data
* @param vectors Training vectors to use for learning centroids
*/
train(vectors) {
if (vectors.length === 0) {
throw new Error('Cannot train product quantizer with empty vector set');
}
this.dimension = vectors[0].length;
this.subvectorSize = Math.ceil(this.dimension / this.numSubvectors);
// Initialize centroids for each subvector
for (let i = 0; i < this.numSubvectors; i++) {
// Extract subvectors from training data
const subvectors = vectors.map((vector) => {
const start = i * this.subvectorSize;
const end = Math.min(start + this.subvectorSize, this.dimension);
return vector.slice(start, end);
});
// Initialize centroids for this subvector using k-means++
this.centroids[i] = this.kMeansPlusPlus(subvectors, this.numCentroids);
}
this.initialized = true;
}
/**
* Quantize a vector using product quantization
* @param vector Vector to quantize
* @returns Array of centroid indices, one for each subvector
*/
quantize(vector) {
if (!this.initialized) {
throw new Error('Product quantizer not initialized. Call train() first.');
}
if (vector.length !== this.dimension) {
throw new Error(`Vector dimension mismatch: expected ${this.dimension}, got ${vector.length}`);
}
const codes = [];
// Quantize each subvector
for (let i = 0; i < this.numSubvectors; i++) {
const start = i * this.subvectorSize;
const end = Math.min(start + this.subvectorSize, this.dimension);
const subvector = vector.slice(start, end);
// Find nearest centroid
let minDist = Number.MAX_VALUE;
let nearestCentroidIndex = 0;
for (let j = 0; j < this.centroids[i].length; j++) {
const centroid = this.centroids[i][j];
const dist = this.euclideanDistanceSquared(subvector, centroid);
if (dist < minDist) {
minDist = dist;
nearestCentroidIndex = j;
}
}
codes.push(nearestCentroidIndex);
}
return codes;
}
/**
* Reconstruct a vector from its quantized representation
* @param codes Array of centroid indices
* @returns Reconstructed vector
*/
reconstruct(codes) {
if (!this.initialized) {
throw new Error('Product quantizer not initialized. Call train() first.');
}
if (codes.length !== this.numSubvectors) {
throw new Error(`Code length mismatch: expected ${this.numSubvectors}, got ${codes.length}`);
}
const reconstructed = [];
// Reconstruct each subvector
for (let i = 0; i < this.numSubvectors; i++) {
const centroidIndex = codes[i];
const centroid = this.centroids[i][centroidIndex];
// Add centroid components to reconstructed vector
for (const component of centroid) {
reconstructed.push(component);
}
}
// Trim to original dimension if needed
return reconstructed.slice(0, this.dimension);
}
/**
* Compute squared Euclidean distance between two vectors
* @param a First vector
* @param b Second vector
* @returns Squared Euclidean distance
*/
euclideanDistanceSquared(a, b) {
let sum = 0;
const length = Math.min(a.length, b.length);
for (let i = 0; i < length; i++) {
const diff = a[i] - b[i];
sum += diff * diff;
}
return sum;
}
/**
* Implement k-means++ algorithm to initialize centroids
* @param vectors Vectors to cluster
* @param k Number of clusters
* @returns Array of centroids
*/
kMeansPlusPlus(vectors, k) {
if (vectors.length < k) {
// If we have fewer vectors than centroids, use the vectors as centroids
return [...vectors];
}
const centroids = [];
// Choose first centroid randomly
const firstIndex = Math.floor(Math.random() * vectors.length);
centroids.push([...vectors[firstIndex]]);
// Choose remaining centroids
for (let i = 1; i < k; i++) {
// Compute distances to nearest centroid for each vector
const distances = vectors.map((vector) => {
let minDist = Number.MAX_VALUE;
for (const centroid of centroids) {
const dist = this.euclideanDistanceSquared(vector, centroid);
minDist = Math.min(minDist, dist);
}
return minDist;
});
// Compute sum of distances
const distSum = distances.reduce((sum, dist) => sum + dist, 0);
// Choose next centroid with probability proportional to distance
let r = Math.random() * distSum;
let nextIndex = 0;
for (let j = 0; j < distances.length; j++) {
r -= distances[j];
if (r <= 0) {
nextIndex = j;
break;
}
}
centroids.push([...vectors[nextIndex]]);
}
return centroids;
}
/**
* Get the centroids for each subvector
* @returns Array of centroid arrays
*/
getCentroids() {
return this.centroids;
}
/**
* Set the centroids for each subvector
* @param centroids Array of centroid arrays
*/
setCentroids(centroids) {
this.centroids = centroids;
this.numSubvectors = centroids.length;
this.numCentroids = centroids[0].length;
this.initialized = true;
}
/**
* Get the dimension of the vectors
* @returns Dimension
*/
getDimension() {
return this.dimension;
}
/**
* Set the dimension of the vectors
* @param dimension Dimension
*/
setDimension(dimension) {
this.dimension = dimension;
this.subvectorSize = Math.ceil(dimension / this.numSubvectors);
}
}
/**
* Optimized HNSW Index implementation
* Extends the base HNSW implementation with support for large datasets
* Uses product quantization for dimensionality reduction and disk-based storage when needed
*/
export class HNSWIndexOptimized extends HNSWIndex {
constructor(config = {}, distanceFunction, storage = null) {
// Initialize base HNSW index with standard config
super(config, distanceFunction);
this.productQuantizer = null;
this.storage = null;
this.useDiskBasedIndex = false;
this.useProductQuantization = false;
this.quantizedVectors = new Map();
this.memoryUsage = 0;
this.vectorCount = 0;
// Thread safety for memory usage tracking
this.memoryUpdateLock = Promise.resolve();
// Set optimized config
this.optimizedConfig = { ...DEFAULT_OPTIMIZED_CONFIG, ...config };
// Set storage adapter
this.storage = storage;
// Initialize product quantizer if enabled
if (this.optimizedConfig.productQuantization?.enabled) {
this.useProductQuantization = true;
this.productQuantizer = new ProductQuantizer(this.optimizedConfig.productQuantization.numSubvectors, this.optimizedConfig.productQuantization.numCentroids);
}
// Set disk-based index flag
this.useDiskBasedIndex = this.optimizedConfig.useDiskBasedIndex || false;
}
/**
* Thread-safe method to update memory usage
* @param memoryDelta Change in memory usage (can be negative)
* @param vectorCountDelta Change in vector count (can be negative)
*/
async updateMemoryUsage(memoryDelta, vectorCountDelta) {
this.memoryUpdateLock = this.memoryUpdateLock.then(async () => {
this.memoryUsage = Math.max(0, this.memoryUsage + memoryDelta);
this.vectorCount = Math.max(0, this.vectorCount + vectorCountDelta);
});
await this.memoryUpdateLock;
}
/**
* Thread-safe method to get current memory usage
* @returns Current memory usage and vector count
*/
async getMemoryUsageAsync() {
await this.memoryUpdateLock;
return {
memoryUsage: this.memoryUsage,
vectorCount: this.vectorCount
};
}
/**
* Add a vector to the index
* Uses product quantization if enabled and memory threshold is exceeded
*/
async addItem(item) {
// Check if item is defined
if (!item) {
throw new Error('Item is undefined or null');
}
const { id, vector } = item;
// Check if vector is defined
if (!vector) {
throw new Error('Vector is undefined or null');
}
// Estimate memory usage for this vector
const vectorMemory = vector.length * 8; // 8 bytes per number (Float64)
const connectionsMemory = this.optimizedConfig.M * this.optimizedConfig.ml * 16; // Estimate for connections
const totalMemory = vectorMemory + connectionsMemory;
// Update memory usage estimate (thread-safe)
await this.updateMemoryUsage(totalMemory, 1);
// Check if we should switch to product quantization
const currentMemoryUsage = await this.getMemoryUsageAsync();
if (this.useProductQuantization &&
currentMemoryUsage.memoryUsage > this.optimizedConfig.memoryThreshold &&
this.productQuantizer &&
!this.productQuantizer.getDimension()) {
// Initialize product quantizer with existing vectors
this.initializeProductQuantizer();
}
// If product quantization is active, quantize the vector
if (this.useProductQuantization &&
this.productQuantizer &&
this.productQuantizer.getDimension() > 0) {
// Quantize the vector
const codes = this.productQuantizer.quantize(vector);
// Store the quantized vector
this.quantizedVectors.set(id, codes);
// Reconstruct the vector for indexing
const reconstructedVector = this.productQuantizer.reconstruct(codes);
// Add the reconstructed vector to the index
return await super.addItem({ id, vector: reconstructedVector });
}
// If disk-based index is active and storage is available, store the vector
if (this.useDiskBasedIndex && this.storage) {
// Create a noun object
const noun = {
id,
vector,
connections: new Map(),
level: 0
};
// Store the noun
this.storage.saveNoun(noun).catch((error) => {
console.error(`Failed to save noun ${id} to storage:`, error);
});
}
// Add the vector to the in-memory index
return await super.addItem(item);
}
/**
* Search for nearest neighbors
* Uses product quantization if enabled
*/
async search(queryVector, k = 10) {
// Check if query vector is defined
if (!queryVector) {
throw new Error('Query vector is undefined or null');
}
// If product quantization is active, quantize the query vector
if (this.useProductQuantization &&
this.productQuantizer &&
this.productQuantizer.getDimension() > 0) {
// Quantize the query vector
const codes = this.productQuantizer.quantize(queryVector);
// Reconstruct the query vector
const reconstructedVector = this.productQuantizer.reconstruct(codes);
// Search with the reconstructed vector
return await super.search(reconstructedVector, k);
}
// Otherwise, use the standard search
return await super.search(queryVector, k);
}
/**
* Remove an item from the index
*/
removeItem(id) {
// If product quantization is active, remove the quantized vector
if (this.useProductQuantization) {
this.quantizedVectors.delete(id);
}
// If disk-based index is active and storage is available, remove the vector from storage
if (this.useDiskBasedIndex && this.storage) {
this.storage.deleteNoun(id).catch((error) => {
console.error(`Failed to delete noun ${id} from storage:`, error);
});
}
// Update memory usage estimate (async operation, but don't block removal)
this.getMemoryUsageAsync().then((currentMemoryUsage) => {
if (currentMemoryUsage.vectorCount > 0) {
const memoryPerVector = currentMemoryUsage.memoryUsage / currentMemoryUsage.vectorCount;
this.updateMemoryUsage(-memoryPerVector, -1);
}
}).catch((error) => {
console.error('Failed to update memory usage after removal:', error);
});
// Remove the item from the in-memory index
return super.removeItem(id);
}
/**
* Clear the index
*/
async clear() {
// Clear product quantization data
if (this.useProductQuantization) {
this.quantizedVectors.clear();
this.productQuantizer = new ProductQuantizer(this.optimizedConfig.productQuantization.numSubvectors, this.optimizedConfig.productQuantization.numCentroids);
}
// Reset memory usage (thread-safe)
const currentMemoryUsage = await this.getMemoryUsageAsync();
await this.updateMemoryUsage(-currentMemoryUsage.memoryUsage, -currentMemoryUsage.vectorCount);
// Clear the in-memory index
super.clear();
}
/**
* Initialize product quantizer with existing vectors
*/
initializeProductQuantizer() {
if (!this.productQuantizer) {
return;
}
// Get all vectors from the index
const nouns = super.getNouns();
const vectors = [];
// Extract vectors
for (const [_, noun] of nouns) {
vectors.push(noun.vector);
}
// Train the product quantizer
if (vectors.length > 0) {
this.productQuantizer.train(vectors);
// Quantize all existing vectors
for (const [id, noun] of nouns) {
const codes = this.productQuantizer.quantize(noun.vector);
this.quantizedVectors.set(id, codes);
}
console.log(`Initialized product quantizer with ${vectors.length} vectors`);
}
}
/**
* Get the product quantizer
* @returns Product quantizer or null if not enabled
*/
getProductQuantizer() {
return this.productQuantizer;
}
/**
* Get the optimized configuration
* @returns Optimized configuration
*/
getOptimizedConfig() {
return { ...this.optimizedConfig };
}
/**
* Get the estimated memory usage
* @returns Estimated memory usage in bytes
*/
getMemoryUsage() {
return this.memoryUsage;
}
/**
* Set the storage adapter
* @param storage Storage adapter
*/
setStorage(storage) {
this.storage = storage;
}
/**
* Get the storage adapter
* @returns Storage adapter or null if not set
*/
getStorage() {
return this.storage;
}
/**
* Set whether to use disk-based index
* @param useDiskBasedIndex Whether to use disk-based index
*/
setUseDiskBasedIndex(useDiskBasedIndex) {
this.useDiskBasedIndex = useDiskBasedIndex;
}
/**
* Get whether disk-based index is used
* @returns Whether disk-based index is used
*/
getUseDiskBasedIndex() {
return this.useDiskBasedIndex;
}
/**
* Set whether to use product quantization
* @param useProductQuantization Whether to use product quantization
*/
setUseProductQuantization(useProductQuantization) {
this.useProductQuantization = useProductQuantization;
}
/**
* Get whether product quantization is used
* @returns Whether product quantization is used
*/
getUseProductQuantization() {
return this.useProductQuantization;
}
}
//# sourceMappingURL=hnswIndexOptimized.js.map