use serde_json::Value;
pub const DIMS: usize = 8;
pub const MAX_VECTORS_PER_BATCH: usize = 256;
pub const MAX_BODY_BYTES: usize = 64 * 1024;
pub const INGEST_PATH: &str = "/api/v1/store/ingest";
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct IngestVector {
pub id: u64,
pub values: [f32; DIMS],
}
#[derive(Debug, Clone, PartialEq)]
pub struct IngestBatch {
pub vectors: Vec<IngestVector>,
pub dedup: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum IngestError {
#[error("malformed request: {0}")]
Malformed(String),
#[error("too large: {0}")]
TooLarge(String),
#[error("unauthorized")]
Unauthorized,
#[error("forbidden")]
Forbidden,
#[error("rate limited")]
RateLimited,
#[error("store unavailable: {0}")]
Unavailable(String),
#[error("not routed: {0}")]
NotRouted(String),
}
impl IngestError {
pub fn status(&self) -> u16 {
match self {
Self::Malformed(_) => 400,
Self::Unauthorized => 401,
Self::Forbidden => 403,
Self::TooLarge(_) => 413,
Self::RateLimited => 429,
Self::Unavailable(_) | Self::NotRouted(_) => 502,
}
}
}
fn bad(m: impl Into<String>) -> IngestError {
IngestError::Malformed(m.into())
}
pub fn parse_batch(body: &[u8]) -> Result<IngestBatch, IngestError> {
if body.len() > MAX_BODY_BYTES {
return Err(IngestError::TooLarge(format!(
"body over {MAX_BODY_BYTES} bytes"
)));
}
let v: Value = serde_json::from_slice(body).map_err(|e| bad(format!("not JSON: {e}")))?;
let obj = v.as_object().ok_or_else(|| bad("body must be an object"))?;
if let Some(k) = obj.keys().find(|k| *k != "vectors" && *k != "dedup") {
return Err(bad(format!("unknown key `{k}`")));
}
let dedup = match obj.get("dedup") {
None => false,
Some(Value::Bool(b)) => *b,
Some(_) => return Err(bad("`dedup` must be a boolean")),
};
let arr = obj
.get("vectors")
.and_then(Value::as_array)
.ok_or_else(|| bad("`vectors` must be an array"))?;
if arr.is_empty() {
return Err(bad("`vectors` is empty"));
}
if arr.len() > MAX_VECTORS_PER_BATCH {
return Err(IngestError::TooLarge(format!(
"{} vectors, at most {MAX_VECTORS_PER_BATCH}",
arr.len()
)));
}
let vectors = arr
.iter()
.enumerate()
.map(|(i, e)| parse_vector(e).map_err(|m| bad(format!("vectors[{i}]: {m}"))))
.collect::<Result<Vec<_>, _>>()?;
Ok(IngestBatch { vectors, dedup })
}
fn parse_vector(e: &Value) -> Result<IngestVector, String> {
let pair = e.as_array().filter(|p| p.len() == 2);
let [id, vals] = pair.map(|p| [&p[0], &p[1]]).ok_or("expected [id, [values]]")?;
let id = id.as_u64().ok_or("id must be a non-negative integer")?;
let vals = vals.as_array().ok_or("values must be an array")?;
if vals.len() != DIMS {
return Err(format!("{} values, expected {DIMS}", vals.len()));
}
let mut values = [0f32; DIMS];
for (slot, x) in values.iter_mut().zip(vals) {
let f = x.as_f64().ok_or("values must be numbers")? as f32;
if !f.is_finite() {
return Err("values must be finite f32".into());
}
*slot = f;
}
Ok(IngestVector { id, values })
}
pub fn valid_project_id(s: &str) -> bool {
clawft_types::project::validate_id(s).is_ok()
}