use std::cell::RefCell;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ResponseNotice {
pub path: Vec<String>,
pub kind: &'static str,
pub detail: serde_json::Value,
}
pub const POSSIBLY_TRUNCATED: &str = "nearest_possibly_truncated";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShortResultPolicy {
#[default]
Signal,
Verify,
Refuse,
}
pub fn settle_short_nearest(
policy: ShortResultPolicy,
response_key: &str,
k: u32,
returned: usize,
matched: Option<u64>,
) -> crate::error::Result<()> {
let returned_u64 = u64::try_from(returned).unwrap_or(u64::MAX);
if returned_u64 >= u64::from(k) {
return Ok(());
}
let verified = match (policy, matched) {
(ShortResultPolicy::Signal, _) | (_, None) => false,
(ShortResultPolicy::Verify | ShortResultPolicy::Refuse, Some(matched)) => {
if matched <= returned_u64 {
return Ok(());
}
true
},
};
if verified && policy == ShortResultPolicy::Refuse {
return Err(crate::error::FraiseQLError::Unsupported {
message: format!(
"`{response_key}`: the similarity search found {returned} of the {k} nearest \
rows its filter matches and stopped short (`vector_on_short_result = \
\"refuse\"`). Raise `vector_hnsw_ef_search` so the search keeps more \
candidates, or turn on `vector_hnsw_iterative_scan`."
),
});
}
push_notice(ResponseNotice {
path: vec![response_key.to_string()],
kind: POSSIBLY_TRUNCATED,
detail: serde_json::json!({
"requested": k,
"returned": returned,
"verified": verified,
}),
});
Ok(())
}
tokio::task_local! {
static NOTICES: RefCell<Vec<ResponseNotice>>;
}
pub async fn collect_notices<F: std::future::Future>(
future: F,
) -> (F::Output, Vec<ResponseNotice>) {
NOTICES
.scope(RefCell::new(Vec::new()), async move {
let output = future.await;
let notices = NOTICES.with(|cell| cell.take());
(output, notices)
})
.await
}
pub fn push_notice(notice: ResponseNotice) {
let _ = NOTICES.try_with(|cell| cell.borrow_mut().push(notice));
}
#[must_use]
pub fn with_notices(
mut response: serde_json::Value,
notices: &[ResponseNotice],
) -> serde_json::Value {
if notices.is_empty() {
return response;
}
if let Some(object) = response.as_object_mut() {
let extensions = object
.entry("extensions")
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let Some(extensions) = extensions.as_object_mut() {
extensions.insert(
"notices".to_string(),
serde_json::to_value(notices).unwrap_or(serde_json::Value::Null),
);
}
}
response
}
#[cfg(test)]
#[path = "notices_tests.rs"]
mod tests;