22
33use crate :: flat:: FlatIndex ;
44use crate :: hnsw:: { HnswIndex , HnswParams } ;
5- use crate :: quantize :: sq8 :: Sq8Codec ;
5+ use crate :: index_config :: { IndexConfig , IndexType } ;
66
77use super :: segment:: { BuildRequest , BuildingSegment , DEFAULT_SEAL_THRESHOLD , SealedSegment } ;
88
@@ -40,6 +40,8 @@ pub struct VectorCollection {
4040 pub multi_doc_map : std:: collections:: HashMap < String , Vec < u32 > > ,
4141 /// Number of vectors in the growing segment before sealing.
4242 pub ( crate ) seal_threshold : usize ,
43+ /// Full index configuration (index type, PQ params, IVF params).
44+ pub ( crate ) index_config : IndexConfig ,
4345}
4446
4547impl VectorCollection {
@@ -50,6 +52,25 @@ impl VectorCollection {
5052
5153 /// Create an empty collection with an explicit seal threshold.
5254 pub fn with_seal_threshold ( dim : usize , params : HnswParams , seal_threshold : usize ) -> Self {
55+ let index_config = IndexConfig {
56+ hnsw : params. clone ( ) ,
57+ ..IndexConfig :: default ( )
58+ } ;
59+ Self :: with_seal_threshold_and_config ( dim, index_config, seal_threshold)
60+ }
61+
62+ /// Create an empty collection with a full index configuration.
63+ pub fn with_index_config ( dim : usize , config : IndexConfig ) -> Self {
64+ Self :: with_seal_threshold_and_config ( dim, config, DEFAULT_SEAL_THRESHOLD )
65+ }
66+
67+ /// Create an empty collection with a full index config and custom seal threshold.
68+ pub fn with_seal_threshold_and_config (
69+ dim : usize ,
70+ config : IndexConfig ,
71+ seal_threshold : usize ,
72+ ) -> Self {
73+ let params = config. hnsw . clone ( ) ;
5374 Self {
5475 growing : FlatIndex :: new ( dim, params. metric ) ,
5576 growing_base_id : 0 ,
@@ -66,6 +87,7 @@ impl VectorCollection {
6687 doc_id_map : std:: collections:: HashMap :: new ( ) ,
6788 multi_doc_map : std:: collections:: HashMap :: new ( ) ,
6889 seal_threshold,
90+ index_config : config,
6991 }
7092 }
7193
@@ -213,53 +235,28 @@ impl VectorCollection {
213235 . position ( |b| b. segment_id == segment_id)
214236 {
215237 let building = self . building . remove ( pos) ;
216- let sq8 = Self :: build_sq8_for_index ( & index) ;
238+ let use_pq = self . index_config . index_type == IndexType :: HnswPq ;
239+ let ( sq8, pq) = if use_pq {
240+ (
241+ None ,
242+ Self :: build_pq_for_index ( & index, self . index_config . pq_m ) ,
243+ )
244+ } else {
245+ ( Self :: build_sq8_for_index ( & index) , None )
246+ } ;
217247 let ( tier, mmap_vectors) = self . resolve_tier_for_build ( segment_id, & index) ;
218248
219249 self . sealed . push ( SealedSegment {
220250 index,
221251 base_id : building. base_id ,
222252 sq8,
253+ pq,
223254 tier,
224255 mmap_vectors,
225256 } ) ;
226257 }
227258 }
228259
229- /// Build SQ8 quantized data for an HNSW index.
230- pub fn build_sq8_for_index ( index : & HnswIndex ) -> Option < ( Sq8Codec , Vec < u8 > ) > {
231- if index. live_count ( ) < 1000 {
232- return None ;
233- }
234- let dim = index. dim ( ) ;
235- let n = index. len ( ) ;
236-
237- let mut refs: Vec < & [ f32 ] > = Vec :: with_capacity ( n) ;
238- for i in 0 ..n {
239- if !index. is_deleted ( i as u32 )
240- && let Some ( v) = index. get_vector ( i as u32 )
241- {
242- refs. push ( v) ;
243- }
244- }
245- if refs. is_empty ( ) {
246- return None ;
247- }
248-
249- let codec = Sq8Codec :: calibrate ( & refs, dim) ;
250-
251- let mut data = Vec :: with_capacity ( dim * n) ;
252- for i in 0 ..n {
253- if let Some ( v) = index. get_vector ( i as u32 ) {
254- data. extend ( codec. quantize ( v) ) ;
255- } else {
256- data. extend ( vec ! [ 0u8 ; dim] ) ;
257- }
258- }
259-
260- Some ( ( codec, data) )
261- }
262-
263260 /// Access sealed segments (read-only).
264261 pub fn sealed_segments ( & self ) -> & [ SealedSegment ] {
265262 & self . sealed
@@ -276,10 +273,64 @@ impl VectorCollection {
276273 }
277274
278275 /// Compact sealed segments by removing tombstoned nodes.
276+ ///
277+ /// Rewrites `doc_id_map` and `multi_doc_map` for every sealed segment
278+ /// so that global ids continue to resolve to the correct document
279+ /// strings after local-id renumbering.
279280 pub fn compact ( & mut self ) -> usize {
280281 let mut total_removed = 0 ;
281282 for seg in & mut self . sealed {
282- total_removed += seg. index . compact ( ) ;
283+ let base_id = seg. base_id ;
284+ let ( removed, id_map) = seg. index . compact_with_map ( ) ;
285+ total_removed += removed;
286+ if removed == 0 {
287+ continue ;
288+ }
289+
290+ // Rebuild doc_id_map for entries in [base_id, base_id + id_map.len()).
291+ let segment_end = base_id as u64 + id_map. len ( ) as u64 ;
292+ let doc_keys: Vec < u32 > = self
293+ . doc_id_map
294+ . keys ( )
295+ . copied ( )
296+ . filter ( |& k| ( k as u64 ) >= base_id as u64 && ( k as u64 ) < segment_end)
297+ . collect ( ) ;
298+ // Two-phase: remove all old entries first, then insert new ones so
299+ // we don't clobber a freshly-remapped entry with a later tombstone
300+ // removal.
301+ let mut new_entries: Vec < ( u32 , String ) > = Vec :: with_capacity ( doc_keys. len ( ) ) ;
302+ for old_global in & doc_keys {
303+ let doc = self . doc_id_map . remove ( old_global) ;
304+ let old_local = ( old_global - base_id) as usize ;
305+ let new_local = id_map[ old_local] ;
306+ if new_local != u32:: MAX
307+ && let Some ( doc) = doc
308+ {
309+ new_entries. push ( ( base_id + new_local, doc) ) ;
310+ }
311+ }
312+ for ( k, v) in new_entries {
313+ self . doc_id_map . insert ( k, v) ;
314+ }
315+
316+ // Rewrite multi_doc_map entries for this segment.
317+ for ids in self . multi_doc_map . values_mut ( ) {
318+ ids. retain_mut ( |vid| {
319+ let v = * vid;
320+ if ( v as u64 ) >= base_id as u64 && ( v as u64 ) < segment_end {
321+ let old_local = ( v - base_id) as usize ;
322+ let new_local = id_map[ old_local] ;
323+ if new_local == u32:: MAX {
324+ false
325+ } else {
326+ * vid = base_id + new_local;
327+ true
328+ }
329+ } else {
330+ true
331+ }
332+ } ) ;
333+ }
283334 }
284335 total_removed
285336 }
@@ -382,12 +433,20 @@ impl VectorCollection {
382433 0.0
383434 } ;
384435
385- let quantization = if self . sealed . iter ( ) . any ( |s| s. sq8 . is_some ( ) ) {
436+ let quantization = if self . sealed . iter ( ) . any ( |s| s. pq . is_some ( ) ) {
437+ nodedb_types:: VectorIndexQuantization :: Pq
438+ } else if self . sealed . iter ( ) . any ( |s| s. sq8 . is_some ( ) ) {
386439 nodedb_types:: VectorIndexQuantization :: Sq8
387440 } else {
388441 nodedb_types:: VectorIndexQuantization :: None
389442 } ;
390443
444+ let index_type = match self . index_config . index_type {
445+ IndexType :: HnswPq => nodedb_types:: VectorIndexType :: HnswPq ,
446+ IndexType :: IvfPq => nodedb_types:: VectorIndexType :: IvfPq ,
447+ IndexType :: Hnsw => nodedb_types:: VectorIndexType :: Hnsw ,
448+ } ;
449+
391450 let hnsw_mem: usize = self
392451 . sealed
393452 . iter ( )
@@ -422,7 +481,7 @@ impl VectorCollection {
422481 memory_bytes,
423482 disk_bytes,
424483 build_in_progress : !self . building . is_empty ( ) ,
425- index_type : nodedb_types :: VectorIndexType :: Hnsw ,
484+ index_type,
426485 hnsw_m : self . params . m ,
427486 hnsw_m0 : self . params . m0 ,
428487 hnsw_ef_construction : self . params . ef_construction ,
0 commit comments