1use khive_storage::{StorageCapability, StorageError, VectorSearchHit, VectorSearchRequest};
2use khive_types::SubstrateKind;
3use uuid::Uuid;
4
5use crate::{KhiveRuntime, NamespaceToken, RuntimeError, RuntimeResult};
6
7impl KhiveRuntime {
8 pub async fn vector_search(
14 &self,
15 token: &NamespaceToken,
16 query_embedding: Option<Vec<f32>>,
17 query_text: Option<&str>,
18 top_k: u32,
19 kind: Option<SubstrateKind>,
20 ) -> RuntimeResult<Vec<VectorSearchHit>> {
21 validate_query_input(query_embedding.as_deref(), query_text)?;
22 let engine = self.require_default_embedder()?;
23 self.vector_search_in(token, engine, query_embedding, query_text, top_k, kind)
24 .await
25 }
26
27 #[allow(clippy::too_many_arguments)]
35 pub async fn vector_search_in(
36 &self,
37 token: &NamespaceToken,
38 engine: &str,
39 query_embedding: Option<Vec<f32>>,
40 query_text: Option<&str>,
41 top_k: u32,
42 kind: Option<SubstrateKind>,
43 ) -> RuntimeResult<Vec<VectorSearchHit>> {
44 validate_query_input(query_embedding.as_deref(), query_text)?;
45 let (engine, dimensions) = self.vector_model_metadata(engine)?;
46 let embedding = match query_embedding {
47 Some(vector) => vector,
48 None => {
49 let text = query_text.ok_or_else(missing_query)?;
50 self.embed_query_with_model_for_token(token, &engine, text)
51 .await?
52 }
53 };
54 validate_vector(&engine, dimensions, &embedding, "vec_search")?;
55 let hits = self
56 .vectors_for_model(token, &engine)?
57 .search(VectorSearchRequest {
58 query_vectors: vec![embedding],
59 top_k,
60 namespace: Some(token.namespace().as_str().to_owned()),
61 kind,
62 embedding_model: None,
63 filter: None,
64 backend_hints: None,
65 })
66 .await;
67 crate::usage::count(crate::usage::UsageUnit::VectorPasses, 1);
68 hits.map_err(RuntimeError::from)
69 }
70
71 pub async fn knn(
74 &self,
75 token: &NamespaceToken,
76 query_vector: Vec<f32>,
77 top_k: u32,
78 ) -> RuntimeResult<Vec<VectorSearchHit>> {
79 let engine = self.require_default_embedder()?;
80 self.knn_in(token, engine, query_vector, top_k).await
81 }
82
83 pub async fn knn_in(
87 &self,
88 token: &NamespaceToken,
89 engine: &str,
90 query_vector: Vec<f32>,
91 top_k: u32,
92 ) -> RuntimeResult<Vec<VectorSearchHit>> {
93 let (engine, dimensions) = self.vector_model_metadata(engine)?;
94 validate_vector(&engine, dimensions, &query_vector, "vec_search")?;
95 Ok(self
96 .vectors_for_model(token, &engine)?
97 .search(VectorSearchRequest {
98 query_vectors: vec![query_vector],
99 top_k,
100 namespace: Some(token.namespace().as_str().to_owned()),
101 kind: Some(SubstrateKind::Entity),
102 embedding_model: None,
103 filter: None,
104 backend_hints: None,
105 })
106 .await?)
107 }
108
109 pub async fn rerank(
112 &self,
113 token: &NamespaceToken,
114 query_vector: &[f32],
115 candidate_ids: &[Uuid],
116 top_k: u32,
117 ) -> RuntimeResult<Vec<VectorSearchHit>> {
118 let engine = self.require_default_embedder()?;
119 self.rerank_in(token, engine, query_vector, candidate_ids, top_k)
120 .await
121 }
122
123 pub async fn rerank_in(
131 &self,
132 token: &NamespaceToken,
133 engine: &str,
134 query_vector: &[f32],
135 candidate_ids: &[Uuid],
136 top_k: u32,
137 ) -> RuntimeResult<Vec<VectorSearchHit>> {
138 let (engine, dimensions) = self.vector_model_metadata(engine)?;
139 validate_vector(&engine, dimensions, query_vector, "score_candidates")?;
140 let mut hits = self
141 .vectors_for_model(token, &engine)?
142 .score_candidates(query_vector, candidate_ids, Some(SubstrateKind::Entity))
143 .await?;
144 hits.truncate(top_k as usize);
145 Ok(hits)
146 }
147}
148
149fn missing_query() -> RuntimeError {
150 RuntimeError::InvalidInput("vector search requires query_embedding or query_text".into())
151}
152
153fn validate_query_input(vector: Option<&[f32]>, text: Option<&str>) -> RuntimeResult<()> {
154 if vector.is_none() {
155 let text = text.ok_or_else(missing_query)?;
156 if text.trim().is_empty() {
157 return Err(RuntimeError::InvalidInput(
158 "query_text must not be empty".into(),
159 ));
160 }
161 }
162 Ok(())
163}
164
165fn validate_vector(
166 engine: &str,
167 dimensions: usize,
168 vector: &[f32],
169 operation: &'static str,
170) -> RuntimeResult<()> {
171 let message = if vector.len() != dimensions {
172 Some(format!(
173 "engine '{engine}' query has {} dims, expected {dimensions}",
174 vector.len()
175 ))
176 } else {
177 vector
178 .iter()
179 .position(|coordinate| !coordinate.is_finite())
180 .map(|index| {
181 format!("engine '{engine}' query has a nonfinite coordinate at index {index}")
182 })
183 };
184 if let Some(message) = message {
185 return Err(StorageError::InvalidInput {
186 capability: StorageCapability::Vectors,
187 operation: operation.into(),
188 message,
189 }
190 .into());
191 }
192 Ok(())
193}