use std::sync::Arc;
use uqa_core::PostingList;
use super::{
pages::{DiskANNOriginReader, DiskANNPageSource, DiskANNReadLimits, DiskANNReader},
search::{DiskANNTraversal, DiskANNTraversalStats},
DiskANNCanonicalCounts, DiskANNCanonicalRead, DiskANNCanonicalScorer,
DiskANNCanonicalVectorVisitor, DiskANNQueryRead, ExactVectorReason, NavigationInput,
};
use crate::{
read_control::StorageReadControl, vector_index::DiskANNIndexParams, StorageBackendResult,
};
mod candidates;
mod matching;
mod retained;
mod selection;
mod statistics;
mod values;
pub use retained::RetainedDiskANNIndex;
pub use selection::{DiskANNReadChanges, DiskANNReadSnapshot};
pub use statistics::DiskANNQueryWork;
#[cfg(test)]
mod tests;
pub struct DiskANNQueryResult {
pub postings: PostingList,
pub exact_reason: Option<ExactVectorReason>,
pub traversal: DiskANNTraversalStats,
pub work: DiskANNQueryWork,
pub populations: Option<DiskANNCanonicalCounts>,
}
pub struct DiskANNQuery<'a> {
canonical: &'a dyn DiskANNQueryRead,
reader: DiskANNReader,
origins: DiskANNOriginReader,
control: StorageReadControl,
}
impl<'a> DiskANNQuery<'a> {
pub fn open(
canonical: &'a dyn DiskANNQueryRead,
source: Arc<dyn DiskANNPageSource>,
parameters: DiskANNIndexParams,
limits: DiskANNReadLimits,
control: &StorageReadControl,
) -> StorageBackendResult<Self> {
canonical.check_control(control)?;
let reader = DiskANNReader::open(
source.clone(),
canonical.dimensions(),
parameters,
limits,
control,
)?;
let origins = DiskANNOriginReader::open(source, limits.max_record_bytes, control)?;
if reader.manifest() != origins.manifest() {
return Err(invalid(
"physical manifest changed while preparing the query",
));
}
canonical.check_control(control)?;
Ok(Self {
canonical,
reader,
origins,
control: control.clone(),
})
}
pub fn search_knn(
&self,
query: &[f32],
k: usize,
control: &StorageReadControl,
) -> StorageBackendResult<DiskANNQueryResult> {
self.check(control)?;
let scorer = DiskANNCanonicalScorer::new(self, query, control)?;
if k == 0 {
self.check(control)?;
return Ok(result(
PostingList::new(),
None,
DiskANNTraversalStats::default(),
DiskANNQueryWork::default(),
));
}
let navigation =
match NavigationInput::from_raw(self.canonical.dimensions(), query, control)? {
NavigationInput::Exact(reason) => {
let (postings, exact) =
if let Some(source) = self.canonical.unversioned_vectors() {
values::exact(&**source, query, k, control)?
} else {
scorer.search_exact_knn_with_stats(k)?
};
self.check(control)?;
return Ok(result(
postings,
Some(reason),
DiskANNTraversalStats::default(),
DiskANNQueryWork {
exact,
..DiskANNQueryWork::default()
},
));
}
NavigationInput::Navigable(navigation) => navigation,
};
let mut traversal = DiskANNTraversal::new(self.reader.clone(), &navigation, control)?;
drop(navigation);
let mut candidates = candidates::Candidates::new(self, scorer, k, control);
loop {
self.check(control)?;
let nodes = traversal.next_beam()?;
if nodes.is_empty() {
break;
}
candidates.nodes(&nodes)?;
}
candidates.side()?;
candidates.changes()?;
let mut raw_vectors = None;
if let Some(source) = self.canonical.unversioned_vectors() {
let mut current = 0_u64;
values::visit(&**source, control, &mut |document| {
self.check(control)?;
let vectors =
if let Some(origin) = self.canonical.document_origin(document, control)? {
origin.count()
} else {
let (score, vectors) = values::score(&**source, document, query, control)?;
if let Some(score) = score {
candidates.offer_unversioned(document, score, vectors)?;
}
vectors
};
current = current
.checked_add(vectors)
.ok_or_else(|| invalid("raw canonical population overflow"))?;
Ok(())
})?;
raw_vectors = Some(current);
}
while candidates.len() < k {
self.check(control)?;
let nodes = traversal.complete_next_beam()?;
if nodes.is_empty() {
break;
}
candidates.nodes(&nodes)?;
}
let mut work = candidates.work();
work.pages = traversal.page_stats();
let postings = candidates.finish()?;
self.check(control)?;
let mut result = result(postings, None, traversal.stats(), work);
if let Some(current) = raw_vectors {
let changed = work
.changed
.vectors
.checked_add(work.unversioned.vectors)
.ok_or_else(|| invalid("raw uncovered population overflow"))?;
result.populations = Some(DiskANNCanonicalCounts::new(current, changed)?);
}
Ok(result)
}
pub fn search_threshold(
&self,
query: &[f32],
threshold: f32,
control: &StorageReadControl,
) -> StorageBackendResult<PostingList> {
self.search_threshold_with_stats(query, threshold, control)
.map(|result| result.postings)
}
pub fn search_threshold_with_stats(
&self,
query: &[f32],
threshold: f32,
control: &StorageReadControl,
) -> StorageBackendResult<DiskANNQueryResult> {
self.check(control)?;
let scorer = DiskANNCanonicalScorer::new(self, query, control)?;
let (postings, exact) = if let Some(source) = self.canonical.unversioned_vectors() {
values::threshold(&**source, query, threshold, control)?
} else {
scorer.search_threshold_with_stats(threshold)?
};
self.check(control)?;
Ok(result(
postings,
None,
DiskANNTraversalStats::default(),
DiskANNQueryWork {
exact,
..DiskANNQueryWork::default()
},
))
}
fn check(&self, control: &StorageReadControl) -> StorageBackendResult<()> {
self.control.check()?;
self.canonical.check_control(control)
}
}
impl DiskANNCanonicalRead for DiskANNQuery<'_> {
fn check_control(&self, control: &StorageReadControl) -> StorageBackendResult<()> {
self.check(control)
}
fn dimensions(&self) -> u32 {
self.canonical.dimensions()
}
fn next_document_after(
&self,
after: Option<uqa_core::DocId>,
control: &StorageReadControl,
) -> StorageBackendResult<Option<uqa_core::DocId>> {
self.check(control)?;
let result = self.canonical.next_document_after(after, control)?;
self.check(control)?;
Ok(result)
}
fn origin(
&self,
document: uqa_core::DocId,
control: &StorageReadControl,
) -> StorageBackendResult<Option<super::format::DiskANNVectorVersion>> {
self.check(control)?;
let result = self.canonical.origin(document, control)?;
self.check(control)?;
Ok(result)
}
fn visit_document(
&self,
document: uqa_core::DocId,
control: &StorageReadControl,
visit: &mut DiskANNCanonicalVectorVisitor<'_>,
) -> StorageBackendResult<Option<super::format::DiskANNVectorVersion>> {
self.check(control)?;
let result =
self.canonical
.visit_document(document, control, &mut |ordinal, version, raw| {
self.check(control)?;
visit(ordinal, version, raw)
})?;
self.check(control)?;
Ok(result)
}
}
fn result(
postings: PostingList,
exact_reason: Option<ExactVectorReason>,
traversal: DiskANNTraversalStats,
work: DiskANNQueryWork,
) -> DiskANNQueryResult {
DiskANNQueryResult {
postings,
exact_reason,
traversal,
work,
populations: None,
}
}
fn invalid(message: &'static str) -> crate::StorageBackendError {
crate::mvcc::VersionError::InvalidEncoding(message).into_storage_error()
}