1#[path = "calibration.rs"]
2mod calibration;
3#[path = "planner.rs"]
4mod planner;
5#[path = "policy.rs"]
6mod policy;
7#[path = "retrieval.rs"]
8mod retrieval;
9use crate::{
10 analyzer::{Analyzer, TextAnalyzer},
11 fde::{MaxSimQuery, Vector, dot, maxsim_flat, normalize},
12 muvera::FdeEncoder,
13 storage::{
14 CompressedVectorStore, FixedVectorStore, ObjectLocation, atomic_write, commit_boundary,
15 record_bytes, verify_record,
16 },
17};
18use annex::{
19 Filter, Payload,
20 payload_storage::stores::PayloadIndex,
21 utils::types::DistanceMetric,
22 vector::hnsw::{HNSWIndex, SearchRuntimeOptions},
23};
24pub use calibration::{CalibrationEntry, CalibrationKey, CalibrationSnapshot, CalibrationTarget};
25pub use planner::{
26 ContextOperator, ContextPlan, FieldStats, FilterStats, FilterStrategy, FusionOperator,
27 LogicalChannel, LogicalChannelKind, LogicalFusion, LogicalPlan, PhysicalOperator, PlanEstimate,
28 PlanReason, PlanStage, PlannedChannel, PlannerStats, QualityPreference, RepresentationKind,
29 RerankPlan, RetrievalObjective, RetrievalPlan,
30};
31pub use policy::{PlanningMode, PolicyPlan, QueryIntent, QueryRepresentations};
32use rayon::prelude::*;
33pub use retrieval::{
34 AdaptiveRerank, Channel, Chunk, ContextHit, ContextOptions, Fusion, Predicate, RankingSignals,
35 Representation, Rerank, RetrievalDocument, RetrievalResponse, RetrievalTrace, RetrieveRequest,
36};
37use retrieval::{DocSet, FieldSchema, Fields, RetrievalState};
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40use std::sync::Arc;
41use std::{
42 collections::{BTreeMap, HashMap, HashSet},
43 fs::{self, File, OpenOptions},
44 io,
45 path::{Path, PathBuf},
46 sync::{Mutex, RwLock},
47};
48use thiserror::Error;
49
50#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
51#[serde(deny_unknown_fields)]
52pub struct IndexConfig {
53 pub dimension: usize,
54 #[serde(default = "default_centroids")]
55 pub centroids: usize,
56 #[serde(default = "default_bits")]
57 pub residual_bits: u8,
58 #[serde(default = "default_probes")]
59 pub probes: usize,
60 #[serde(default = "default_fde_repetitions")]
61 pub fde_repetitions: usize,
62 #[serde(default = "default_fde_ksim")]
63 pub fde_ksim: usize,
64 #[serde(default = "default_fde_projected")]
65 pub fde_projected: usize,
66 #[serde(default)]
69 pub analyzer: TextAnalyzer,
70}
71fn default_centroids() -> usize {
72 64
73}
74fn default_bits() -> u8 {
75 2
76}
77fn default_probes() -> usize {
78 4
79}
80fn default_fde_repetitions() -> usize {
81 20
82}
83fn default_fde_ksim() -> usize {
84 4
85}
86fn default_fde_projected() -> usize {
87 8
88}
89impl IndexConfig {
90 pub fn new(dimension: usize) -> Self {
91 Self {
92 dimension,
93 centroids: 64,
94 residual_bits: 2,
95 probes: 4,
96 fde_repetitions: 20,
97 fde_ksim: 4,
98 fde_projected: 8,
99 analyzer: TextAnalyzer::plain(),
100 }
101 }
102}
103
104#[derive(Clone, Debug, Deserialize, Serialize)]
105struct DocumentRecord {
106 centroid_ids: Vec<u32>,
107 unique_centroids: Vec<u32>,
108 location: ObjectLocation,
109 fde_location: ObjectLocation,
110 metadata: Value,
111 tokens: usize,
112 compressed_bytes: u64,
113 #[serde(default)]
114 fields: Arc<Fields>,
115 #[serde(default)]
116 storage_id: u64,
117}
118const FORMAT_VERSION: u32 = 3;
125
126#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
130pub enum Durability {
131 #[default]
132 Fsync,
133 Buffered,
134}
135
136#[derive(Clone, Copy, Deserialize, Serialize)]
137struct SegmentBoundaries {
138 objects: u64,
139 fde: u64,
140}
141
142#[derive(Deserialize, Serialize)]
145struct ManifestEnvelope {
146 format_version: u32,
147 checksum_blake3: String,
148 manifest: String,
149}
150
151#[derive(Deserialize, Serialize)]
152struct Manifest {
153 #[serde(default = "current_format_version")]
154 format_version: u32,
155 #[serde(default)]
156 generation: u64,
157 #[serde(default = "legacy_fde_encoding_version")]
158 fde_encoding_version: u32,
159 config: IndexConfig,
160 codebook: Vec<Vector>,
161 residual_codebook: Vec<f32>,
162 documents: HashMap<String, DocumentRecord>,
163 #[serde(default)]
166 representation_schema: BTreeMap<String, FieldSchema>,
167 #[serde(default)]
168 segments: Option<SegmentBoundaries>,
169 #[serde(default)]
170 storage_generation: Option<u64>,
171 #[serde(default)]
172 sealed: Vec<(u64, SegmentBoundaries)>,
173}
174
175fn legacy_fde_encoding_version() -> u32 {
176 1
177}
178
179fn current_format_version() -> u32 {
180 1 }
182
183fn verify_manifest_checksum(manifest_bytes: &[u8], sidecar_path: &Path) -> Result<(), IndexError> {
187 if !sidecar_path.exists() {
188 return Ok(());
189 }
190 let expected = fs::read_to_string(sidecar_path)?;
191 let expected = expected.trim();
192 let actual = blake3::hash(manifest_bytes);
193 if actual.to_hex().as_str() != expected {
194 return Err(IndexError::Invalid(format!(
195 "manifest.sha256 does not match manifest.json — index may be corrupt or torn: expected={expected}, actual={}",
196 actual.to_hex(),
197 )));
198 }
199 Ok(())
200}
201#[derive(Clone, Debug, Serialize, PartialEq)]
202pub struct Hit {
203 pub id: String,
204 pub score: f32,
205 #[serde(skip_serializing_if = "Option::is_none")]
210 pub fde_score: Option<f32>,
211 pub metadata: Value,
212}
213#[derive(Clone, Debug, Serialize, PartialEq)]
214pub struct CandidateHit {
215 pub id: String,
216 pub score: f32,
217}
218#[derive(Clone, Debug)]
219pub struct UpsertDocument {
220 pub id: String,
221 pub vectors: Vec<Vector>,
222 pub metadata: Value,
223}
224#[derive(Clone, Debug, Serialize, PartialEq)]
225pub struct IndexStats {
226 pub documents: usize,
227 pub generation: u64,
228 pub token_vectors: usize,
229 pub compressed_bytes: u64,
230 pub centroids: usize,
231 pub residual_bits: u8,
232 pub trained: bool,
233 pub fde_dimension: usize,
234 pub fde_ann_nodes: usize,
235 pub fde_ann_base_nodes: usize,
236 pub fde_ann_delta_documents: usize,
237 pub fde_ann_tombstones: usize,
238 pub fde_encoding_version: u32,
239 pub storage_segments: usize,
240 pub dense_ann_fields: HashMap<String, usize>,
241}
242#[derive(Debug, Error)]
243pub enum IndexError {
244 #[error("{0}")]
245 Invalid(String),
246 #[error("index configuration is {actual:?}, requested {requested:?}")]
247 Config {
248 actual: Box<IndexConfig>,
249 requested: Box<IndexConfig>,
250 },
251 #[error(transparent)]
252 Io(#[from] io::Error),
253 #[error(transparent)]
254 Json(#[from] serde_json::Error),
255 #[error(
256 "generation published, but durable commit is uncertain; reopen or retry idempotently: {0}"
257 )]
258 CommitUncertain(io::Error),
259}
260#[derive(serde::Deserialize, serde::Serialize)]
263struct PersistedAnn {
264 generation: u64,
265 ids: Vec<String>,
267 hnsw: annex::vector::hnsw::HnswSnapshot,
268}
269
270struct FdeAnnBase {
272 index: HNSWIndex,
273 ids: Vec<String>,
274 by_id: HashMap<String, u64>,
275 field: Option<String>,
276 payloads: HashMap<u64, Payload>,
277 payload_index: PayloadIndex,
278}
279
280#[derive(Clone)]
281struct FdeAnn {
282 base: Arc<FdeAnnBase>,
283 delta: HashSet<String>,
286 tombstones: HashSet<u64>,
287 generation: u64,
288}
289
290#[derive(Clone)]
291struct State {
292 generation: u64,
293 codebook: Vec<Vector>,
294 residual_codebook: Vec<f32>,
295 documents: HashMap<String, DocumentRecord>,
296 postings: Vec<HashSet<String>>,
297 fde_ann: Option<FdeAnn>,
298 named_ann: HashMap<String, FdeAnn>,
299 stores: Arc<SegmentStores>,
300 retrieval: Arc<RetrievalState>,
301 planner_stats: planner::CachedPlannerStats,
302 objects_map: Option<Arc<memmap2::Mmap>>,
303 fde_map: Option<Arc<memmap2::Mmap>>,
304 sealed: HashMap<u64, Arc<SegmentSnapshot>>,
305}
306struct SegmentSnapshot {
307 stores: Arc<SegmentStores>,
308 objects: Option<Arc<memmap2::Mmap>>,
309 fde: Option<Arc<memmap2::Mmap>>,
310 bounds: SegmentBoundaries,
311}
312impl SegmentSnapshot {
313 fn open(root: &Path, id: u64, bounds: SegmentBoundaries) -> Result<Self, IndexError> {
314 let path = if id == 0 {
315 root.to_owned()
316 } else {
317 root.join("segments").join(id.to_string())
318 };
319 if !path.is_dir() {
320 return Err(IndexError::Invalid("missing sealed segment".into()));
321 }
322 let stores = Arc::new(SegmentStores {
323 objects: CompressedVectorStore::new(path.join("objects"))?,
324 fde: FixedVectorStore::new(path.join("fde"))?,
325 root: path,
326 id: if id == 0 { None } else { Some(id) },
327 retired: std::sync::atomic::AtomicBool::new(false),
328 });
329 if stores.objects.len()? < bounds.objects || stores.fde.len()? < bounds.fde {
330 return Err(IndexError::Invalid(
331 "sealed segment shorter than committed boundary".into(),
332 ));
333 }
334 Ok(Self {
335 objects: if bounds.objects > 0 {
336 Some(Arc::new(stores.objects.map()?))
337 } else {
338 None
339 },
340 fde: if bounds.fde > 0 {
341 Some(Arc::new(stores.fde.map()?))
342 } else {
343 None
344 },
345 stores,
346 bounds,
347 })
348 }
349}
350impl State {
351 fn record_fde(&self, record: &DocumentRecord) -> &[u8] {
352 if record.storage_id == self.stores.id.unwrap_or(0) {
353 self.fde_bytes()
354 } else {
355 self.sealed[&record.storage_id]
356 .fde
357 .as_deref()
358 .map(|m| &m[..self.sealed[&record.storage_id].bounds.fde as usize])
359 .unwrap_or(&[])
360 }
361 }
362 fn record_objects(&self, record: &DocumentRecord) -> &[u8] {
363 if record.storage_id == self.stores.id.unwrap_or(0) {
364 self.object_bytes()
365 } else {
366 self.sealed[&record.storage_id]
367 .objects
368 .as_deref()
369 .map(|m| &m[..self.sealed[&record.storage_id].bounds.objects as usize])
370 .unwrap_or(&[])
371 }
372 }
373 fn object_bytes(&self) -> &[u8] {
374 self.objects_map.as_deref().map(|m| &m[..]).unwrap_or(&[])
375 }
376 fn fde_bytes(&self) -> &[u8] {
377 self.fde_map.as_deref().map(|m| &m[..]).unwrap_or(&[])
378 }
379}
380
381struct SegmentStores {
382 objects: CompressedVectorStore,
383 fde: FixedVectorStore,
384 root: PathBuf,
385 id: Option<u64>,
386 retired: std::sync::atomic::AtomicBool,
387}
388
389impl Drop for SegmentStores {
390 fn drop(&mut self) {
391 if self.retired.load(std::sync::atomic::Ordering::Relaxed) {
392 if self.id.is_some() {
393 let _ = fs::remove_dir_all(&self.root);
394 } else {
395 let _ = fs::remove_dir_all(self.root.join("objects"));
396 let _ = fs::remove_dir_all(self.root.join("fde"));
397 }
398 }
399 }
400}
401
402struct DirectoryLock(File);
403
404impl Drop for DirectoryLock {
405 fn drop(&mut self) {
406 let _ = fs2::FileExt::unlock(&self.0);
409 }
410}
411
412pub struct MultiVectorIndex {
413 root: PathBuf,
414 config: IndexConfig,
415 fde: FdeEncoder,
416 state: RwLock<Arc<State>>,
417 writer: Mutex<()>,
418 calibration: Mutex<calibration::CalibrationStats>,
419 durability: Durability,
420 _directory_lock: DirectoryLock,
422}
423
424impl MultiVectorIndex {
425 pub(crate) fn initialize(&self) -> Result<(), IndexError> {
426 let _writer = self.writer.lock().unwrap();
427 self.persist(&self.snapshot())
428 }
429 pub fn open_existing(
430 path: impl AsRef<Path>,
431 durability: Durability,
432 ) -> Result<Self, IndexError> {
433 let bytes = fs::read(path.as_ref().join("manifest.json"))?;
434 let value: Value = serde_json::from_slice(&bytes)?;
435 let manifest: Manifest = if value.get("manifest").is_some() {
436 let envelope: ManifestEnvelope = serde_json::from_value(value)?;
437 if blake3::hash(envelope.manifest.as_bytes()).to_hex().as_str()
438 != envelope.checksum_blake3
439 {
440 return Err(IndexError::Invalid("manifest checksum mismatch".into()));
441 }
442 serde_json::from_str(&envelope.manifest)?
443 } else {
444 serde_json::from_slice(&bytes)?
445 };
446 Self::open_with_durability(path, manifest.config, durability)
447 }
448 pub fn open(path: impl AsRef<Path>, config: IndexConfig) -> Result<Self, IndexError> {
449 Self::open_with_durability(path, config, Durability::Fsync)
450 }
451
452 pub fn open_with_durability(
453 path: impl AsRef<Path>,
454 config: IndexConfig,
455 durability: Durability,
456 ) -> Result<Self, IndexError> {
457 if config.dimension == 0
458 || config.centroids == 0
459 || config.probes == 0
460 || !(1..=8).contains(&config.residual_bits)
461 || config.fde_repetitions == 0
462 || config.fde_ksim == 0
463 || config.fde_ksim > 12
464 || config.fde_projected == 0
465 {
466 return Err(IndexError::Invalid(
467 "dimension, centroids, probes, and residual_bits (1..=8) must be valid".into(),
468 ));
469 }
470 validate_config_size(&config)?;
471 config.analyzer.validate().map_err(IndexError::Invalid)?;
472 let root = path.as_ref().to_owned();
473 let mut created_parents = Vec::new();
474 let mut missing = root.as_path();
475 while !missing.exists() {
476 let parent = missing
477 .parent()
478 .filter(|p| !p.as_os_str().is_empty())
479 .unwrap_or(Path::new("."));
480 created_parents.push(parent.to_owned());
481 missing = parent;
482 }
483 fs::create_dir_all(&root)?;
484 let directory_lock = OpenOptions::new()
485 .create(true)
486 .truncate(false)
487 .read(true)
488 .write(true)
489 .open(root.join("index.lock"))?;
490 fs2::FileExt::try_lock_exclusive(&directory_lock).map_err(|e| {
491 IndexError::Invalid(format!("index already open or cannot lock directory: {e}"))
492 })?;
493 let directory_lock = DirectoryLock(directory_lock);
494 let manifest_path = root.join("manifest.json");
495 let (
496 generation,
497 fde_encoding_version,
498 codebook,
499 residual_codebook,
500 mut documents,
501 representation_schema,
502 segments,
503 storage_generation,
504 sealed_bounds,
505 checksummed,
506 ) = if manifest_path.exists() {
507 let bytes = fs::read(&manifest_path)?;
508 let header: Value = serde_json::from_slice(&bytes)?;
509 let (m, checksummed): (Manifest, bool) = if header.get("manifest").is_some() {
510 let envelope: ManifestEnvelope = serde_json::from_slice(&bytes)?;
511 if !matches!(envelope.format_version, 2 | FORMAT_VERSION)
512 || blake3::hash(envelope.manifest.as_bytes()).to_hex().as_str()
513 != envelope.checksum_blake3
514 {
515 return Err(IndexError::Invalid(
516 "manifest checksum or envelope version mismatch".into(),
517 ));
518 }
519 (serde_json::from_str(&envelope.manifest)?, true)
520 } else {
521 verify_manifest_checksum(&bytes, &root.join("manifest.sha256"))?;
522 (serde_json::from_slice(&bytes)?, false)
523 };
524 if !(if checksummed {
525 matches!(m.format_version, 2 | FORMAT_VERSION)
526 } else {
527 m.format_version == 1
528 }) || (checksummed && m.segments.is_none())
529 {
530 return Err(IndexError::Invalid(
531 "unsupported manifest version or missing committed boundaries".into(),
532 ));
533 }
534 if m.config != config {
535 return Err(IndexError::Config {
536 actual: Box::new(m.config),
537 requested: Box::new(config),
538 });
539 }
540 (
541 m.generation,
542 m.fde_encoding_version,
543 m.codebook,
544 m.residual_codebook,
545 m.documents,
546 m.representation_schema,
547 m.segments,
548 m.storage_generation,
549 m.sealed,
550 checksummed,
551 )
552 } else {
553 (
555 0,
556 2,
557 vec![],
558 vec![],
559 HashMap::new(),
560 BTreeMap::new(),
561 Some(SegmentBoundaries { objects: 0, fde: 0 }),
562 None,
563 Vec::new(),
564 false,
565 )
566 };
567 if !matches!(fde_encoding_version, 1 | 2) {
568 return Err(IndexError::Invalid(format!(
569 "unsupported FDE encoding version {fde_encoding_version}"
570 )));
571 }
572 validate_codebooks(
573 &config,
574 &codebook,
575 &residual_codebook,
576 documents.values().any(|d| d.tokens > 0),
577 )?;
578 let store_root = storage_generation
579 .map(|id| root.join("segments").join(id.to_string()))
580 .unwrap_or_else(|| root.clone());
581 if storage_generation.is_some() && !store_root.is_dir() {
582 return Err(IndexError::Invalid(
583 "missing committed segment directory".into(),
584 ));
585 }
586 let objects = CompressedVectorStore::new(store_root.join("objects"))?;
587 let fde_store = FixedVectorStore::new(store_root.join("fde"))?;
588 if !manifest_path.exists()
589 && (objects.len()? != 0
590 || fde_store.len()? != 0
591 || root
592 .join("segments")
593 .read_dir()
594 .is_ok_and(|mut entries| entries.next().is_some()))
595 {
596 return Err(IndexError::Invalid(
597 "missing manifest for non-empty segments".into(),
598 ));
599 }
600 let boundaries = segments.unwrap_or(SegmentBoundaries {
601 objects: objects.len()?,
602 fde: fde_store.len()?,
603 });
604 if objects.len()? < boundaries.objects || fde_store.len()? < boundaries.fde {
605 return Err(IndexError::Invalid(
606 "segment shorter than committed boundary".into(),
607 ));
608 }
609 let mut sealed = HashMap::new();
610 for (id, bounds) in sealed_bounds {
611 if id == storage_generation.unwrap_or(0) || sealed.contains_key(&id) {
612 return Err(IndexError::Invalid("duplicate storage segment".into()));
613 }
614 sealed.insert(id, Arc::new(SegmentSnapshot::open(&root, id, bounds)?));
615 }
616 if !documents.is_empty() {
617 let objects_map = if objects.len()? > 0 {
618 Some(objects.map()?)
619 } else {
620 None
621 };
622 let fde_map = if fde_store.len()? > 0 {
623 Some(fde_store.map()?)
624 } else {
625 None
626 };
627 let object_bytes = objects_map
628 .as_deref()
629 .unwrap_or(&[])
630 .get(
631 ..usize::try_from(boundaries.objects)
632 .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?,
633 )
634 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?;
635 let fde_bytes = fde_map
636 .as_deref()
637 .unwrap_or(&[])
638 .get(
639 ..usize::try_from(boundaries.fde)
640 .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?,
641 )
642 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?;
643 let fde_dimension =
644 (1usize << config.fde_ksim) * config.fde_projected * config.fde_repetitions;
645 for d in documents.values_mut() {
646 let (object_bytes, fde_bytes) = if d.storage_id == storage_generation.unwrap_or(0) {
647 (object_bytes, fde_bytes)
648 } else {
649 let segment = sealed.get(&d.storage_id).ok_or_else(|| {
650 IndexError::Invalid("document references missing segment".into())
651 })?;
652 (
653 segment
654 .objects
655 .as_deref()
656 .map(|m| &m[..segment.bounds.objects as usize])
657 .unwrap_or(&[]),
658 segment
659 .fde
660 .as_deref()
661 .map(|m| &m[..segment.bounds.fde as usize])
662 .unwrap_or(&[]),
663 )
664 };
665 d.fields.verify(fde_bytes)?;
666 if d.tokens == 0 {
667 if !d.centroid_ids.is_empty()
668 || !d.unique_centroids.is_empty()
669 || d.location.length != 0
670 || d.fde_location.length != 0
671 {
672 return Err(IndexError::Invalid("invalid empty multivector".into()));
673 }
674 continue;
675 }
676 verify_record(object_bytes, d.location, checksummed)?;
677 verify_record(fde_bytes, d.fde_location, checksummed)?;
678 let decoded = CompressedVectorStore::decode(
679 object_bytes,
680 d.location,
681 &codebook,
682 &residual_codebook,
683 )?;
684 if decoded.dimension != config.dimension
685 || d.tokens == 0
686 || decoded.values.len() / config.dimension != d.tokens
687 || d.centroid_ids.len() != d.tokens
688 || d.compressed_bytes != d.location.length
689 || d.centroid_ids.iter().any(|&c| c as usize >= codebook.len())
690 || decoded.values.iter().any(|v| !v.is_finite())
691 {
692 return Err(IndexError::Invalid(
693 "invalid document shape or centroid IDs".into(),
694 ));
695 }
696 let bytes = record_bytes(object_bytes, d.location)?;
697 let stored_ids = bytes[16..16 + d.tokens * 4]
698 .as_chunks::<4>()
699 .0
700 .iter()
701 .map(|v| u32::from_le_bytes(*v));
702 if !stored_ids.eq(d.centroid_ids.iter().copied()) {
703 return Err(IndexError::Invalid(
704 "manifest/record centroid mismatch".into(),
705 ));
706 }
707 let mut unique = d.centroid_ids.clone();
708 unique.sort_unstable();
709 unique.dedup();
710 if unique != d.unique_centroids {
711 return Err(IndexError::Invalid("invalid document posting list".into()));
712 }
713 if FixedVectorStore::get(fde_bytes, d.fde_location, fde_dimension)?
714 .iter()
715 .any(|v| !v.is_finite())
716 {
717 return Err(IndexError::Invalid("non-finite FDE record".into()));
718 }
719 d.location.checksum =
721 Some(*blake3::hash(record_bytes(object_bytes, d.location)?).as_bytes());
722 d.fde_location.checksum =
723 Some(*blake3::hash(record_bytes(fde_bytes, d.fde_location)?).as_bytes());
724 }
725 }
726 for segment in sealed.values() {
728 segment.stores.objects.recover(segment.bounds.objects)?;
729 segment.stores.fde.recover(segment.bounds.fde)?;
730 }
731 objects.recover(boundaries.objects)?;
732 fde_store.recover(boundaries.fde)?;
733 if durability == Durability::Fsync {
734 File::open(store_root.join("objects"))?.sync_all()?;
735 File::open(store_root.join("fde"))?.sync_all()?;
736 File::open(&root)?.sync_all()?;
737 for parent in created_parents {
738 File::open(parent)?.sync_all()?;
739 }
740 }
741 if manifest_path.exists() {
742 if let Ok(entries) = fs::read_dir(root.join("segments")) {
743 for entry in entries {
744 let entry = entry?;
745 if !entry.file_type()?.is_dir() {
746 continue;
747 }
748 if let Some(id) = entry
749 .file_name()
750 .to_str()
751 .and_then(|s| s.parse::<u64>().ok())
752 && Some(id) != storage_generation
753 && !sealed.contains_key(&id)
754 {
755 fs::remove_dir_all(entry.path())?;
756 }
757 }
758 }
759 if storage_generation.is_some() && !sealed.contains_key(&0) {
760 for name in ["objects", "fde"] {
761 let path = root.join(name);
762 if path.exists() {
763 fs::remove_dir_all(path)?;
764 }
765 }
766 }
767 }
768 let mut postings = vec![HashSet::new(); codebook.len()];
769 for (id, d) in &documents {
770 for &c in &d.centroid_ids {
771 postings[c as usize].insert(id.clone());
772 }
773 }
774 let mut retrieval = RetrievalState::from_schema(
779 representation_schema,
780 Analyzer::new(config.analyzer.clone()),
781 )?;
782 let mut ordered: Vec<_> = documents.iter().collect();
783 ordered.sort_by(|a, b| a.0.cmp(b.0));
784 for (id, d) in ordered {
785 retrieval.insert(id, &d.fields, &d.metadata)?;
786 }
787 let planner_stats =
788 planner::CachedPlannerStats::from_documents(&documents, retrieval.schema());
789 let index = Self {
790 fde: if fde_encoding_version == 2 {
791 FdeEncoder::new(
792 config.dimension,
793 config.fde_ksim,
794 config.fde_projected,
795 config.fde_repetitions,
796 0x4d55_5645_5241,
797 )
798 } else {
799 FdeEncoder::with_version(
800 config.dimension,
801 config.fde_ksim,
802 config.fde_projected,
803 config.fde_repetitions,
804 0x4d55_5645_5241,
805 fde_encoding_version,
806 )
807 },
808 writer: Mutex::new(()),
809 calibration: Mutex::new(calibration::CalibrationStats::default()),
810 durability: durability.clone(),
811 _directory_lock: directory_lock,
812 state: RwLock::new(Arc::new(State {
813 generation,
814 codebook,
815 residual_codebook,
816 documents,
817 postings,
818 fde_ann: None,
819 named_ann: HashMap::new(),
820 objects_map: if objects.len()? > 0 {
821 Some(Arc::new(objects.map()?))
822 } else {
823 None
824 },
825 fde_map: if fde_store.len()? > 0 {
826 Some(Arc::new(fde_store.map()?))
827 } else {
828 None
829 },
830 stores: Arc::new(SegmentStores {
831 objects,
832 fde: fde_store,
833 root: store_root,
834 id: storage_generation,
835 retired: std::sync::atomic::AtomicBool::new(false),
836 }),
837 retrieval: Arc::new(retrieval),
838 planner_stats,
839 sealed,
840 })),
841 root,
842 config,
843 };
844 index.load_calibration();
845 {
848 let mut s = (*index.snapshot()).clone();
849 index.load_all_ann(&mut s);
850 *index.state.write().unwrap() = Arc::new(s);
851 }
852 Ok(index)
853 }
854 fn snapshot(&self) -> Arc<State> {
855 Arc::clone(&self.state.read().unwrap())
856 }
857
858 pub fn calibration_snapshot(&self) -> CalibrationSnapshot {
859 self.calibration.lock().unwrap().snapshot()
860 }
861
862 fn calibration_path(&self) -> PathBuf {
863 self.root.join("calibration.json")
864 }
865
866 fn load_calibration(&self) {
867 let path = self.calibration_path();
868 let Ok(bytes) = fs::read(&path) else { return };
869 let Ok(snapshot) = serde_json::from_slice::<CalibrationSnapshot>(&bytes) else {
870 return;
871 };
872 self.calibration.lock().unwrap().restore(&snapshot);
873 }
874
875 fn save_calibration(&self) {
876 let snapshot = self.calibration.lock().unwrap().snapshot();
877 if let Ok(bytes) = serde_json::to_vec(&snapshot) {
878 let tmp = self.calibration_path().with_extension("json.tmp");
879 if fs::write(&tmp, &bytes).is_ok() {
880 let _ = fs::rename(&tmp, self.calibration_path());
881 self.calibration.lock().unwrap().since_save = 0;
882 }
883 }
884 }
885
886 pub fn flush_calibration(&self) {
891 self.save_calibration();
892 }
893
894 fn ann_dir(&self) -> PathBuf {
895 self.root.join("ann")
896 }
897
898 fn ann_path(&self, field: Option<&str>) -> PathBuf {
899 self.ann_dir()
900 .join(format!("{}.ann", field.unwrap_or("fde")))
901 }
902
903 fn save_ann(&self, field: Option<&str>, generation: u64, ids: &[String], hnsw: &HNSWIndex) {
904 let dir = self.ann_dir();
905 let _ = fs::create_dir_all(&dir);
906 let path = self.ann_path(field);
907 let tmp = path.with_extension("ann.tmp");
908 let persisted = PersistedAnn {
909 generation,
910 ids: ids.to_vec(),
911 hnsw: hnsw.to_snapshot(),
912 };
913 if let Ok(bytes) = bincode::serialize(&persisted) {
914 if fs::write(&tmp, &bytes).is_ok() {
915 let _ = fs::rename(&tmp, path);
916 }
917 }
918 }
919
920 fn delete_ann(&self, field: Option<&str>) {
921 let _ = fs::remove_file(self.ann_path(field));
922 }
923
924 fn load_all_ann(&self, s: &mut State) {
928 let dir = self.ann_dir();
929 let Ok(entries) = fs::read_dir(&dir) else {
930 return;
931 };
932 for entry in entries.flatten() {
933 let path = entry.path();
934 if path.extension().and_then(|e| e.to_str()) != Some("ann") {
935 continue;
936 }
937 let stem = path
938 .file_stem()
939 .and_then(|s| s.to_str())
940 .unwrap_or("")
941 .to_owned();
942 let field: Option<&str> = if stem == "fde" { None } else { Some(&stem) };
943 let Ok(bytes) = fs::read(&path) else { continue };
944 let Ok(persisted) = bincode::deserialize::<PersistedAnn>(&bytes) else {
945 continue;
946 };
947 if persisted.generation != s.generation {
955 continue;
956 }
957 let by_id: HashMap<String, u64> = persisted
961 .ids
962 .iter()
963 .enumerate()
964 .map(|(i, id)| (id.clone(), i as u64))
965 .collect();
966 let mut payloads = HashMap::with_capacity(persisted.ids.len());
967 let mut payload_index = PayloadIndex::new();
968 for (point, id) in persisted.ids.iter().enumerate() {
969 if let Some(doc) = s.documents.get(id) {
970 let payload = retrieval::metadata_ann_payload(&doc.metadata);
971 payload_index.insert(point as u64, &payload);
972 payloads.insert(point as u64, payload);
973 }
974 }
975 let ann = FdeAnn {
976 base: Arc::new(FdeAnnBase {
977 index: HNSWIndex::from_snapshot(persisted.hnsw),
978 ids: persisted.ids,
979 by_id,
980 field: field.map(str::to_owned),
981 payloads,
982 payload_index,
983 }),
984 delta: HashSet::new(),
985 tombstones: HashSet::new(),
986 generation: s.generation,
989 };
990 if let Some(f) = field {
991 s.named_ann.insert(f.to_owned(), ann);
992 } else {
993 s.fde_ann = Some(ann);
994 }
995 }
996 }
997
998 fn validate(&self, v: &[Vector]) -> Result<(), IndexError> {
999 if v.is_empty()
1000 || v.iter()
1001 .any(|x| x.len() != self.config.dimension || x.iter().any(|n| !n.is_finite()))
1002 {
1003 Err(IndexError::Invalid(format!(
1004 "vectors must be a non-empty matrix of {} finite values",
1005 self.config.dimension
1006 )))
1007 } else {
1008 Ok(())
1009 }
1010 }
1011 fn persist(&self, s: &State) -> Result<(), IndexError> {
1012 if self.durability == Durability::Fsync {
1013 s.stores.objects.sync()?;
1014 commit_boundary("objects_synced")?;
1015 s.stores.fde.sync()?;
1016 commit_boundary("fde_synced")?;
1017 }
1018 let manifest = serde_json::to_string(&Manifest {
1019 format_version: FORMAT_VERSION,
1020 generation: s.generation,
1021 fde_encoding_version: self.fde.encoding_version(),
1022 config: self.config.clone(),
1023 codebook: s.codebook.clone(),
1024 residual_codebook: s.residual_codebook.clone(),
1025 documents: s.documents.clone(),
1026 representation_schema: s.retrieval.schema().clone(),
1027 storage_generation: s.stores.id,
1028 sealed: s
1029 .sealed
1030 .iter()
1031 .map(|(&id, segment)| (id, segment.bounds))
1032 .collect(),
1033 segments: Some(SegmentBoundaries {
1034 objects: s.stores.objects.len()?,
1035 fde: s.stores.fde.len()?,
1036 }),
1037 })?;
1038 let envelope = ManifestEnvelope {
1039 format_version: FORMAT_VERSION,
1040 checksum_blake3: blake3::hash(manifest.as_bytes()).to_hex().to_string(),
1041 manifest,
1042 };
1043 let bytes = serde_json::to_vec(&envelope)?;
1044 atomic_write(
1045 &self.root.join("manifest.json"),
1046 &bytes,
1047 self.durability == Durability::Fsync,
1048 )
1049 .map_err(|e| {
1050 if e.published {
1051 IndexError::CommitUncertain(e.source)
1052 } else {
1053 IndexError::Io(e.source)
1054 }
1055 })?;
1056 self.save_calibration();
1058 Ok(())
1059 }
1060
1061 fn commit(&self, current: &State, mut next: State) -> Result<(), IndexError> {
1062 next.generation = current
1063 .generation
1064 .checked_add(1)
1065 .ok_or_else(|| IndexError::Invalid("generation exhausted".into()))?;
1066 if let Some(ann) = next.fde_ann.as_mut() {
1069 ann.generation = next.generation;
1070 }
1071 for ann in next.named_ann.values_mut() {
1072 ann.generation = next.generation;
1073 }
1074 if next.stores.objects.len()? as usize != next.object_bytes().len() {
1075 next.objects_map = Some(Arc::new(next.stores.objects.map()?));
1076 }
1077 if next.stores.fde.len()? as usize != next.fde_bytes().len() {
1078 next.fde_map = Some(Arc::new(next.stores.fde.map()?));
1079 }
1080 let result = self.persist(&next);
1081 if result.is_ok() || matches!(result, Err(IndexError::CommitUncertain(_))) {
1082 next.stores
1083 .retired
1084 .store(false, std::sync::atomic::Ordering::Relaxed);
1085 *self.state.write().unwrap() = Arc::new(next);
1086 }
1087 result
1088 }
1089 fn invalidate_fde_ann(next: &mut State) {
1090 next.fde_ann = None;
1091 }
1092 pub fn train(&self, samples: &[Vector], iterations: usize) -> Result<(), IndexError> {
1094 self.validate(samples)?;
1095 if samples.len() < self.config.centroids {
1096 return Err(IndexError::Invalid(
1097 "training samples must be >= centroid count".into(),
1098 ));
1099 }
1100 let _writer = self.writer.lock().unwrap();
1101 let s = self.snapshot();
1102 if s.documents.values().any(|d| d.tokens > 0) {
1103 return Err(IndexError::Invalid(
1104 "cannot retrain an index containing token vectors".into(),
1105 ));
1106 }
1107 let samples: Vec<_> = samples.iter().map(|sample| normalize(sample)).collect();
1108 let mut centers: Vec<_> = (0..self.config.centroids)
1109 .map(|i| samples[i * samples.len() / self.config.centroids].clone())
1110 .collect();
1111 for _ in 0..iterations.max(1) {
1112 let mut sums = vec![vec![0.; self.config.dimension]; centers.len()];
1113 let mut counts = vec![0usize; centers.len()];
1114 for v in &samples {
1115 let c = nearest(v, ¢ers);
1116 counts[c] += 1;
1117 for (i, x) in v.iter().enumerate() {
1118 sums[c][i] += x;
1119 }
1120 }
1121 for c in 0..centers.len() {
1122 if counts[c] > 0 {
1123 for x in &mut sums[c] {
1124 *x /= counts[c] as f32;
1125 }
1126 centers[c] = normalize(&sums[c]);
1127 }
1128 }
1129 }
1130 let mut next = (*s).clone();
1131 next.codebook = centers;
1132 let residuals: Vec<f32> = samples
1133 .iter()
1134 .flat_map(|vector| {
1135 let center = &next.codebook[nearest(vector, &next.codebook)];
1136 vector
1137 .iter()
1138 .zip(center)
1139 .map(|(value, centroid)| value - centroid)
1140 .collect::<Vec<_>>()
1141 })
1142 .collect();
1143 next.residual_codebook =
1144 train_scalar_codebook(&residuals, 1usize << self.config.residual_bits, 12);
1145 next.postings = vec![HashSet::new(); next.codebook.len()];
1146 Self::invalidate_fde_ann(&mut next);
1148 self.delete_ann(None); self.commit(&s, next)
1150 }
1151 pub fn upsert(
1152 &self,
1153 id: impl Into<String>,
1154 vectors: Vec<Vector>,
1155 metadata: Value,
1156 ) -> Result<(), IndexError> {
1157 self.upsert_batch(vec![UpsertDocument {
1158 id: id.into(),
1159 vectors,
1160 metadata,
1161 }])
1162 }
1163 pub fn upsert_batch(&self, batch: Vec<UpsertDocument>) -> Result<(), IndexError> {
1168 self.upsert_records(
1169 batch
1170 .into_iter()
1171 .map(|d| RetrievalDocument {
1172 id: d.id,
1173 vectors: d.vectors,
1174 metadata: d.metadata,
1175 ..RetrievalDocument::default()
1176 })
1177 .collect(),
1178 )
1179 }
1180 pub fn upsert_records(&self, batch: Vec<RetrievalDocument>) -> Result<(), IndexError> {
1181 if batch.is_empty() {
1182 return Ok(());
1183 }
1184 for document in &batch {
1185 if !document.vectors.is_empty() {
1186 self.validate(&document.vectors)?;
1187 }
1188 }
1189 let _writer = self.writer.lock().unwrap();
1190 let s = self.snapshot();
1191 if s.codebook.is_empty() && batch.iter().any(|d| !d.vectors.is_empty()) {
1192 return Err(IndexError::Invalid(
1193 "index is untrained; call train first".into(),
1194 ));
1195 }
1196 let mut next = (*s).clone();
1197 if next.stores.objects.len()? + next.stores.fde.len()? >= 64 * 1024 * 1024 {
1198 self.rotate_segment(&mut next)?;
1199 }
1200 for document in batch {
1201 let fields = Arc::new(Fields::prepare(&document, &next.stores)?);
1202 let id = document.id;
1203 Arc::make_mut(&mut next.retrieval).insert(&id, &fields, &document.metadata)?;
1204 if let Some(old) = next.documents.get(&id) {
1205 next.planner_stats.remove(old, next.retrieval.schema());
1206 }
1207 if let Some(old_ids) = next
1208 .documents
1209 .get(&id)
1210 .map(|old| old.unique_centroids.clone())
1211 {
1212 for c in old_ids {
1213 next.postings[c as usize].remove(&id);
1214 }
1215 }
1216 let vectors: Vec<_> = document
1217 .vectors
1218 .iter()
1219 .map(|vector| normalize(vector))
1220 .collect();
1221 let ids: Vec<u32> = vectors
1222 .iter()
1223 .map(|v| nearest(v, &next.codebook) as u32)
1224 .collect();
1225 let mut unique_centroids = ids.clone();
1226 unique_centroids.sort_unstable();
1227 unique_centroids.dedup();
1228 let (location, size) = if vectors.is_empty() {
1229 (
1230 ObjectLocation {
1231 offset: 0,
1232 length: 0,
1233 checksum: None,
1234 },
1235 0,
1236 )
1237 } else {
1238 next.stores.objects.put(
1239 &vectors,
1240 &ids,
1241 &next.codebook,
1242 &next.residual_codebook,
1243 self.config.residual_bits,
1244 )?
1245 };
1246 commit_boundary("object_appended")?;
1247 let fde_location = if vectors.is_empty() {
1248 ObjectLocation {
1249 offset: 0,
1250 length: 0,
1251 checksum: None,
1252 }
1253 } else {
1254 next.stores.fde.put(&self.fde.encode_document(&vectors))?
1255 };
1256 commit_boundary("fde_appended")?;
1257 for &c in &unique_centroids {
1258 next.postings[c as usize].insert(id.clone());
1259 }
1260 if let Some(ann) = next.fde_ann.as_mut() {
1261 if let Some(&point_id) = ann.base.by_id.get(&id) {
1262 ann.tombstones.insert(point_id);
1263 }
1264 if vectors.is_empty() {
1265 ann.delta.remove(&id);
1266 } else {
1267 ann.delta.insert(id.clone());
1268 }
1269 }
1270 for (field, ann) in &mut next.named_ann {
1271 if let Some(&point) = ann.base.by_id.get(&id) {
1272 ann.tombstones.insert(point);
1273 }
1274 if fields.has_dense(field) {
1275 ann.delta.insert(id.clone());
1276 } else {
1277 ann.delta.remove(&id);
1278 }
1279 }
1280 let record = DocumentRecord {
1281 centroid_ids: ids,
1282 unique_centroids,
1283 location,
1284 fde_location,
1285 metadata: document.metadata,
1286 tokens: vectors.len(),
1287 compressed_bytes: size,
1288 fields,
1289 storage_id: next.stores.id.unwrap_or(0),
1290 };
1291 next.planner_stats.add(&record, next.retrieval.schema());
1292 next.documents.insert(id, record);
1293 }
1294 self.commit(&s, next)
1295 }
1296 pub fn delete(&self, id: &str) -> Result<bool, IndexError> {
1297 let _writer = self.writer.lock().unwrap();
1298 let s = self.snapshot();
1299 if !s.documents.contains_key(id) {
1300 return Ok(false);
1301 }
1302 let mut next = (*s).clone();
1303 let d = next.documents.remove(id).unwrap();
1304 Arc::make_mut(&mut next.retrieval).remove(id);
1305 next.planner_stats.remove(&d, next.retrieval.schema());
1306 for c in d.unique_centroids {
1307 next.postings[c as usize].remove(id);
1308 }
1309 if let Some(ann) = next.fde_ann.as_mut() {
1310 if let Some(&point_id) = ann.base.by_id.get(id) {
1311 ann.tombstones.insert(point_id);
1312 }
1313 ann.delta.remove(id);
1314 }
1315 for ann in next.named_ann.values_mut() {
1316 if let Some(&point) = ann.base.by_id.get(id) {
1317 ann.tombstones.insert(point);
1318 }
1319 ann.delta.remove(id);
1320 }
1321 self.commit(&s, next)?;
1322 Ok(true)
1323 }
1324 pub fn query(
1326 &self,
1327 vectors: &[Vector],
1328 top_k: usize,
1329 candidates: Option<usize>,
1330 ) -> Result<Vec<Hit>, IndexError> {
1331 self.validate(vectors)?;
1332 if top_k == 0 {
1333 return Err(IndexError::Invalid("top_k must be positive".into()));
1334 }
1335 let normalized: Vec<_> = vectors.iter().map(|vector| normalize(vector)).collect();
1336 let cap = candidates.unwrap_or(top_k.saturating_mul(8)).max(top_k);
1340 let s = self.snapshot();
1341 let approximate = self.exact_fde_scores_capped(&s, &normalized, Some(cap))?;
1342 self.rescore(&s, &normalized, approximate, top_k, candidates)
1343 }
1344 pub fn hnsw_ready(&self) -> bool {
1346 let s = self.snapshot();
1347 s.fde_ann
1348 .as_ref()
1349 .is_some_and(|ann| ann.generation == s.generation)
1350 }
1351
1352 pub fn query_auto(
1355 &self,
1356 vectors: &[Vector],
1357 top_k: usize,
1358 candidates: Option<usize>,
1359 ef_search: usize,
1360 ) -> Result<Vec<Hit>, IndexError> {
1361 self.query_auto_with_backend(vectors, top_k, candidates, ef_search)
1362 .map(|(hits, _)| hits)
1363 }
1364
1365 pub fn query_auto_with_backend(
1367 &self,
1368 vectors: &[Vector],
1369 top_k: usize,
1370 candidates: Option<usize>,
1371 ef_search: usize,
1372 ) -> Result<(Vec<Hit>, &'static str), IndexError> {
1373 self.validate(vectors)?;
1374 if top_k == 0 || ef_search == 0 {
1375 return Err(IndexError::Invalid(
1376 "top_k and ef_search must be positive".into(),
1377 ));
1378 }
1379 let normalized: Vec<_> = vectors.iter().map(|v| normalize(v)).collect();
1380 let count = candidates.unwrap_or(top_k.saturating_mul(8)).max(top_k);
1381 let s = self.snapshot();
1382 let (approximate, backend) = if s
1383 .fde_ann
1384 .as_ref()
1385 .is_some_and(|ann| ann.generation == s.generation)
1386 {
1387 (
1388 self.ann_fde_scores(&s, &self.fde.encode_query(&normalized), count, ef_search)?,
1389 "hnsw",
1390 )
1391 } else {
1392 (
1393 self.exact_fde_scores_capped(&s, &normalized, Some(count))?,
1394 "muvera",
1395 )
1396 };
1397 Ok((
1398 self.rescore(&s, &normalized, approximate, top_k, candidates)?,
1399 backend,
1400 ))
1401 }
1402 pub fn query_with_centroid_pruning(
1405 &self,
1406 vectors: &[Vector],
1407 top_k: usize,
1408 candidates: usize,
1409 rerank_candidates: usize,
1410 ) -> Result<Vec<Hit>, IndexError> {
1411 self.validate(vectors)?;
1412 check_pruning_shape(top_k, candidates, rerank_candidates)?;
1413 let normalized: Vec<_> = vectors.iter().map(|vector| normalize(vector)).collect();
1414 let s = self.snapshot();
1415 let approximate = self.exact_fde_scores_capped(&s, &normalized, Some(candidates))?;
1416 self.prune_and_rescore(
1417 &s,
1418 &normalized,
1419 approximate,
1420 top_k,
1421 candidates,
1422 rerank_candidates,
1423 )
1424 }
1425 pub fn query_with_fde_ann_and_pruning(
1428 &self,
1429 vectors: &[Vector],
1430 top_k: usize,
1431 candidates: usize,
1432 rerank_candidates: usize,
1433 ef_search: usize,
1434 ) -> Result<Vec<Hit>, IndexError> {
1435 self.validate(vectors)?;
1436 if ef_search == 0 {
1437 return Err(IndexError::Invalid("ef_search must be positive".into()));
1438 }
1439 check_pruning_shape(top_k, candidates, rerank_candidates)?;
1440 let normalized: Vec<_> = vectors.iter().map(|vector| normalize(vector)).collect();
1441 let query_fde = self.fde.encode_query(&normalized);
1442 let s = self.snapshot();
1443 let approximate = self.ann_fde_scores(&s, &query_fde, candidates, ef_search)?;
1444 self.prune_and_rescore(
1445 &s,
1446 &normalized,
1447 approximate,
1448 top_k,
1449 candidates,
1450 rerank_candidates,
1451 )
1452 }
1453 fn prune_and_rescore(
1454 &self,
1455 s: &State,
1456 normalized: &[Vector],
1457 approximate: Vec<(String, f32)>,
1458 top_k: usize,
1459 candidates: usize,
1460 rerank_candidates: usize,
1461 ) -> Result<Vec<Hit>, IndexError> {
1462 let broad: Vec<_> = approximate.into_iter().take(candidates).collect();
1463 let pruned = centroid_prune(s, normalized, broad, rerank_candidates);
1464 self.rescore(s, normalized, pruned, top_k, Some(rerank_candidates))
1465 }
1466 fn exact_fde_scores(&self, normalized: &[Vector]) -> Result<Vec<(String, f32)>, IndexError> {
1467 let s = self.snapshot();
1468 self.exact_fde_scores_capped(&s, normalized, None)
1469 }
1470
1471 fn exact_fde_scores_capped(
1477 &self,
1478 s: &State,
1479 normalized: &[Vector],
1480 cap: Option<usize>,
1481 ) -> Result<Vec<(String, f32)>, IndexError> {
1482 self.exact_fde_scores_filtered(s, normalized, cap, None)
1483 }
1484 fn exact_fde_scores_filtered(
1485 &self,
1486 s: &State,
1487 normalized: &[Vector],
1488 cap: Option<usize>,
1489 eligible: Option<&retrieval::DocSet>,
1490 ) -> Result<Vec<(String, f32)>, IndexError> {
1491 if !s.documents.values().any(|d| d.tokens > 0) {
1492 return Ok(Vec::new());
1493 }
1494 let query_fde = self.fde.encode_query(normalized);
1495 let fde_dimension = self.fde.output_dimension();
1496 let score = |id: &str, record: &DocumentRecord| {
1497 (record.tokens > 0).then(|| {
1498 Ok((
1499 id.to_owned(),
1500 dot(
1501 &query_fde,
1502 FixedVectorStore::get(
1503 s.record_fde(record),
1504 record.fde_location,
1505 fde_dimension,
1506 )?,
1507 ),
1508 ))
1509 })
1510 };
1511 let approximate_results: Result<Vec<_>, io::Error> = match eligible {
1512 Some(eligible) => eligible
1513 .iter()
1514 .filter_map(|number| s.retrieval.external_id(number))
1515 .collect::<Vec<_>>()
1516 .into_par_iter()
1517 .filter_map(|id| s.documents.get(id).and_then(|record| score(id, record)))
1518 .collect(),
1519 None => s
1520 .documents
1521 .par_iter()
1522 .filter_map(|(id, record)| score(id, record))
1523 .collect(),
1524 };
1525 let mut approximate = approximate_results?;
1526 let n = approximate.len();
1527 let by_desc =
1528 |a: &(String, f32), b: &(String, f32)| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0));
1529 match cap {
1530 Some(k) if k < n => {
1531 approximate.select_nth_unstable_by(k, by_desc);
1532 approximate.truncate(k);
1533 approximate.par_sort_unstable_by(by_desc);
1534 }
1535 _ => {
1536 approximate.par_sort_unstable_by(by_desc);
1537 }
1538 }
1539 Ok(approximate)
1540 }
1541 pub fn exact_fde_candidates(
1543 &self,
1544 vectors: &[Vector],
1545 count: usize,
1546 ) -> Result<Vec<CandidateHit>, IndexError> {
1547 self.validate(vectors)?;
1548 if count == 0 {
1549 return Err(IndexError::Invalid(
1550 "candidate count must be positive".into(),
1551 ));
1552 }
1553 let normalized: Vec<_> = vectors.iter().map(|vector| normalize(vector)).collect();
1554 Ok(self
1555 .exact_fde_scores(&normalized)?
1556 .into_iter()
1557 .take(count)
1558 .map(|(id, score)| CandidateHit { id, score })
1559 .collect())
1560 }
1561 pub fn build_fde_ann(&self, m: usize, ef_construct: usize) -> Result<usize, IndexError> {
1563 self.build_ann(None, m, ef_construct)
1564 }
1565
1566 pub fn auto_compact_fde_ann(
1575 &self,
1576 m: usize,
1577 ef_construct: usize,
1578 threshold_fraction: f64,
1579 ) -> Result<Option<usize>, IndexError> {
1580 let s = self.snapshot();
1581 let should_compact = s.fde_ann.as_ref().is_some_and(|ann| {
1582 let base = ann.base.ids.len().saturating_sub(ann.tombstones.len()) as f64;
1583 let delta = ann.delta.len() as f64;
1584 base > 0.0 && delta / base > threshold_fraction
1585 });
1586 if should_compact {
1587 Ok(Some(self.build_fde_ann(m, ef_construct)?))
1588 } else {
1589 Ok(None)
1590 }
1591 }
1592
1593 pub fn auto_compact_dense_ann(
1596 &self,
1597 field: &str,
1598 m: usize,
1599 ef_construct: usize,
1600 threshold_fraction: f64,
1601 ) -> Result<Option<usize>, IndexError> {
1602 let s = self.snapshot();
1603 let should_compact = s.named_ann.get(field).is_some_and(|ann| {
1604 let base = ann.base.ids.len().saturating_sub(ann.tombstones.len()) as f64;
1605 let delta = ann.delta.len() as f64;
1606 base > 0.0 && delta / base > threshold_fraction
1607 });
1608 if should_compact {
1609 Ok(Some(self.build_dense_ann(field, m, ef_construct)?))
1610 } else {
1611 Ok(None)
1612 }
1613 }
1614 pub fn build_dense_ann(
1615 &self,
1616 field: &str,
1617 m: usize,
1618 ef_construct: usize,
1619 ) -> Result<usize, IndexError> {
1620 self.build_ann(Some(field), m, ef_construct)
1621 }
1622 fn build_ann(
1623 &self,
1624 field: Option<&str>,
1625 m: usize,
1626 ef_construct: usize,
1627 ) -> Result<usize, IndexError> {
1628 if !(1..=128).contains(&m) || !(1..=65_536).contains(&ef_construct) {
1629 return Err(IndexError::Invalid(
1630 "HNSW m must be in 1..=128 and ef_construct in 1..=65536".into(),
1631 ));
1632 }
1633 let s = self.snapshot();
1634 let dimension = if let Some(field) = field {
1635 s.retrieval.dense_dimension(field)?
1636 } else {
1637 self.fde.output_dimension()
1638 };
1639 let mut ids: Vec<_> = s
1640 .documents
1641 .iter()
1642 .filter(|(_, d)| field.map_or(d.tokens > 0, |f| d.fields.has_dense(f)))
1643 .map(|(id, _)| id.clone())
1644 .collect();
1645 ids.sort();
1646 let mut hnsw = HNSWIndex::new(DistanceMetric::Dot, m, ef_construct, 16, dimension);
1647 const CHUNK: usize = 4096;
1652 for (chunk_idx, chunk_ids) in ids.chunks(CHUNK).enumerate() {
1653 let base = chunk_idx * CHUNK;
1654 let entries: Vec<(u64, Vec<f32>)> = chunk_ids
1655 .iter()
1656 .enumerate()
1657 .map(|(offset, id)| {
1658 let vector = if let Some(field) = field {
1659 self.dense_vector(&s, id, field)?
1660 } else {
1661 FixedVectorStore::get(
1662 s.record_fde(&s.documents[id]),
1663 s.documents[id].fde_location,
1664 dimension,
1665 )?
1666 }
1667 .to_vec();
1668 Ok::<_, IndexError>(((base + offset) as u64, vector))
1669 })
1670 .collect::<Result<Vec<_>, _>>()?;
1671 hnsw.par_insert_batch(&entries).map_err(|error| {
1672 IndexError::Invalid(format!("HNSW par_insert_batch failed: {error}"))
1673 })?;
1674 }
1675 if !ids.is_empty() {
1678 hnsw.reorder_rcm();
1679 }
1680 let built_generation = s.generation;
1681 drop(s);
1682 commit_boundary("ann_built_before_publish")?;
1683 #[cfg(test)]
1684 transaction_tests::before_ann_publish();
1685 let _writer = self.writer.lock().unwrap();
1686 let s = self.snapshot();
1687 if s.generation != built_generation {
1688 return Err(IndexError::Invalid(format!(
1689 "index generation moved during HNSW build ({} -> {}); retry",
1690 built_generation, s.generation,
1691 )));
1692 }
1693 let count = ids.len();
1694 self.save_ann(field, built_generation, &ids, &hnsw);
1697 let by_id = ids
1698 .iter()
1699 .enumerate()
1700 .map(|(idx, id)| (id.clone(), idx as u64))
1701 .collect();
1702 let mut payloads = HashMap::with_capacity(ids.len());
1703 let mut payload_index = PayloadIndex::new();
1704 for (point, id) in ids.iter().enumerate() {
1705 let payload = retrieval::metadata_ann_payload(&s.documents[id].metadata);
1706 payload_index.insert(point as u64, &payload);
1707 payloads.insert(point as u64, payload);
1708 }
1709 let mut next = (*s).clone();
1710 let ann = FdeAnn {
1711 base: Arc::new(FdeAnnBase {
1712 index: hnsw,
1713 ids,
1714 by_id,
1715 field: field.map(str::to_owned),
1716 payloads,
1717 payload_index,
1718 }),
1719 delta: HashSet::new(),
1720 tombstones: HashSet::new(),
1721 generation: built_generation,
1722 };
1723 if let Some(field) = field {
1724 next.named_ann.insert(field.to_owned(), ann);
1725 } else {
1726 next.fde_ann = Some(ann);
1727 }
1728 *self.state.write().unwrap() = Arc::new(next);
1729 Ok(count)
1730 }
1731 pub fn query_with_fde_ann(
1732 &self,
1733 vectors: &[Vector],
1734 top_k: usize,
1735 candidates: Option<usize>,
1736 ef_search: usize,
1737 ) -> Result<Vec<Hit>, IndexError> {
1738 self.validate(vectors)?;
1739 if top_k == 0 || ef_search == 0 {
1740 return Err(IndexError::Invalid(
1741 "top_k and ef_search must be positive".into(),
1742 ));
1743 }
1744 let normalized: Vec<_> = vectors.iter().map(|v| normalize(v)).collect();
1745 let query_fde = self.fde.encode_query(&normalized);
1746 let s = self.snapshot();
1747 let count = candidates.unwrap_or(top_k.saturating_mul(8)).max(top_k);
1748 let approximate = self.ann_fde_scores(&s, &query_fde, count, ef_search)?;
1749 self.rescore(&s, &normalized, approximate, top_k, candidates)
1750 }
1751 fn ann_fde_scores(
1752 &self,
1753 s: &State,
1754 query_fde: &Vector,
1755 count: usize,
1756 ef_search: usize,
1757 ) -> Result<Vec<(String, f32)>, IndexError> {
1758 self.ann_fde_scores_filtered(s, query_fde, count, ef_search, None, None)
1759 }
1760
1761 fn ann_fde_scores_filtered(
1762 &self,
1763 s: &State,
1764 query_fde: &Vector,
1765 count: usize,
1766 ef_search: usize,
1767 filter: Option<&Filter>,
1768 eligible: Option<&retrieval::DocSet>,
1769 ) -> Result<Vec<(String, f32)>, IndexError> {
1770 let ann = s.fde_ann.as_ref().ok_or_else(|| {
1771 IndexError::Invalid("FDE ANN is not built; call /v1/fde/index".into())
1772 })?;
1773 self.ann_scores_filtered(s, ann, query_fde, count, ef_search, filter, eligible)
1774 }
1775 fn ann_scores_filtered(
1776 &self,
1777 s: &State,
1778 ann: &FdeAnn,
1779 query_fde: &Vector,
1780 count: usize,
1781 ef_search: usize,
1782 filter: Option<&Filter>,
1783 eligible: Option<&retrieval::DocSet>,
1784 ) -> Result<Vec<(String, f32)>, IndexError> {
1785 if ann.generation != s.generation {
1786 return Err(IndexError::Invalid(
1787 "FDE ANN generation is stale; rebuild".into(),
1788 ));
1789 }
1790 let mut scores = Vec::new();
1791 if ann.base.ids.len() > ann.tombstones.len() {
1792 let base_count = count
1794 .saturating_add(ann.tombstones.len())
1795 .min(ann.base.ids.len());
1796 let options = SearchRuntimeOptions {
1797 ef_search: Some(ef_search.max(base_count)),
1798 ..SearchRuntimeOptions::default()
1799 };
1800 let points = match filter {
1801 Some(filter) => ann.base.index.in_place_filtered_search(
1802 query_fde,
1803 base_count,
1804 &options,
1805 &ann.base.payloads,
1806 &ann.base.payload_index,
1807 Some(filter),
1808 ),
1809 None => ann
1810 .base
1811 .index
1812 .search_with_options(query_fde, base_count, &options),
1813 }
1814 .map_err(|error| IndexError::Invalid(format!("HNSW search failed: {error}")))?;
1815 for point in points {
1816 if ann.tombstones.contains(&point.id) {
1817 continue;
1818 }
1819 let id = ann
1820 .base
1821 .ids
1822 .get(point.id as usize)
1823 .ok_or_else(|| IndexError::Invalid("invalid HNSW point id".into()))?;
1824 if !s.documents.contains_key(id) || ann.delta.contains(id) {
1825 return Err(IndexError::Invalid(
1826 "FDE ANN overlay is inconsistent; rebuild".into(),
1827 ));
1828 }
1829 scores.push((id.clone(), -point.sort_key));
1830 }
1831 }
1832 if !ann.delta.is_empty() {
1833 for id in &ann.delta {
1834 if eligible
1835 .is_some_and(|eligible| !s.retrieval.contains_external(eligible, id.as_str()))
1836 {
1837 continue;
1838 }
1839 let record = s
1840 .documents
1841 .get(id)
1842 .ok_or_else(|| IndexError::Invalid("missing delta document".into()))?;
1843 let vector = if let Some(field) = &ann.base.field {
1844 self.dense_vector(s, id, field)?
1845 } else {
1846 FixedVectorStore::get(
1847 s.record_fde(record),
1848 record.fde_location,
1849 self.fde.output_dimension(),
1850 )?
1851 };
1852 scores.push((id.clone(), dot(query_fde, vector)));
1853 }
1854 }
1855 let by_score =
1856 |a: &(String, f32), b: &(String, f32)| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0));
1857 if scores.len() > count {
1858 scores.select_nth_unstable_by(count, by_score);
1859 scores.truncate(count);
1860 }
1861 scores.sort_unstable_by(by_score);
1862 Ok(scores)
1863 }
1864 pub fn ann_fde_candidates(
1866 &self,
1867 vectors: &[Vector],
1868 count: usize,
1869 ef_search: usize,
1870 ) -> Result<Vec<CandidateHit>, IndexError> {
1871 self.validate(vectors)?;
1872 if count == 0 || ef_search == 0 {
1873 return Err(IndexError::Invalid(
1874 "candidate count and ef_search must be positive".into(),
1875 ));
1876 }
1877 let normalized: Vec<_> = vectors.iter().map(|vector| normalize(vector)).collect();
1878 let query_fde = self.fde.encode_query(&normalized);
1879 let s = self.snapshot();
1880 Ok(self
1881 .ann_fde_scores(&s, &query_fde, count, ef_search)?
1882 .into_iter()
1883 .map(|(id, score)| CandidateHit { id, score })
1884 .collect())
1885 }
1886 pub fn query_with_probes(
1887 &self,
1888 vectors: &[Vector],
1889 top_k: usize,
1890 candidates: Option<usize>,
1891 probes: usize,
1892 ) -> Result<Vec<Hit>, IndexError> {
1893 self.validate(vectors)?;
1894 if top_k == 0 {
1895 return Err(IndexError::Invalid("top_k must be positive".into()));
1896 }
1897 let normalized: Vec<_> = vectors.iter().map(|vector| normalize(vector)).collect();
1898 let s = self.snapshot();
1899 if s.codebook.is_empty() {
1900 return Err(IndexError::Invalid("index is untrained".into()));
1901 }
1902 let interaction: Vec<Vec<f32>> = normalized
1904 .iter()
1905 .map(|q| s.codebook.iter().map(|c| dot(q, c)).collect())
1906 .collect();
1907 let mut selected = HashSet::new();
1908 for row in &interaction {
1909 let mut scored: Vec<_> = row.iter().copied().enumerate().collect();
1910 scored.sort_by(|a, b| b.1.total_cmp(&a.1));
1911 for &(c, _) in scored.iter().take(probes.max(1)) {
1912 selected.insert(c);
1913 }
1914 }
1915 let mut candidate_ids = HashSet::new();
1916 for c in selected {
1917 candidate_ids.extend(s.postings[c].iter().cloned());
1918 }
1919 let mut approx: Vec<_> = candidate_ids
1920 .into_iter()
1921 .map(|id| {
1922 let d = &s.documents[&id];
1923 let score = interaction
1924 .iter()
1925 .map(|row| {
1926 d.unique_centroids
1927 .iter()
1928 .map(|&c| row[c as usize])
1929 .fold(f32::NEG_INFINITY, f32::max)
1930 })
1931 .sum::<f32>();
1932 (id, score)
1933 })
1934 .collect();
1935 approx.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
1936 let mut hits = self.rescore(&s, &normalized, approx, top_k, candidates)?;
1937 for hit in &mut hits {
1938 hit.fde_score = None;
1939 }
1940 Ok(hits)
1941 }
1942 fn rescore(
1943 &self,
1944 s: &State,
1945 normalized: &[Vector],
1946 approximate: Vec<(String, f32)>,
1947 top_k: usize,
1948 candidates: Option<usize>,
1949 ) -> Result<Vec<Hit>, IndexError> {
1950 if approximate.is_empty() {
1951 return Ok(Vec::new());
1952 }
1953 let count = candidates.unwrap_or(top_k.saturating_mul(8)).max(top_k);
1954 use std::sync::OnceLock;
1958 static TIMING: OnceLock<bool> = OnceLock::new();
1959 let timing = *TIMING.get_or_init(|| std::env::var("MULTIVECTOR_TIMING").is_ok());
1960 let decode_ns = std::sync::atomic::AtomicU64::new(0);
1961 let maxsim_ns = std::sync::atomic::AtomicU64::new(0);
1962 thread_local! {
1969 static DECODE_SCRATCH: std::cell::RefCell<Vec<f32>> =
1970 const { std::cell::RefCell::new(Vec::new()) };
1971 }
1972 let prepared = MaxSimQuery::new(normalized, self.config.dimension);
1980 let approximate_slice: &[(String, f32)] = approximate.as_slice();
1981 let scored: Vec<(usize, f32)> = approximate_slice
1982 .par_iter()
1983 .take(count)
1984 .enumerate()
1985 .map(|(idx, (id, _))| -> Result<(usize, f32), io::Error> {
1986 let record = &s.documents[id];
1987 let t0 = if timing {
1988 Some(std::time::Instant::now())
1989 } else {
1990 None
1991 };
1992 let (score, t1) = DECODE_SCRATCH.with(
1993 |cell| -> Result<(f32, Option<std::time::Instant>), io::Error> {
1994 let mut scratch = cell.borrow_mut();
1995 let dim = CompressedVectorStore::decode_into(
1996 s.record_objects(record),
1997 record.location,
1998 &s.codebook,
1999 &s.residual_codebook,
2000 &mut scratch,
2001 )?;
2002 let t1 = if timing {
2003 Some(std::time::Instant::now())
2004 } else {
2005 None
2006 };
2007 Ok((prepared.score(&scratch, dim), t1))
2008 },
2009 )?;
2010 let t2 = if timing {
2011 Some(std::time::Instant::now())
2012 } else {
2013 None
2014 };
2015 if let (Some(t0), Some(t1), Some(t2)) = (t0, t1, t2) {
2016 decode_ns.fetch_add(
2017 t1.duration_since(t0).as_nanos() as u64,
2018 std::sync::atomic::Ordering::Relaxed,
2019 );
2020 maxsim_ns.fetch_add(
2021 t2.duration_since(t1).as_nanos() as u64,
2022 std::sync::atomic::Ordering::Relaxed,
2023 );
2024 }
2025 Ok((idx, score))
2026 })
2027 .collect::<Result<Vec<_>, io::Error>>()?;
2028 if timing {
2029 let d = decode_ns.load(std::sync::atomic::Ordering::Relaxed);
2030 let m = maxsim_ns.load(std::sync::atomic::Ordering::Relaxed);
2031 eprintln!(
2032 "[timing] candidates={count} decode_us={} maxsim_us={} (per-thread aggregate)",
2033 d / 1000,
2034 m / 1000
2035 );
2036 }
2037 let mut scored = scored;
2042 let n = scored.len();
2043 let by_score_desc = |a: &(usize, f32), b: &(usize, f32)| {
2044 b.1.total_cmp(&a.1)
2045 .then_with(|| approximate_slice[a.0].0.cmp(&approximate_slice[b.0].0))
2046 };
2047 if top_k < n {
2048 scored.select_nth_unstable_by(top_k, by_score_desc);
2049 scored.truncate(top_k);
2050 }
2051 scored.sort_unstable_by(by_score_desc);
2052 let hits: Vec<Hit> = scored
2053 .into_iter()
2054 .map(|(idx, score)| {
2055 let (id, fde) = &approximate_slice[idx];
2056 let record = &s.documents[id];
2057 Hit {
2058 id: id.clone(),
2059 score,
2060 fde_score: Some(*fde),
2061 metadata: record.metadata.clone(),
2062 }
2063 })
2064 .collect();
2065 Ok(hits)
2066 }
2067 fn new_segment(&self, generation: u64) -> Result<Arc<SegmentStores>, IndexError> {
2068 let mut id = generation
2069 .checked_add(1)
2070 .ok_or_else(|| IndexError::Invalid("generation exhausted".into()))?;
2071 let parent = self.root.join("segments");
2072 fs::create_dir_all(&parent)?;
2073 let root = loop {
2074 let path = parent.join(id.to_string());
2075 match fs::create_dir(&path) {
2076 Ok(()) => break path,
2077 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
2078 id = id
2079 .checked_add(1)
2080 .ok_or_else(|| IndexError::Invalid("segment IDs exhausted".into()))?;
2081 }
2082 Err(e) => return Err(e.into()),
2083 }
2084 };
2085 let stores = Arc::new(SegmentStores {
2086 objects: CompressedVectorStore::new(root.join("objects"))?,
2087 fde: FixedVectorStore::new(root.join("fde"))?,
2088 root,
2089 id: Some(id),
2090 retired: std::sync::atomic::AtomicBool::new(true),
2091 });
2092 Ok(stores)
2093 }
2094 fn rotate_segment(&self, next: &mut State) -> Result<(), IndexError> {
2095 let stores = self.new_segment(next.generation)?;
2096 let bounds = SegmentBoundaries {
2099 objects: next.object_bytes().len() as u64,
2100 fde: next.fde_bytes().len() as u64,
2101 };
2102 next.sealed.insert(
2103 next.stores.id.unwrap_or(0),
2104 Arc::new(SegmentSnapshot {
2105 stores: Arc::clone(&next.stores),
2106 objects: next.objects_map.take(),
2107 fde: next.fde_map.take(),
2108 bounds,
2109 }),
2110 );
2111 if self.durability == Durability::Fsync {
2112 for path in [
2113 stores.root.join("objects"),
2114 stores.root.join("fde"),
2115 stores.root.clone(),
2116 self.root.join("segments"),
2117 ] {
2118 File::open(path)?.sync_all()?;
2119 }
2120 File::open(&self.root)?.sync_all()?;
2121 }
2122 next.stores = stores;
2123 Ok(())
2124 }
2125 pub fn seal(&self) -> Result<(), IndexError> {
2126 let _writer = self.writer.lock().unwrap();
2127 let s = self.snapshot();
2128 let mut next = (*s).clone();
2129 self.rotate_segment(&mut next)?;
2130 self.commit(&s, next)
2131 }
2132 pub fn compact(&self) -> Result<serde_json::Value, IndexError> {
2135 let s = self.snapshot();
2136 let before = s.stores.objects.len()?
2137 + s.stores.fde.len()?
2138 + s.sealed
2139 .values()
2140 .map(|v| Ok::<_, io::Error>(v.stores.objects.len()? + v.stores.fde.len()?))
2141 .collect::<Result<Vec<_>, _>>()?
2142 .iter()
2143 .sum::<u64>();
2144 let stores = self.new_segment(s.generation)?;
2145 let mut next = (*s).clone();
2146 next.stores = Arc::clone(&stores);
2147 next.objects_map = None;
2148 next.fde_map = None;
2149 next.sealed.clear();
2150 for document in next.documents.values_mut() {
2151 if document.tokens > 0 {
2152 document.location = stores
2153 .objects
2154 .copy_record(s.record_objects(document), document.location)?;
2155 document.fde_location = stores
2156 .fde
2157 .copy_record(s.record_fde(document), document.fde_location)?;
2158 }
2159 let source = s.record_fde(document);
2160 Arc::make_mut(&mut document.fields).relocate(source, &stores.fde)?;
2161 document.storage_id = stores.id.unwrap();
2162 }
2163 if self.durability == Durability::Fsync {
2164 for path in [
2165 stores.root.join("objects"),
2166 stores.root.join("fde"),
2167 stores.root.clone(),
2168 self.root.join("segments"),
2169 ] {
2170 File::open(path)?.sync_all()?;
2171 }
2172 File::open(&self.root)?.sync_all()?;
2173 }
2174 commit_boundary("compaction_copied")?;
2175 let _writer = self.writer.lock().unwrap();
2176 if self.snapshot().generation != s.generation {
2177 return Err(IndexError::Invalid(
2178 "generation changed during compaction; retry".into(),
2179 ));
2180 }
2181 let result = self.commit(&s, next);
2182 if result.is_ok() || matches!(result, Err(IndexError::CommitUncertain(_))) {
2183 stores
2184 .retired
2185 .store(false, std::sync::atomic::Ordering::Relaxed);
2186 }
2187 if result.is_ok() {
2188 s.stores
2189 .retired
2190 .store(true, std::sync::atomic::Ordering::Relaxed);
2191 for old in s.sealed.values() {
2192 old.stores
2193 .retired
2194 .store(true, std::sync::atomic::Ordering::Relaxed);
2195 }
2196 }
2197 result?;
2198 let after = stores.objects.len()? + stores.fde.len()?;
2199 Ok(
2200 serde_json::json!({"generation":s.generation+1,"bytes_before":before,"bytes_after":after,"bytes_reclaimed":before.saturating_sub(after)}),
2201 )
2202 }
2203
2204 pub fn stats(&self) -> IndexStats {
2205 let s = self.snapshot();
2206 IndexStats {
2207 documents: s.documents.len(),
2208 generation: s.generation,
2209 token_vectors: s.documents.values().map(|d| d.tokens).sum(),
2210 compressed_bytes: s.documents.values().map(|d| d.compressed_bytes).sum(),
2211 centroids: s.codebook.len(),
2212 residual_bits: self.config.residual_bits,
2213 trained: !s.codebook.is_empty(),
2214 fde_dimension: self.fde.output_dimension(),
2215 fde_ann_nodes: s.fde_ann.as_ref().map_or(0, |ann| {
2216 ann.base.ids.len() - ann.tombstones.len() + ann.delta.len()
2217 }),
2218 fde_ann_base_nodes: s.fde_ann.as_ref().map_or(0, |ann| ann.base.ids.len()),
2219 fde_ann_delta_documents: s.fde_ann.as_ref().map_or(0, |ann| ann.delta.len()),
2220 fde_ann_tombstones: s.fde_ann.as_ref().map_or(0, |ann| ann.tombstones.len()),
2221 fde_encoding_version: self.fde.encoding_version(),
2222 storage_segments: s.sealed.len() + 1,
2223 dense_ann_fields: s
2224 .named_ann
2225 .iter()
2226 .map(|(field, ann)| {
2227 (
2228 field.clone(),
2229 ann.base.ids.len() - ann.tombstones.len() + ann.delta.len(),
2230 )
2231 })
2232 .collect(),
2233 }
2234 }
2235 pub fn score_uncompressed(
2237 &self,
2238 query: &[Vector],
2239 document: &[Vector],
2240 ) -> Result<f32, IndexError> {
2241 self.validate(query)?;
2242 self.validate(document)?;
2243 let query: Vec<_> = query.iter().map(|v| normalize(v)).collect();
2244 let document: Vec<_> = document.iter().map(|v| normalize(v)).collect();
2245 let flat: Vec<_> = document.into_iter().flatten().collect();
2246 Ok(maxsim_flat(&query, &flat, self.config.dimension))
2247 }
2248 pub fn score_compressed(&self, query: &[Vector], id: &str) -> Result<f32, IndexError> {
2250 self.validate(query)?;
2251 let query: Vec<_> = query.iter().map(|v| normalize(v)).collect();
2252 let s = self.snapshot();
2253 let record = s
2254 .documents
2255 .get(id)
2256 .ok_or_else(|| IndexError::Invalid(format!("unknown document: {id}")))?;
2257 let document = CompressedVectorStore::decode(
2258 s.record_objects(record),
2259 record.location,
2260 &s.codebook,
2261 &s.residual_codebook,
2262 )?;
2263 Ok(maxsim_flat(&query, &document.values, document.dimension))
2264 }
2265}
2266
2267fn validate_config_size(c: &IndexConfig) -> Result<(), IndexError> {
2268 const MAX_VALUES: usize = 16 * 1024 * 1024;
2270 let bounded = |n: Option<usize>| n.is_some_and(|n| n <= MAX_VALUES);
2271 let buckets = 1usize << c.fde_ksim;
2272 if c.centroids > u32::MAX as usize
2273 || !bounded(c.dimension.checked_mul(c.centroids))
2274 || !bounded(
2275 buckets
2276 .checked_mul(c.fde_projected)
2277 .and_then(|n| n.checked_mul(c.fde_repetitions)),
2278 )
2279 || !bounded(
2280 c.fde_projected
2281 .checked_add(c.fde_ksim)
2282 .and_then(|n| n.checked_mul(c.dimension))
2283 .and_then(|n| n.checked_mul(c.fde_repetitions)),
2284 )
2285 || !bounded(buckets.checked_mul(c.dimension))
2286 {
2287 return Err(IndexError::Invalid(
2288 "configuration exceeds checked 64 MiB workspace limit".into(),
2289 ));
2290 }
2291 Ok(())
2292}
2293
2294fn validate_codebooks(
2295 c: &IndexConfig,
2296 centers: &[Vector],
2297 residuals: &[f32],
2298 has_docs: bool,
2299) -> Result<(), IndexError> {
2300 if centers.is_empty() && residuals.is_empty() && !has_docs {
2301 return Ok(());
2302 }
2303 if centers.len() != c.centroids
2304 || residuals.len() != 1usize << c.residual_bits
2305 || centers
2306 .iter()
2307 .any(|v| v.len() != c.dimension || v.iter().any(|x| !x.is_finite()))
2308 || residuals.iter().any(|x| !x.is_finite())
2309 {
2310 return Err(IndexError::Invalid("invalid persisted codebook".into()));
2311 }
2312 Ok(())
2313}
2314
2315fn check_pruning_shape(
2316 top_k: usize,
2317 candidates: usize,
2318 rerank_candidates: usize,
2319) -> Result<(), IndexError> {
2320 if top_k == 0 || rerank_candidates < top_k || candidates < rerank_candidates {
2321 return Err(IndexError::Invalid(
2322 "require top_k > 0 and candidates >= rerank_candidates >= top_k".into(),
2323 ));
2324 }
2325 Ok(())
2326}
2327fn centroid_prune(
2328 s: &State,
2329 query: &[Vector],
2330 candidates: Vec<(String, f32)>,
2331 survivors: usize,
2332) -> Vec<(String, f32)> {
2333 let interaction: Vec<Vec<f32>> = query
2334 .iter()
2335 .map(|q| s.codebook.iter().map(|centroid| dot(q, centroid)).collect())
2336 .collect();
2337 let mut scored: Vec<_> = candidates
2338 .into_par_iter()
2339 .map(|(id, fde_score)| {
2340 let document = &s.documents[&id];
2341 let score = interaction
2342 .iter()
2343 .map(|row| {
2344 document
2345 .unique_centroids
2346 .iter()
2347 .map(|¢roid| row[centroid as usize])
2348 .fold(f32::NEG_INFINITY, f32::max)
2349 })
2350 .sum::<f32>();
2351 (id, fde_score, score)
2352 })
2353 .collect();
2354 scored.par_sort_unstable_by(|a, b| b.2.total_cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
2355 scored.truncate(survivors);
2356 scored
2357 .into_iter()
2358 .map(|(id, fde_score, _)| (id, fde_score))
2359 .collect()
2360}
2361fn nearest(vector: &Vector, centroids: &[Vector]) -> usize {
2362 centroids
2363 .iter()
2364 .enumerate()
2365 .min_by(|(_, a), (_, b)| distance(vector, a).total_cmp(&distance(vector, b)))
2366 .unwrap()
2367 .0
2368}
2369fn distance(a: &[f32], b: &[f32]) -> f32 {
2370 a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
2371}
2372fn train_scalar_codebook(values: &[f32], levels: usize, iterations: usize) -> Vec<f32> {
2373 let mut sorted = values.to_vec();
2374 sorted.sort_unstable_by(|a, b| a.total_cmp(b));
2375 let mut centers: Vec<_> = (0..levels)
2376 .map(|i| sorted[((2 * i + 1) * sorted.len() / (2 * levels)).min(sorted.len() - 1)])
2377 .collect();
2378 for _ in 0..iterations {
2379 let mut sums = vec![0.; levels];
2380 let mut counts = vec![0usize; levels];
2381 for &value in values {
2382 let index = centers
2383 .iter()
2384 .enumerate()
2385 .min_by(|(_, a), (_, b)| (value - **a).abs().total_cmp(&(value - **b).abs()))
2386 .unwrap()
2387 .0;
2388 sums[index] += value;
2389 counts[index] += 1;
2390 }
2391 for i in 0..levels {
2392 if counts[i] > 0 {
2393 centers[i] = sums[i] / counts[i] as f32;
2394 }
2395 }
2396 }
2397 centers.sort_unstable_by(|a, b| a.total_cmp(b));
2398 centers
2399}
2400
2401#[cfg(test)]
2402#[path = "transaction_tests.rs"]
2403mod transaction_tests;