use khive_score::DeterministicScore;
use khive_storage::{StorageCapability, StorageError, VectorSearchHit, VectorSearchRequest};
use khive_types::SubstrateKind;
use uuid::Uuid;
use crate::{KhiveRuntime, NamespaceToken, RuntimeError, RuntimeResult};
impl KhiveRuntime {
pub(super) async fn hybrid_vector_stage(
&self,
token: &NamespaceToken,
query_text: &str,
query_vector: Option<Vec<f32>>,
candidates: u32,
vector_similarity_floor: Option<f64>,
tolerate_vector_error: bool,
) -> RuntimeResult<(Vec<VectorSearchHit>, Option<String>)> {
khive_storage::await_request_read_phase("hybrid_search.vector", async {
let mut vector_error: Option<String> = None;
let mut vector_hits =
if query_vector.is_some() || !self.default_embedder_name().is_empty() {
match self
.vector_search(
token,
query_vector,
Some(query_text),
candidates,
Some(SubstrateKind::Entity),
)
.await
{
Ok(hits) => hits,
Err(e) if tolerate_vector_error => {
vector_error = Some(e.to_string());
Vec::new()
}
Err(e) => return Err(e),
}
} else {
Vec::new()
};
if let Some(cosine_floor) = vector_similarity_floor {
let score_floor = DeterministicScore::from_f64(cosine_floor);
vector_hits.retain(|hit| hit.score >= score_floor);
}
Ok((vector_hits, vector_error))
})
.await?
}
pub async fn vector_search(
&self,
token: &NamespaceToken,
query_embedding: Option<Vec<f32>>,
query_text: Option<&str>,
top_k: u32,
kind: Option<SubstrateKind>,
) -> RuntimeResult<Vec<VectorSearchHit>> {
validate_query_input(query_embedding.as_deref(), query_text)?;
let engine = self.require_default_embedder()?;
self.vector_search_in(token, engine, query_embedding, query_text, top_k, kind)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn vector_search_in(
&self,
token: &NamespaceToken,
engine: &str,
query_embedding: Option<Vec<f32>>,
query_text: Option<&str>,
top_k: u32,
kind: Option<SubstrateKind>,
) -> RuntimeResult<Vec<VectorSearchHit>> {
validate_query_input(query_embedding.as_deref(), query_text)?;
let (engine, dimensions) = self.vector_model_metadata(engine)?;
let embedding = match query_embedding {
Some(vector) => vector,
None => {
let text = query_text.ok_or_else(missing_query)?;
self.embed_query_with_model_for_token(token, &engine, text)
.await?
}
};
validate_vector(&engine, dimensions, &embedding, "vec_search")?;
let hits = self
.vectors_for_model(token, &engine)?
.search(VectorSearchRequest {
query_vectors: vec![embedding],
top_k,
namespace: Some(token.namespace().as_str().to_owned()),
kind,
embedding_model: None,
filter: None,
backend_hints: None,
})
.await;
crate::usage::count(crate::usage::UsageUnit::VectorPasses, 1);
hits.map_err(RuntimeError::from)
}
pub async fn knn(
&self,
token: &NamespaceToken,
query_vector: Vec<f32>,
top_k: u32,
) -> RuntimeResult<Vec<VectorSearchHit>> {
let engine = self.require_default_embedder()?;
self.knn_in(token, engine, query_vector, top_k).await
}
pub async fn knn_in(
&self,
token: &NamespaceToken,
engine: &str,
query_vector: Vec<f32>,
top_k: u32,
) -> RuntimeResult<Vec<VectorSearchHit>> {
let (engine, dimensions) = self.vector_model_metadata(engine)?;
validate_vector(&engine, dimensions, &query_vector, "vec_search")?;
Ok(self
.vectors_for_model(token, &engine)?
.search(VectorSearchRequest {
query_vectors: vec![query_vector],
top_k,
namespace: Some(token.namespace().as_str().to_owned()),
kind: Some(SubstrateKind::Entity),
embedding_model: None,
filter: None,
backend_hints: None,
})
.await?)
}
pub async fn rerank(
&self,
token: &NamespaceToken,
query_vector: &[f32],
candidate_ids: &[Uuid],
top_k: u32,
) -> RuntimeResult<Vec<VectorSearchHit>> {
let engine = self.require_default_embedder()?;
self.rerank_in(token, engine, query_vector, candidate_ids, top_k)
.await
}
pub async fn rerank_in(
&self,
token: &NamespaceToken,
engine: &str,
query_vector: &[f32],
candidate_ids: &[Uuid],
top_k: u32,
) -> RuntimeResult<Vec<VectorSearchHit>> {
let (engine, dimensions) = self.vector_model_metadata(engine)?;
validate_vector(&engine, dimensions, query_vector, "score_candidates")?;
let mut hits = self
.vectors_for_model(token, &engine)?
.score_candidates(query_vector, candidate_ids, Some(SubstrateKind::Entity))
.await?;
hits.truncate(top_k as usize);
Ok(hits)
}
}
fn missing_query() -> RuntimeError {
RuntimeError::InvalidInput("vector search requires query_embedding or query_text".into())
}
fn validate_query_input(vector: Option<&[f32]>, text: Option<&str>) -> RuntimeResult<()> {
if vector.is_none() {
let text = text.ok_or_else(missing_query)?;
if text.trim().is_empty() {
return Err(RuntimeError::InvalidInput(
"query_text must not be empty".into(),
));
}
}
Ok(())
}
fn validate_vector(
engine: &str,
dimensions: usize,
vector: &[f32],
operation: &'static str,
) -> RuntimeResult<()> {
let message = if vector.len() != dimensions {
Some(format!(
"engine '{engine}' query has {} dims, expected {dimensions}",
vector.len()
))
} else {
vector
.iter()
.position(|coordinate| !coordinate.is_finite())
.map(|index| {
format!("engine '{engine}' query has a nonfinite coordinate at index {index}")
})
};
if let Some(message) = message {
return Err(StorageError::InvalidInput {
capability: StorageCapability::Vectors,
operation: operation.into(),
message,
}
.into());
}
Ok(())
}