uqa_storage/vector_index/
retained.rs1use std::sync::Arc;
10
11use uqa_core::{
12 memory::{Budgeted, BudgetedVec},
13 DocId, Payload, PostingEntry, PostingList,
14};
15
16use crate::read_control::StorageReadControl;
17use crate::vector_index::{
18 cosine_similarity, select_top_k_scored, validate_vector_values, VectorIndex,
19};
20use crate::{StorageBackendError, StorageBackendResult};
21use sha2::{Digest, Sha256};
22
23mod builder;
24mod canonical;
25pub use builder::RetainedVectorIndexBuilder;
26
27pub(crate) type VectorEntries = Vec<(DocId, u32, Vec<f32>)>;
28
29#[derive(Clone)]
31pub struct RetainedVectorIndex {
32 entries: Arc<Budgeted<VectorEntries>>,
33 dimensions: u32,
34 index_kind: &'static str,
35 control: StorageReadControl,
36 fingerprint: [u8; 32],
37}
38
39impl RetainedVectorIndex {
40 pub(crate) fn from_entries(
41 entries: Budgeted<VectorEntries>,
42 dimensions: u32,
43 index_kind: &'static str,
44 control: &StorageReadControl,
45 ) -> StorageBackendResult<Self> {
46 control.check()?;
47 let mut digest = Sha256::new();
48 digest.update(b"uqa-retained-vector-values-v1\0");
49 digest.update(dimensions.to_le_bytes());
50 for (document, ordinal, vector) in entries.iter() {
51 control.check()?;
52 digest.update(document.to_le_bytes());
53 digest.update(ordinal.to_le_bytes());
54 digest.update((vector.len() as u64).to_le_bytes());
55 for chunk in vector.chunks(1024) {
56 control.check()?;
57 for value in chunk {
58 digest.update(value.to_bits().to_le_bytes());
59 }
60 }
61 }
62 Ok(Self {
63 entries: entries.into_shared()?,
64 dimensions,
65 index_kind,
66 control: control.clone(),
67 fingerprint: digest.finalize().into(),
68 })
69 }
70
71 fn scores(
72 &self,
73 query: &[f32],
74 threshold: Option<f32>,
75 ) -> StorageBackendResult<BudgetedVec<(DocId, f32)>> {
76 let mut scores = BudgetedVec::new(self.control.memory());
77 for vectors in self.entries.chunk_by(|a, b| a.0 == b.0) {
78 self.control.check()?;
79 let mut best = cosine_similarity(query, &vectors[0].2);
80 for (_, _, vector) in &vectors[1..] {
81 self.control.check()?;
82 let score = cosine_similarity(query, vector);
83 if score.total_cmp(&best).is_gt() {
84 best = score;
85 }
86 }
87 if threshold.is_none_or(|threshold| best >= threshold) {
88 scores.push((vectors[0].0, best))?;
89 }
90 }
91 Ok(scores)
92 }
93
94 fn postings(&self, scores: &[(DocId, f32)]) -> StorageBackendResult<PostingList> {
95 let mut postings = BudgetedVec::new(self.control.memory());
96 postings.reserve(scores.len())?;
97 for &(doc_id, score) in scores {
98 self.control.check()?;
99 postings.push(PostingEntry::new(
100 doc_id,
101 Payload::with_score(f64::from(score)),
102 ))?;
103 }
104 let (postings, _memory) = postings.into_parts();
105 Ok(PostingList::from_sorted_unchecked(postings))
106 }
107}
108
109impl VectorIndex for RetainedVectorIndex {
110 fn contains_document(&self, doc_id: DocId) -> StorageBackendResult<bool> {
111 self.control.check()?;
112 Ok(self
113 .entries
114 .binary_search_by_key(&doc_id, |entry| entry.0)
115 .is_ok())
116 }
117
118 fn dimensions(&self) -> u32 {
119 self.dimensions
120 }
121 fn index_kind(&self) -> &'static str {
122 self.index_kind
123 }
124 fn add(&mut self, _: DocId, _: Vec<f32>) -> StorageBackendResult<()> {
125 Err(read_only())
126 }
127 fn add_many(&mut self, _: DocId, _: Vec<Vec<f32>>) -> StorageBackendResult<()> {
128 Err(read_only())
129 }
130 fn delete(&mut self, _: DocId) -> StorageBackendResult<()> {
131 Err(read_only())
132 }
133 fn clear(&mut self) -> StorageBackendResult<()> {
134 Err(read_only())
135 }
136 fn initialize(&mut self) -> StorageBackendResult<()> {
137 Err(read_only())
138 }
139 fn search_knn(&self, query: &[f32], k: usize) -> StorageBackendResult<PostingList> {
140 self.control.check()?;
141 validate_vector_values(self.dimensions, query)?;
142 if k == 0 {
143 return Ok(PostingList::new());
144 }
145 let mut scores = self.scores(query, None)?.into_parts();
147 select_top_k_scored(&mut scores.0, k);
148 scores.0.sort_unstable_by_key(|(doc_id, _)| *doc_id);
149 self.postings(&scores.0)
150 }
151 fn search_threshold(&self, query: &[f32], threshold: f32) -> StorageBackendResult<PostingList> {
152 self.control.check()?;
153 validate_vector_values(self.dimensions, query)?;
154 if !threshold.is_finite() {
155 return Err(StorageBackendError::Other(format!(
156 "vector similarity threshold must be finite, got {threshold}"
157 )));
158 }
159 let scores = self.scores(query, Some(threshold))?;
160 self.postings(&scores)
161 }
162 fn count(&self) -> StorageBackendResult<usize> {
163 self.control.check()?;
164 Ok(self.entries.len())
165 }
166 fn snapshot(&self) -> StorageBackendResult<Arc<dyn VectorIndex>> {
167 Ok(Arc::new(self.clone()))
168 }
169
170 fn vector_read_snapshot(
171 &self,
172 control: &StorageReadControl,
173 ) -> StorageBackendResult<Option<super::VectorReadSnapshot>> {
174 self.control.check()?;
175 control.check()?;
176 Ok(Some(
177 Budgeted::new(self.clone(), self.control.memory().empty_reservation()).into_shared()?,
178 ))
179 }
180}
181
182fn read_only() -> StorageBackendError {
183 StorageBackendError::Other("cannot write a retained vector snapshot".into())
184}
185
186#[cfg(test)]
187mod tests;