use crate::error::QdrantResult;
use crate::point::{PayloadValue, Point, PointId, ScoredPoint};
use serde_json::{Value as JsonValue, json};
fn serialize_json_request(request: &JsonValue) -> Vec<u8> {
match serde_json::to_vec(request) {
Ok(bytes) => bytes,
Err(_) => b"{}".to_vec(),
}
}
pub fn encode_search_request(
vector: &[f32],
limit: u64,
offset: Option<u64>,
score_threshold: Option<f32>,
with_vector: bool,
) -> Vec<u8> {
let mut request = json!({
"vector": vector,
"limit": limit,
"with_payload": true,
"with_vector": with_vector,
});
if let Some(off) = offset {
request["offset"] = json!(off);
}
if let Some(threshold) = score_threshold {
request["score_threshold"] = json!(threshold);
}
serialize_json_request(&request)
}
pub fn encode_search_request_with_filter(
vector: &[f32],
limit: u64,
offset: Option<u64>,
score_threshold: Option<f32>,
with_vector: bool,
filter: JsonValue,
) -> Vec<u8> {
let mut request = json!({
"vector": vector,
"limit": limit,
"with_payload": true,
"with_vector": with_vector,
"filter": filter,
});
if let Some(off) = offset {
request["offset"] = json!(off);
}
if let Some(threshold) = score_threshold {
request["score_threshold"] = json!(threshold);
}
serialize_json_request(&request)
}
pub fn encode_upsert_request(points: &[Point]) -> Vec<u8> {
let points_json: Vec<JsonValue> = points
.iter()
.map(|p| {
let id = match &p.id {
PointId::Uuid(s) => json!(s),
PointId::Num(n) => json!(n),
};
let payload: JsonValue = p
.payload
.iter()
.map(|(k, v)| (k.clone(), payload_value_to_json(v)))
.collect();
json!({
"id": id,
"vector": p.vector,
"payload": payload,
})
})
.collect();
let request = json!({ "points": points_json });
serialize_json_request(&request)
}
pub fn encode_upsert_multi_vector_request(points: &[crate::point::MultiVectorPoint]) -> Vec<u8> {
use crate::point::MultiVectorPoint;
let points_json: Vec<JsonValue> = points
.iter()
.map(|p: &MultiVectorPoint| {
let id = match &p.id {
PointId::Uuid(s) => json!(s),
PointId::Num(n) => json!(n),
};
let payload: JsonValue = p
.payload
.iter()
.map(|(k, v)| (k.clone(), payload_value_to_json(v)))
.collect();
let vectors: JsonValue = p
.vectors
.iter()
.map(|(k, v)| (k.clone(), json!(v)))
.collect();
json!({
"id": id,
"vector": vectors,
"payload": payload,
})
})
.collect();
let request = json!({ "points": points_json });
serialize_json_request(&request)
}
pub fn encode_delete_request(ids: &[PointId]) -> Vec<u8> {
let ids_json: Vec<JsonValue> = ids
.iter()
.map(|id| match id {
PointId::Uuid(s) => json!(s),
PointId::Num(n) => json!(n),
})
.collect();
let request = json!({ "points": ids_json });
serialize_json_request(&request)
}
pub fn encode_create_collection_request(
vector_size: u64,
distance: &str, ) -> Vec<u8> {
let request = json!({
"vectors": {
"size": vector_size,
"distance": distance,
}
});
serialize_json_request(&request)
}
pub fn encode_conditions_to_filter(
conditions: &[qail_core::ast::Condition],
is_or: bool,
) -> JsonValue {
use qail_core::ast::{Expr, Operator, Value};
let clauses: Vec<JsonValue> = conditions
.iter()
.filter_map(|cond| {
let key = match &cond.left {
Expr::Named(name) => name.clone(),
Expr::Aliased { name, .. } => name.clone(),
_ => return None,
};
let clause = match (&cond.op, &cond.value) {
(Operator::Eq, Value::String(s)) => json!({
"key": key,
"match": { "value": s }
}),
(Operator::Eq, Value::Int(n)) => json!({
"key": key,
"match": { "value": n }
}),
(Operator::Eq, Value::Bool(b)) => json!({
"key": key,
"match": { "value": b }
}),
(Operator::Gt, Value::Int(n)) => json!({
"key": key,
"range": { "gt": n }
}),
(Operator::Gt, Value::Float(f)) => json!({
"key": key,
"range": { "gt": f }
}),
(Operator::Gte, Value::Int(n)) => json!({
"key": key,
"range": { "gte": n }
}),
(Operator::Gte, Value::Float(f)) => json!({
"key": key,
"range": { "gte": f }
}),
(Operator::Lt, Value::Int(n)) => json!({
"key": key,
"range": { "lt": n }
}),
(Operator::Lt, Value::Float(f)) => json!({
"key": key,
"range": { "lt": f }
}),
(Operator::Lte, Value::Int(n)) => json!({
"key": key,
"range": { "lte": n }
}),
(Operator::Lte, Value::Float(f)) => json!({
"key": key,
"range": { "lte": f }
}),
(Operator::In, Value::Array(arr)) => {
let values: Vec<JsonValue> = arr.iter().filter_map(value_to_json).collect();
json!({
"key": key,
"match": { "any": values }
})
}
(Operator::IsNull, _) => json!({
"is_null": { "key": key }
}),
(Operator::IsNotNull, _) => json!({
"is_empty": { "key": key, "is_empty": false }
}),
(Operator::Contains | Operator::Like, Value::String(s)) => json!({
"key": key,
"match": { "text": s }
}),
(_, Value::String(s)) => json!({
"key": key,
"match": { "value": s }
}),
(_, Value::Int(n)) => json!({
"key": key,
"match": { "value": n }
}),
_ => return None,
};
Some(clause)
})
.collect();
if is_or {
json!({ "should": clauses })
} else {
json!({ "must": clauses })
}
}
fn value_to_json(value: &qail_core::ast::Value) -> Option<JsonValue> {
use qail_core::ast::Value;
match value {
Value::String(s) => Some(json!(s)),
Value::Int(n) => Some(json!(n)),
Value::Float(f) => Some(json!(f)),
Value::Bool(b) => Some(json!(b)),
Value::Null => Some(JsonValue::Null),
_ => None,
}
}
pub fn decode_search_response(data: &[u8]) -> QdrantResult<Vec<ScoredPoint>> {
let response: JsonValue = serde_json::from_slice(data)
.map_err(|e| crate::error::QdrantError::Decode(e.to_string()))?;
let results = response["result"]
.as_array()
.ok_or_else(|| crate::error::QdrantError::Decode("Missing 'result' array".to_string()))?;
results
.iter()
.enumerate()
.map(|(idx, item)| {
let id = item.get("id").and_then(parse_point_id).ok_or_else(|| {
crate::error::QdrantError::Decode(format!("Missing point id at result index {idx}"))
})?;
let score = item
.get("score")
.and_then(JsonValue::as_f64)
.filter(|score| score.is_finite())
.ok_or_else(|| {
crate::error::QdrantError::Decode(format!(
"Invalid score at result index {idx}"
))
})?;
let payload = match item.get("payload") {
Some(payload) => parse_payload_checked(payload, idx)?,
None => crate::point::Payload::new(),
};
let vector = decode_result_vector(item.get("vector"), idx)?;
Ok(ScoredPoint {
id,
score: score as f32,
payload,
vector,
})
})
.collect()
}
fn decode_result_vector(
value: Option<&JsonValue>,
result_idx: usize,
) -> QdrantResult<Option<Vec<f32>>> {
let Some(value) = value else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
let arr = value.as_array().ok_or_else(|| {
crate::error::QdrantError::Decode(format!("Invalid vector at result index {result_idx}"))
})?;
let mut vector = Vec::with_capacity(arr.len());
for (idx, item) in arr.iter().enumerate() {
let value = item
.as_f64()
.filter(|value| value.is_finite())
.ok_or_else(|| {
crate::error::QdrantError::Decode(format!(
"Invalid vector value at result index {result_idx}, vector index {idx}"
))
})?;
let value = value as f32;
if !value.is_finite() {
return Err(crate::error::QdrantError::Decode(format!(
"Invalid vector value at result index {result_idx}, vector index {idx}"
)));
}
vector.push(value);
}
Ok(Some(vector))
}
pub fn parse_point_id(value: &JsonValue) -> Option<PointId> {
if let Some(s) = value.as_str() {
Some(PointId::Uuid(s.to_string()))
} else {
value.as_u64().map(PointId::Num)
}
}
pub fn parse_payload(value: &JsonValue) -> crate::point::Payload {
parse_payload_checked(value, 0).unwrap_or_default()
}
fn parse_payload_checked(
value: &JsonValue,
result_idx: usize,
) -> QdrantResult<crate::point::Payload> {
let mut payload = crate::point::Payload::new();
match value {
JsonValue::Null => Ok(payload),
JsonValue::Object(obj) => {
for (key, value) in obj {
let payload_value = json_to_payload_value_checked(
value,
&format!("result[{result_idx}].payload.{key}"),
)?;
payload.insert(key.clone(), payload_value);
}
Ok(payload)
}
_ => Err(crate::error::QdrantError::Decode(format!(
"Invalid payload object at result index {result_idx}"
))),
}
}
fn payload_value_to_json(value: &PayloadValue) -> JsonValue {
match value {
PayloadValue::String(s) => json!(s),
PayloadValue::Integer(n) => json!(n),
PayloadValue::Float(f) => json!(f),
PayloadValue::Bool(b) => json!(b),
PayloadValue::List(arr) => {
JsonValue::Array(arr.iter().map(payload_value_to_json).collect())
}
PayloadValue::Object(obj) => JsonValue::Object(
obj.iter()
.map(|(k, v)| (k.clone(), payload_value_to_json(v)))
.collect(),
),
PayloadValue::Null => JsonValue::Null,
}
}
fn json_to_payload_value_checked(value: &JsonValue, path: &str) -> QdrantResult<PayloadValue> {
match value {
JsonValue::Null => Ok(PayloadValue::Null),
JsonValue::Bool(b) => Ok(PayloadValue::Bool(*b)),
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(PayloadValue::Integer(i))
} else if let Some(u) = n.as_u64() {
let i = i64::try_from(u).map_err(|_| {
crate::error::QdrantError::Decode(format!(
"Payload integer out of range at {path}"
))
})?;
Ok(PayloadValue::Integer(i))
} else {
let f = n
.as_f64()
.filter(|value| value.is_finite())
.ok_or_else(|| {
crate::error::QdrantError::Decode(format!(
"Invalid payload number at {path}"
))
})?;
Ok(PayloadValue::Float(f))
}
}
JsonValue::String(s) => Ok(PayloadValue::String(s.clone())),
JsonValue::Array(arr) => {
let mut items = Vec::with_capacity(arr.len());
for (idx, value) in arr.iter().enumerate() {
items.push(json_to_payload_value_checked(
value,
&format!("{path}[{idx}]"),
)?);
}
Ok(PayloadValue::List(items))
}
JsonValue::Object(obj) => {
let mut map = std::collections::HashMap::with_capacity(obj.len());
for (key, value) in obj {
map.insert(
key.clone(),
json_to_payload_value_checked(value, &format!("{path}.{key}"))?,
);
}
Ok(PayloadValue::Object(map))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_search_request() {
let vector = vec![0.1, 0.2, 0.3];
let json_bytes = encode_search_request(&vector, 10, None, None, false);
let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
assert!(json["vector"].is_array());
assert_eq!(json["limit"], 10);
assert_eq!(json["with_payload"], true);
assert_eq!(json["vector"].as_array().unwrap().len(), 3);
}
#[test]
fn test_encode_upsert_request() {
let point = Point::new("test-id", vec![0.5, 0.5]);
let json_bytes = encode_upsert_request(&[point]);
let json_str = String::from_utf8(json_bytes).unwrap();
assert!(json_str.contains("\"points\""));
assert!(json_str.contains("\"test-id\""));
assert!(json_str.contains("[0.5,0.5]"));
}
#[test]
fn test_encode_delete_request() {
let ids = vec![PointId::Uuid("id1".to_string()), PointId::Num(42)];
let json_bytes = encode_delete_request(&ids);
let json_str = String::from_utf8(json_bytes).unwrap();
assert!(json_str.contains("\"id1\""));
assert!(json_str.contains("42"));
}
#[test]
fn test_decode_search_response() {
let response = r#"{
"result": [
{"id": "abc", "score": 0.95, "payload": {"name": "test"}},
{"id": 123, "score": 0.80, "payload": {}, "vector": [0.1, 0.2]}
]
}"#;
let results = decode_search_response(response.as_bytes()).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].score, 0.95);
assert_eq!(results[1].score, 0.80);
assert_eq!(results[1].vector.as_deref(), Some(&[0.1, 0.2][..]));
}
#[test]
fn decode_search_response_rejects_malformed_results() {
let missing_id = r#"{
"result": [
{"score": 0.95, "payload": {"name": "test"}}
]
}"#;
let err = decode_search_response(missing_id.as_bytes())
.expect_err("missing id should fail closed");
assert!(err.to_string().contains("Missing point id"));
let bad_vector = r#"{
"result": [
{"id": "abc", "score": 0.95, "payload": {}, "vector": [0.1, "oops"]}
]
}"#;
let err = decode_search_response(bad_vector.as_bytes())
.expect_err("bad vector should fail closed");
assert!(err.to_string().contains("Invalid vector value"));
}
#[test]
fn decode_search_response_rejects_malformed_payload() {
let payload_array = r#"{
"result": [
{"id": "abc", "score": 0.95, "payload": ["not", "an", "object"]}
]
}"#;
let err = decode_search_response(payload_array.as_bytes())
.expect_err("payload array should fail closed");
assert!(err.to_string().contains("Invalid payload object"));
let oversized_integer = r#"{
"result": [
{"id": "abc", "score": 0.95, "payload": {"too_big": 18446744073709551615}}
]
}"#;
let err = decode_search_response(oversized_integer.as_bytes())
.expect_err("payload integer overflow should fail closed");
assert!(err.to_string().contains("Payload integer out of range"));
}
#[test]
fn test_encode_conditions_to_filter() {
use qail_core::ast::{Condition, Expr, Operator, Value};
let conditions = vec![
Condition {
left: Expr::Named("category".to_string()),
op: Operator::Eq,
value: Value::String("electronics".to_string()),
is_array_unnest: false,
},
Condition {
left: Expr::Named("price".to_string()),
op: Operator::Lt,
value: Value::Int(1000),
is_array_unnest: false,
},
];
let filter = encode_conditions_to_filter(&conditions, false);
assert!(filter["must"].is_array());
let must = filter["must"].as_array().unwrap();
assert_eq!(must.len(), 2);
assert_eq!(must[0]["key"], "category");
assert_eq!(must[0]["match"]["value"], "electronics");
assert_eq!(must[1]["key"], "price");
assert_eq!(must[1]["range"]["lt"], 1000);
}
#[test]
fn test_encode_conditions_to_filter_or() {
use qail_core::ast::{Condition, Expr, Operator, Value};
let conditions = vec![Condition {
left: Expr::Named("status".to_string()),
op: Operator::Eq,
value: Value::String("active".to_string()),
is_array_unnest: false,
}];
let filter = encode_conditions_to_filter(&conditions, true);
assert!(filter["should"].is_array());
assert!(filter["must"].is_null());
}
}