1use khive_score::DeterministicScore;
2use khive_storage::{StorageCapability, StorageError, VectorSearchHit, VectorSearchRequest};
3use khive_types::SubstrateKind;
4use uuid::Uuid;
5
6use crate::{KhiveRuntime, NamespaceToken, RuntimeError, RuntimeResult};
7
8impl KhiveRuntime {
9 pub(super) async fn hybrid_vector_stage(
13 &self,
14 token: &NamespaceToken,
15 query_text: &str,
16 query_vector: Option<Vec<f32>>,
17 candidates: u32,
18 vector_similarity_floor: Option<f64>,
19 tolerate_vector_error: bool,
20 ) -> RuntimeResult<(Vec<VectorSearchHit>, Option<String>)> {
21 khive_storage::await_request_read_phase("hybrid_search.vector", async {
22 let mut vector_error: Option<String> = None;
23 let mut vector_hits =
24 if query_vector.is_some() || !self.default_embedder_name().is_empty() {
25 match self
26 .vector_search(
27 token,
28 query_vector,
29 Some(query_text),
30 candidates,
31 Some(SubstrateKind::Entity),
32 )
33 .await
34 {
35 Ok(hits) => hits,
36 Err(e) if tolerate_vector_error => {
37 vector_error = Some(e.to_string());
38 Vec::new()
39 }
40 Err(e) => return Err(e),
41 }
42 } else {
43 Vec::new()
44 };
45 if let Some(cosine_floor) = vector_similarity_floor {
46 let score_floor = DeterministicScore::from_f64(cosine_floor);
49 vector_hits.retain(|hit| hit.score >= score_floor);
50 }
51 Ok((vector_hits, vector_error))
52 })
53 .await?
54 }
55
56 pub async fn vector_search(
62 &self,
63 token: &NamespaceToken,
64 query_embedding: Option<Vec<f32>>,
65 query_text: Option<&str>,
66 top_k: u32,
67 kind: Option<SubstrateKind>,
68 ) -> RuntimeResult<Vec<VectorSearchHit>> {
69 validate_query_input(query_embedding.as_deref(), query_text)?;
70 let engine = self.require_default_embedder()?;
71 self.vector_search_in(token, engine, query_embedding, query_text, top_k, kind)
72 .await
73 }
74
75 #[allow(clippy::too_many_arguments)]
83 pub async fn vector_search_in(
84 &self,
85 token: &NamespaceToken,
86 engine: &str,
87 query_embedding: Option<Vec<f32>>,
88 query_text: Option<&str>,
89 top_k: u32,
90 kind: Option<SubstrateKind>,
91 ) -> RuntimeResult<Vec<VectorSearchHit>> {
92 validate_query_input(query_embedding.as_deref(), query_text)?;
93 let (engine, dimensions) = self.vector_model_metadata(engine)?;
94 let embedding = match query_embedding {
95 Some(vector) => vector,
96 None => {
97 let text = query_text.ok_or_else(missing_query)?;
98 self.embed_query_with_model_for_token(token, &engine, text)
99 .await?
100 }
101 };
102 validate_vector(&engine, dimensions, &embedding, "vec_search")?;
103 let hits = self
104 .vectors_for_model(token, &engine)?
105 .search(VectorSearchRequest {
106 query_vectors: vec![embedding],
107 top_k,
108 namespace: Some(token.namespace().as_str().to_owned()),
109 kind,
110 embedding_model: None,
111 filter: None,
112 backend_hints: None,
113 })
114 .await;
115 crate::usage::count(crate::usage::UsageUnit::VectorPasses, 1);
116 hits.map_err(RuntimeError::from)
117 }
118
119 pub async fn knn(
122 &self,
123 token: &NamespaceToken,
124 query_vector: Vec<f32>,
125 top_k: u32,
126 ) -> RuntimeResult<Vec<VectorSearchHit>> {
127 let engine = self.require_default_embedder()?;
128 self.knn_in(token, engine, query_vector, top_k).await
129 }
130
131 pub async fn knn_in(
135 &self,
136 token: &NamespaceToken,
137 engine: &str,
138 query_vector: Vec<f32>,
139 top_k: u32,
140 ) -> RuntimeResult<Vec<VectorSearchHit>> {
141 let (engine, dimensions) = self.vector_model_metadata(engine)?;
142 validate_vector(&engine, dimensions, &query_vector, "vec_search")?;
143 Ok(self
144 .vectors_for_model(token, &engine)?
145 .search(VectorSearchRequest {
146 query_vectors: vec![query_vector],
147 top_k,
148 namespace: Some(token.namespace().as_str().to_owned()),
149 kind: Some(SubstrateKind::Entity),
150 embedding_model: None,
151 filter: None,
152 backend_hints: None,
153 })
154 .await?)
155 }
156
157 pub async fn rerank(
160 &self,
161 token: &NamespaceToken,
162 query_vector: &[f32],
163 candidate_ids: &[Uuid],
164 top_k: u32,
165 ) -> RuntimeResult<Vec<VectorSearchHit>> {
166 let engine = self.require_default_embedder()?;
167 self.rerank_in(token, engine, query_vector, candidate_ids, top_k)
168 .await
169 }
170
171 pub async fn rerank_in(
179 &self,
180 token: &NamespaceToken,
181 engine: &str,
182 query_vector: &[f32],
183 candidate_ids: &[Uuid],
184 top_k: u32,
185 ) -> RuntimeResult<Vec<VectorSearchHit>> {
186 let (engine, dimensions) = self.vector_model_metadata(engine)?;
187 validate_vector(&engine, dimensions, query_vector, "score_candidates")?;
188 let mut hits = self
189 .vectors_for_model(token, &engine)?
190 .score_candidates(query_vector, candidate_ids, Some(SubstrateKind::Entity))
191 .await?;
192 hits.truncate(top_k as usize);
193 Ok(hits)
194 }
195}
196
197fn missing_query() -> RuntimeError {
198 RuntimeError::InvalidInput("vector search requires query_embedding or query_text".into())
199}
200
201fn validate_query_input(vector: Option<&[f32]>, text: Option<&str>) -> RuntimeResult<()> {
202 if vector.is_none() {
203 let text = text.ok_or_else(missing_query)?;
204 if text.trim().is_empty() {
205 return Err(RuntimeError::InvalidInput(
206 "query_text must not be empty".into(),
207 ));
208 }
209 }
210 Ok(())
211}
212
213fn validate_vector(
214 engine: &str,
215 dimensions: usize,
216 vector: &[f32],
217 operation: &'static str,
218) -> RuntimeResult<()> {
219 let message = if vector.len() != dimensions {
220 Some(format!(
221 "engine '{engine}' query has {} dims, expected {dimensions}",
222 vector.len()
223 ))
224 } else {
225 vector
226 .iter()
227 .position(|coordinate| !coordinate.is_finite())
228 .map(|index| {
229 format!("engine '{engine}' query has a nonfinite coordinate at index {index}")
230 })
231 };
232 if let Some(message) = message {
233 return Err(StorageError::InvalidInput {
234 capability: StorageCapability::Vectors,
235 operation: operation.into(),
236 message,
237 }
238 .into());
239 }
240 Ok(())
241}