use super::{format::DiskANNVectorVersion, DiskANNCanonicalRead};
use crate::{
mvcc::VersionError,
read_control::StorageReadControl,
vector_index::{
cosine_similarity_controlled,
query::{postings_from_unique_scores, VectorQueryBuffer},
validate_threshold, validate_vector_values_controlled,
},
StorageBackendResult,
};
use uqa_core::{DocId, PostingList};
pub(super) mod selection;
mod statistics;
pub use statistics::DiskANNScoringStats;
pub(crate) use statistics::ScoringStatsExt;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy)]
pub struct DiskANNDocumentScore {
document: DocId,
version: DiskANNVectorVersion,
vectors: u64,
score: f32,
}
impl DiskANNDocumentScore {
pub fn document(&self) -> DocId {
self.document
}
pub fn version(&self) -> DiskANNVectorVersion {
self.version
}
pub fn vector_count(&self) -> u64 {
self.vectors
}
pub fn raw_cosine(&self) -> f32 {
self.score
}
}
pub struct DiskANNCanonicalScorer<'a> {
source: &'a dyn DiskANNCanonicalRead,
query: &'a [f32],
control: &'a StorageReadControl,
}
impl<'a> DiskANNCanonicalScorer<'a> {
pub fn new(
source: &'a dyn DiskANNCanonicalRead,
query: &'a [f32],
control: &'a StorageReadControl,
) -> StorageBackendResult<Self> {
source.check_control(control)?;
if source.dimensions() == 0 {
return Err(invalid("canonical scoring requires positive dimensions"));
}
validate_vector_values_controlled(source.dimensions(), query, Some(control))?;
source.check_control(control)?;
Ok(Self {
source,
query,
control,
})
}
pub fn score_document(
&self,
document: DocId,
) -> StorageBackendResult<Option<DiskANNDocumentScore>> {
self.document(document).map(|(_, score)| score)
}
pub fn score_candidate(
&self,
document: DocId,
ordinal: u32,
version: DiskANNVectorVersion,
) -> StorageBackendResult<Option<DiskANNDocumentScore>> {
self.source.check_control(self.control)?;
if self.source.origin(document, self.control)? != Some(version) {
self.source.check_control(self.control)?;
return Ok(None);
}
let (actual, score) = self.document(document)?;
if actual != Some(version) {
return Err(invalid("canonical origin changed on a retained source"));
}
let score = score
.filter(|score| u64::from(ordinal) < score.vectors)
.ok_or_else(|| invalid("candidate ordinal is outside its canonical tensor"))?;
Ok(Some(score))
}
pub fn visit_scores(
&self,
visit: &mut dyn FnMut(DiskANNDocumentScore) -> StorageBackendResult<()>,
) -> StorageBackendResult<()> {
self.source.check_control(self.control)?;
let mut after = None;
while let Some(document) = self.source.next_document_after(after, self.control)? {
if after.is_some_and(|previous| document <= previous) {
return Err(invalid("canonical scoring cursor did not advance"));
}
let (version, score) = self.document(document)?;
if version.is_none() {
return Err(invalid("enumerated canonical document has no origin"));
}
if let Some(score) = score {
visit(score)?;
}
after = Some(document);
}
self.source.check_control(self.control)
}
pub fn search_exact_knn(&self, k: usize) -> StorageBackendResult<PostingList> {
self.search_exact_knn_with_stats(k)
.map(|(postings, _)| postings)
}
pub fn search_exact_knn_with_stats(
&self,
k: usize,
) -> StorageBackendResult<(PostingList, DiskANNScoringStats)> {
self.source.check_control(self.control)?;
let mut stats = DiskANNScoringStats::default();
if k == 0 {
return Ok((PostingList::new(), stats));
}
let mut selected = selection::TopK::new(k, self.control);
self.visit_scores(&mut |score| {
stats.record(score.vector_count())?;
selected.offer(score)
})?;
let result = selected.finish(self.control)?;
self.source.check_control(self.control)?;
Ok((result, stats))
}
pub fn search_threshold(&self, threshold: f32) -> StorageBackendResult<PostingList> {
self.search_threshold_with_stats(threshold)
.map(|(postings, _)| postings)
}
pub fn search_threshold_with_stats(
&self,
threshold: f32,
) -> StorageBackendResult<(PostingList, DiskANNScoringStats)> {
self.source.check_control(self.control)?;
validate_threshold(threshold)?;
let mut scores = VectorQueryBuffer::new(Some(self.control));
let mut stats = DiskANNScoringStats::default();
self.visit_scores(&mut |score| {
stats.record(score.vector_count())?;
if score.score >= threshold {
scores.push((score.document, score.score))?;
}
Ok(())
})?;
let result = postings_from_unique_scores(scores, None, Some(self.control))?;
self.source.check_control(self.control)?;
Ok((result, stats))
}
fn document(
&self,
document: DocId,
) -> StorageBackendResult<(Option<DiskANNVectorVersion>, Option<DiskANNDocumentScore>)> {
self.source.check_control(self.control)?;
let mut version = None;
let mut best: Option<f32> = None;
let mut count = 0_u64;
let mut failure = None;
let returned =
self.source
.visit_document(document, self.control, &mut |ordinal, current, raw| {
if failure.is_none() {
let result = (|| {
self.source.check_control(self.control)?;
if u64::from(ordinal) != count
|| version.is_some_and(|version| version != current)
{
return Err(invalid(
"canonical tensor identity changed while scoring",
));
}
validate_vector_values_controlled(
self.source.dimensions(),
raw,
Some(self.control),
)?;
let score = cosine_similarity_controlled(self.query, raw, || {
self.source.check_control(self.control)
})?;
if best.is_none_or(|best| score.total_cmp(&best).is_gt()) {
best = Some(score);
}
version = Some(current);
count += 1;
Ok(())
})();
if let Err(error) = result {
failure = Some(error);
}
}
if failure.is_some() {
Err(invalid("canonical score consumer rejected an ordinal"))
} else {
Ok(())
}
});
if let Some(error) = failure {
return Err(error);
}
let returned = returned?;
self.source.check_control(self.control)?;
if count != 0 && returned != version {
return Err(invalid("canonical tensor returned a different origin"));
}
let score = best.map(|score| DiskANNDocumentScore {
document,
version: version.expect("a scored ordinal has an origin"),
vectors: count,
score,
});
Ok((returned, score))
}
}
fn invalid(message: &'static str) -> crate::StorageBackendError {
VersionError::InvalidEncoding(message).into_storage_error()
}