@@ -98,6 +98,9 @@ type HybridCacheOptions struct {
9898
9999 // Milvus settings
100100 MilvusConfigPath string
101+
102+ // Startup settings
103+ DisableRebuildOnStartup bool // Skip rebuilding HNSW index from Milvus on startup (default: false, meaning rebuild IS enabled)
101104}
102105
103106// NewHybridCache creates a new hybrid cache instance
@@ -153,6 +156,26 @@ func NewHybridCache(options HybridCacheOptions) (*HybridCache, error) {
153156 observability .Infof ("Hybrid cache initialized: HNSW(M=%d, ef=%d), maxMemory=%d" ,
154157 options .HNSWM , options .HNSWEfConstruction , options .MaxMemoryEntries )
155158
159+ // Rebuild HNSW index from Milvus on startup (enabled by default)
160+ // This ensures the in-memory index is populated after a restart
161+ // Set DisableRebuildOnStartup=true to skip this step (not recommended for production)
162+ if ! options .DisableRebuildOnStartup {
163+ observability .Infof ("Hybrid cache: rebuilding HNSW index from Milvus..." )
164+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Minute )
165+ defer cancel ()
166+
167+ if err := cache .RebuildFromMilvus (ctx ); err != nil {
168+ observability .Warnf ("Hybrid cache: failed to rebuild HNSW index from Milvus: %v" , err )
169+ observability .Warnf ("Hybrid cache: continuing with empty HNSW index" )
170+ // Don't fail initialization, just log warning and continue with empty index
171+ } else {
172+ observability .Infof ("Hybrid cache: HNSW index rebuild complete" )
173+ }
174+ } else {
175+ observability .Warnf ("Hybrid cache: skipping HNSW index rebuild (DisableRebuildOnStartup=true)" )
176+ observability .Warnf ("Hybrid cache: index will be empty until entries are added" )
177+ }
178+
156179 return cache , nil
157180}
158181
@@ -161,6 +184,83 @@ func (h *HybridCache) IsEnabled() bool {
161184 return h .enabled
162185}
163186
187+ // RebuildFromMilvus rebuilds the in-memory HNSW index from persistent Milvus storage
188+ // This is called on startup to recover the index after a restart
189+ func (h * HybridCache ) RebuildFromMilvus (ctx context.Context ) error {
190+ if ! h .enabled {
191+ return nil
192+ }
193+
194+ start := time .Now ()
195+ observability .Infof ("HybridCache.RebuildFromMilvus: starting HNSW index rebuild from Milvus" )
196+
197+ // Query all entries from Milvus
198+ requestIDs , embeddings , err := h .milvusCache .GetAllEntries (ctx )
199+ if err != nil {
200+ return fmt .Errorf ("failed to get entries from Milvus: %w" , err )
201+ }
202+
203+ if len (requestIDs ) == 0 {
204+ observability .Infof ("HybridCache.RebuildFromMilvus: no entries to rebuild, starting with empty index" )
205+ return nil
206+ }
207+
208+ observability .Infof ("HybridCache.RebuildFromMilvus: rebuilding HNSW index with %d entries" , len (requestIDs ))
209+
210+ // Lock for the entire rebuild process
211+ h .mu .Lock ()
212+ defer h .mu .Unlock ()
213+
214+ // Clear existing index
215+ h .embeddings = make ([][]float32 , 0 , len (embeddings ))
216+ h .idMap = make (map [int ]string )
217+ h .hnswIndex = newHNSWIndex (h .hnswIndex .M , h .hnswIndex .efConstruction )
218+
219+ // Rebuild HNSW index with progress logging
220+ batchSize := 1000
221+ for i , embedding := range embeddings {
222+ // Check memory limits
223+ if len (h .embeddings ) >= h .maxMemoryEntries {
224+ observability .Warnf ("HybridCache.RebuildFromMilvus: reached max memory entries (%d), stopping rebuild at %d/%d" ,
225+ h .maxMemoryEntries , i , len (embeddings ))
226+ break
227+ }
228+
229+ // Add to HNSW
230+ entryIndex := len (h .embeddings )
231+ h .embeddings = append (h .embeddings , embedding )
232+ h .idMap [entryIndex ] = requestIDs [i ]
233+ h .addNodeHybrid (entryIndex , embedding )
234+
235+ // Progress logging for large datasets
236+ if (i + 1 )% batchSize == 0 {
237+ elapsed := time .Since (start )
238+ rate := float64 (i + 1 ) / elapsed .Seconds ()
239+ remaining := len (embeddings ) - (i + 1 )
240+ eta := time .Duration (float64 (remaining )/ rate ) * time .Second
241+ observability .Infof ("HybridCache.RebuildFromMilvus: progress %d/%d (%.1f%%, %.0f entries/sec, ETA: %v)" ,
242+ i + 1 , len (embeddings ), float64 (i + 1 )/ float64 (len (embeddings ))* 100 , rate , eta )
243+ }
244+ }
245+
246+ elapsed := time .Since (start )
247+ rate := float64 (len (h .embeddings )) / elapsed .Seconds ()
248+ observability .Infof ("HybridCache.RebuildFromMilvus: rebuild complete - %d entries in %v (%.0f entries/sec)" ,
249+ len (h .embeddings ), elapsed , rate )
250+
251+ observability .LogEvent ("hybrid_cache_rebuilt" , map [string ]interface {}{
252+ "backend" : "hybrid" ,
253+ "entries_loaded" : len (h .embeddings ),
254+ "entries_in_milvus" : len (embeddings ),
255+ "duration_seconds" : elapsed .Seconds (),
256+ "entries_per_sec" : rate ,
257+ })
258+
259+ metrics .UpdateCacheEntries ("hybrid" , len (h .embeddings ))
260+
261+ return nil
262+ }
263+
164264// AddPendingRequest stores a request awaiting its response
165265func (h * HybridCache ) AddPendingRequest (requestID string , model string , query string , requestBody []byte ) error {
166266 start := time .Now ()
0 commit comments