/** * Read-Only Storage Optimizations for Production Deployments * Implements compression, memory-mapping, and pre-built index segments */ import { HNSWNoun, HNSWVerb, Vector } from '../coreTypes.js' // Compression types supported enum CompressionType { NONE = 'none', GZIP = 'gzip', BROTLI = 'brotli', QUANTIZATION = 'quantization', HYBRID = 'hybrid' } // Vector quantization methods enum QuantizationType { SCALAR = 'scalar', // 8-bit scalar quantization PRODUCT = 'product', // Product quantization BINARY = 'binary' // Binary quantization } interface CompressionConfig { vectorCompression: CompressionType metadataCompression: CompressionType quantizationType?: QuantizationType quantizationBits?: number compressionLevel?: number } interface ReadOnlyConfig { prebuiltIndexPath?: string memoryMapped?: boolean compression: CompressionConfig segmentSize?: number // For index segmentation prefetchSegments?: number cacheIndexInMemory?: boolean } interface IndexSegment { id: string nodeCount: number vectorDimension: number compression: CompressionType s3Key?: string localPath?: string loadedInMemory: boolean lastAccessed: number } /** * Read-only storage optimizations for high-performance production deployments */ export class ReadOnlyOptimizations { private config: Required private segments: Map = new Map() private compressionStats = { originalSize: 0, compressedSize: 0, compressionRatio: 0, decompressionTime: 0 } // Quantization codebooks for vector compression private quantizationCodebooks: Map = new Map() // Memory-mapped buffers for large datasets private memoryMappedBuffers: Map = new Map() constructor(config: Partial = {}) { this.config = { prebuiltIndexPath: '', memoryMapped: true, compression: { vectorCompression: CompressionType.QUANTIZATION, metadataCompression: CompressionType.GZIP, quantizationType: QuantizationType.SCALAR, quantizationBits: 8, compressionLevel: 6 }, segmentSize: 10000, // 10k nodes per segment prefetchSegments: 3, cacheIndexInMemory: false, ...config } if (config.compression) { this.config.compression = { ...this.config.compression, ...config.compression } } } /** * Compress vector data using specified compression method */ public async compressVector(vector: Vector, segmentId: string): Promise { const startTime = Date.now() let compressedData: ArrayBuffer switch (this.config.compression.vectorCompression) { case CompressionType.QUANTIZATION: compressedData = await this.quantizeVector(vector, segmentId) break case CompressionType.GZIP: compressedData = await this.gzipCompress(new Float32Array(vector).buffer) break case CompressionType.BROTLI: compressedData = await this.brotliCompress(new Float32Array(vector).buffer) break case CompressionType.HYBRID: // First quantize, then compress const quantized = await this.quantizeVector(vector, segmentId) compressedData = await this.gzipCompress(quantized) break default: compressedData = new Float32Array(vector).buffer break } // Update compression statistics const originalSize = vector.length * 4 // 4 bytes per float32 this.compressionStats.originalSize += originalSize this.compressionStats.compressedSize += compressedData.byteLength this.compressionStats.decompressionTime += Date.now() - startTime this.updateCompressionRatio() return compressedData } /** * Decompress vector data */ public async decompressVector( compressedData: ArrayBuffer, segmentId: string, originalDimension: number ): Promise { switch (this.config.compression.vectorCompression) { case CompressionType.QUANTIZATION: return this.dequantizeVector(compressedData, segmentId, originalDimension) case CompressionType.GZIP: const gzipDecompressed = await this.gzipDecompress(compressedData) return Array.from(new Float32Array(gzipDecompressed)) case CompressionType.BROTLI: const brotliDecompressed = await this.brotliDecompress(compressedData) return Array.from(new Float32Array(brotliDecompressed)) case CompressionType.HYBRID: const gzipStage = await this.gzipDecompress(compressedData) return this.dequantizeVector(gzipStage, segmentId, originalDimension) default: return Array.from(new Float32Array(compressedData)) } } /** * Scalar quantization of vectors to 8-bit integers */ private async quantizeVector(vector: Vector, segmentId: string): Promise { let codebook = this.quantizationCodebooks.get(segmentId) if (!codebook) { // Create codebook (min/max values for scaling) const min = Math.min(...vector) const max = Math.max(...vector) codebook = new Float32Array([min, max]) this.quantizationCodebooks.set(segmentId, codebook) } const [min, max] = codebook const scale = (max - min) / 255 // 8-bit quantization const quantized = new Uint8Array(vector.length) for (let i = 0; i < vector.length; i++) { quantized[i] = Math.round((vector[i] - min) / scale) } // Store codebook with quantized data const result = new ArrayBuffer(quantized.byteLength + codebook.byteLength) const resultView = new Uint8Array(result) // First 8 bytes: codebook (min, max as float32) resultView.set(new Uint8Array(codebook.buffer), 0) // Remaining bytes: quantized vector resultView.set(quantized, codebook.byteLength) return result } /** * Dequantize 8-bit vectors back to float32 */ private dequantizeVector( quantizedData: ArrayBuffer, segmentId: string, dimension: number ): Vector { const dataView = new Uint8Array(quantizedData) // Extract codebook (first 8 bytes) const codebookBytes = dataView.slice(0, 8) const codebook = new Float32Array(codebookBytes.buffer) const [min, max] = codebook // Extract quantized vector const quantized = dataView.slice(8) const scale = (max - min) / 255 const result: Vector = [] for (let i = 0; i < dimension; i++) { result[i] = min + quantized[i] * scale } return result } /** * GZIP compression using browser/Node.js APIs */ private async gzipCompress(data: ArrayBuffer): Promise { if (typeof CompressionStream !== 'undefined') { // Browser environment const stream = new CompressionStream('gzip') const writer = stream.writable.getWriter() const reader = stream.readable.getReader() writer.write(new Uint8Array(data)) writer.close() const chunks: Uint8Array[] = [] let result = await reader.read() while (!result.done) { chunks.push(result.value) result = await reader.read() } // Combine chunks const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0) const combined = new Uint8Array(totalLength) let offset = 0 for (const chunk of chunks) { combined.set(chunk, offset) offset += chunk.length } return combined.buffer } else { // Node.js environment - would use zlib console.warn('GZIP compression not available, returning original data') return data } } /** * GZIP decompression */ private async gzipDecompress(compressedData: ArrayBuffer): Promise { if (typeof DecompressionStream !== 'undefined') { // Browser environment const stream = new DecompressionStream('gzip') const writer = stream.writable.getWriter() const reader = stream.readable.getReader() writer.write(new Uint8Array(compressedData)) writer.close() const chunks: Uint8Array[] = [] let result = await reader.read() while (!result.done) { chunks.push(result.value) result = await reader.read() } // Combine chunks const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0) const combined = new Uint8Array(totalLength) let offset = 0 for (const chunk of chunks) { combined.set(chunk, offset) offset += chunk.length } return combined.buffer } else { console.warn('GZIP decompression not available, returning original data') return compressedData } } /** * Brotli compression (placeholder - similar to GZIP) */ private async brotliCompress(data: ArrayBuffer): Promise { // Would implement Brotli compression here console.warn('Brotli compression not implemented, falling back to GZIP') return this.gzipCompress(data) } /** * Brotli decompression (placeholder) */ private async brotliDecompress(compressedData: ArrayBuffer): Promise { console.warn('Brotli decompression not implemented, falling back to GZIP') return this.gzipDecompress(compressedData) } /** * Create prebuilt index segments for faster loading */ public async createPrebuiltSegments( nodes: HNSWNoun[], outputPath: string ): Promise { const segments: IndexSegment[] = [] const segmentSize = this.config.segmentSize console.log(`Creating ${Math.ceil(nodes.length / segmentSize)} prebuilt segments`) for (let i = 0; i < nodes.length; i += segmentSize) { const segmentNodes = nodes.slice(i, i + segmentSize) const segmentId = `segment_${Math.floor(i / segmentSize)}` const segment: IndexSegment = { id: segmentId, nodeCount: segmentNodes.length, vectorDimension: segmentNodes[0]?.vector.length || 0, compression: this.config.compression.vectorCompression, localPath: `${outputPath}/${segmentId}.dat`, loadedInMemory: false, lastAccessed: 0 } // Compress and serialize segment data const compressedData = await this.compressSegment(segmentNodes) // In a real implementation, you would write this to disk/S3 console.log(`Created segment ${segmentId} with ${compressedData.byteLength} bytes`) segments.push(segment) this.segments.set(segmentId, segment) } return segments } /** * Compress an entire segment of nodes */ private async compressSegment(nodes: HNSWNoun[]): Promise { const serialized = JSON.stringify(nodes.map(node => ({ id: node.id, vector: node.vector, connections: this.serializeConnections(node.connections) }))) const encoder = new TextEncoder() const data = encoder.encode(serialized) // Apply metadata compression switch (this.config.compression.metadataCompression) { case CompressionType.GZIP: return this.gzipCompress(data.buffer) case CompressionType.BROTLI: return this.brotliCompress(data.buffer) default: return data.buffer } } /** * Load a segment from storage with caching */ public async loadSegment(segmentId: string): Promise { const segment = this.segments.get(segmentId) if (!segment) { throw new Error(`Segment ${segmentId} not found`) } segment.lastAccessed = Date.now() // Check if segment is already loaded in memory if (segment.loadedInMemory && this.memoryMappedBuffers.has(segmentId)) { return this.deserializeSegment(this.memoryMappedBuffers.get(segmentId)!) } // Load from storage (S3, disk, etc.) const compressedData = await this.loadSegmentFromStorage(segment) // Cache in memory if configured if (this.config.cacheIndexInMemory) { this.memoryMappedBuffers.set(segmentId, compressedData) segment.loadedInMemory = true } return this.deserializeSegment(compressedData) } /** * Load segment data from storage */ private async loadSegmentFromStorage(segment: IndexSegment): Promise { // This would integrate with your S3 storage adapter // For now, return a placeholder console.log(`Loading segment ${segment.id} from storage`) return new ArrayBuffer(0) } /** * Deserialize and decompress segment data */ private async deserializeSegment(compressedData: ArrayBuffer): Promise { // Decompress metadata let decompressed: ArrayBuffer switch (this.config.compression.metadataCompression) { case CompressionType.GZIP: decompressed = await this.gzipDecompress(compressedData) break case CompressionType.BROTLI: decompressed = await this.brotliDecompress(compressedData) break default: decompressed = compressedData break } // Parse JSON const decoder = new TextDecoder() const jsonStr = decoder.decode(decompressed) const parsed = JSON.parse(jsonStr) // Reconstruct HNSWNoun objects return parsed.map((item: any) => ({ id: item.id, vector: item.vector, connections: this.deserializeConnections(item.connections) })) } /** * Serialize connections Map for storage */ private serializeConnections(connections: Map>): Record { const result: Record = {} for (const [level, nodeIds] of connections.entries()) { result[level.toString()] = Array.from(nodeIds) } return result } /** * Deserialize connections from storage format */ private deserializeConnections(serialized: Record): Map> { const result = new Map>() for (const [levelStr, nodeIds] of Object.entries(serialized)) { result.set(parseInt(levelStr), new Set(nodeIds)) } return result } /** * Prefetch segments based on access patterns */ public async prefetchSegments(currentSegmentId: string): Promise { const segment = this.segments.get(currentSegmentId) if (!segment) return // Simple prefetching strategy - load adjacent segments const segmentNumber = parseInt(currentSegmentId.split('_')[1]) const toPrefetch: string[] = [] for (let i = 1; i <= this.config.prefetchSegments; i++) { const nextId = `segment_${segmentNumber + i}` const prevId = `segment_${segmentNumber - i}` if (this.segments.has(nextId) && !this.memoryMappedBuffers.has(nextId)) { toPrefetch.push(nextId) } if (this.segments.has(prevId) && !this.memoryMappedBuffers.has(prevId)) { toPrefetch.push(prevId) } } // Prefetch in background for (const segmentId of toPrefetch) { this.loadSegment(segmentId).catch(error => { console.warn(`Failed to prefetch segment ${segmentId}:`, error) }) } } /** * Update compression statistics */ private updateCompressionRatio(): void { if (this.compressionStats.originalSize > 0) { this.compressionStats.compressionRatio = this.compressionStats.compressedSize / this.compressionStats.originalSize } } /** * Get compression statistics */ public getCompressionStats(): typeof this.compressionStats & { segmentCount: number memoryUsage: number } { const memoryUsage = Array.from(this.memoryMappedBuffers.values()) .reduce((sum, buffer) => sum + buffer.byteLength, 0) return { ...this.compressionStats, segmentCount: this.segments.size, memoryUsage } } /** * Cleanup memory-mapped buffers */ public cleanup(): void { this.memoryMappedBuffers.clear() this.quantizationCodebooks.clear() // Mark all segments as not loaded for (const segment of this.segments.values()) { segment.loadedInMemory = false } } }