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(err) => {
encode_error_request(&format!("failed to serialize Qdrant JSON request: {err}"))
}
}
}
fn encode_error_request(message: &str) -> Vec<u8> {
serde_json::to_vec(&json!({ "error": message }))
.unwrap_or_else(|_| b"{\"error\":\"failed to encode Qdrant JSON error\"}".to_vec())
}
fn vector_to_json(values: &[f32], label: &str) -> Result<JsonValue, String> {
let mut json_values = Vec::with_capacity(values.len());
for (idx, value) in values.iter().enumerate() {
if !value.is_finite() {
return Err(format!(
"Qdrant {label} contains non-finite vector value at index {idx}: {value}"
));
}
json_values.push(json!(value));
}
Ok(JsonValue::Array(json_values))
}
fn optional_threshold_to_json(value: Option<f32>) -> Result<Option<JsonValue>, String> {
match value {
Some(value) if !value.is_finite() => Err(format!(
"Qdrant score threshold must be finite, got {value}"
)),
Some(value) => Ok(Some(json!(value))),
None => Ok(None),
}
}
pub fn encode_search_request(
vector: &[f32],
limit: u64,
offset: Option<u64>,
score_threshold: Option<f32>,
with_vector: bool,
) -> Vec<u8> {
let vector = match vector_to_json(vector, "search request") {
Ok(vector) => vector,
Err(err) => return encode_error_request(&err),
};
let mut request = json!({
"vector": vector,
"limit": limit,
"with_payload": true,
"with_vector": with_vector,
});
if let Some(off) = offset {
request["offset"] = json!(off);
}
let score_threshold = match optional_threshold_to_json(score_threshold) {
Ok(score_threshold) => score_threshold,
Err(err) => return encode_error_request(&err),
};
if let Some(threshold) = score_threshold {
request["score_threshold"] = 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 vector = match vector_to_json(vector, "search request") {
Ok(vector) => vector,
Err(err) => return encode_error_request(&err),
};
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);
}
let score_threshold = match optional_threshold_to_json(score_threshold) {
Ok(score_threshold) => score_threshold,
Err(err) => return encode_error_request(&err),
};
if let Some(threshold) = score_threshold {
request["score_threshold"] = threshold;
}
serialize_json_request(&request)
}
pub fn encode_upsert_request(points: &[Point]) -> Vec<u8> {
let points_json: Result<Vec<JsonValue>, String> = points
.iter()
.enumerate()
.map(|(idx, p)| {
let id = match &p.id {
PointId::Uuid(s) => json!(s),
PointId::Num(n) => json!(n),
};
let payload: Result<serde_json::Map<String, JsonValue>, String> = p
.payload
.iter()
.map(|(k, v)| Ok((k.clone(), payload_value_to_json(v)?)))
.collect();
let vector = vector_to_json(&p.vector, &format!("upsert point {idx}"))?;
Ok(json!({
"id": id,
"vector": vector,
"payload": JsonValue::Object(payload?),
}))
})
.collect();
let points_json = match points_json {
Ok(points_json) => points_json,
Err(err) => return encode_error_request(&err),
};
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: Result<Vec<JsonValue>, String> = points
.iter()
.enumerate()
.map(|(idx, p): (usize, &MultiVectorPoint)| {
let id = match &p.id {
PointId::Uuid(s) => json!(s),
PointId::Num(n) => json!(n),
};
let payload: Result<serde_json::Map<String, JsonValue>, String> = p
.payload
.iter()
.map(|(k, v)| Ok((k.clone(), payload_value_to_json(v)?)))
.collect();
let vectors: Result<serde_json::Map<String, JsonValue>, String> = p
.vectors
.iter()
.map(|(k, v)| {
Ok((
k.clone(),
vector_to_json(v, &format!("multi-vector point {idx}.{k}"))?,
))
})
.collect();
Ok(json!({
"id": id,
"vector": JsonValue::Object(vectors?),
"payload": JsonValue::Object(payload?),
}))
})
.collect();
let points_json = match points_json {
Ok(points_json) => points_json,
Err(err) => return encode_error_request(&err),
};
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 mut clauses = Vec::with_capacity(conditions.len());
for cond in conditions {
let key = match &cond.left {
Expr::Named(name) => name.clone(),
Expr::Aliased { name, .. } => name.clone(),
_ => return impossible_filter(),
};
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)) if f.is_finite() => json!({
"key": key,
"range": { "gt": f }
}),
(Operator::Gte, Value::Int(n)) => json!({
"key": key,
"range": { "gte": n }
}),
(Operator::Gte, Value::Float(f)) if f.is_finite() => json!({
"key": key,
"range": { "gte": f }
}),
(Operator::Lt, Value::Int(n)) => json!({
"key": key,
"range": { "lt": n }
}),
(Operator::Lt, Value::Float(f)) if f.is_finite() => json!({
"key": key,
"range": { "lt": f }
}),
(Operator::Lte, Value::Int(n)) => json!({
"key": key,
"range": { "lte": n }
}),
(Operator::Lte, Value::Float(f)) if f.is_finite() => json!({
"key": key,
"range": { "lte": f }
}),
(Operator::In, Value::Array(arr)) => {
let Some(values) = values_to_json(arr) else {
return impossible_filter();
};
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 impossible_filter(),
};
clauses.push(clause);
}
if is_or {
json!({ "should": clauses })
} else {
json!({ "must": clauses })
}
}
fn impossible_filter() -> JsonValue {
json!({
"must": [{
"key": "__qail_unrepresentable_filter__",
"range": { "gt": 1, "lt": 0 }
}]
})
}
fn values_to_json(values: &[qail_core::ast::Value]) -> Option<Vec<JsonValue>> {
values.iter().map(value_to_json).collect()
}
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) if f.is_finite() => 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) -> Result<JsonValue, String> {
match value {
PayloadValue::String(s) => Ok(json!(s)),
PayloadValue::Integer(n) => Ok(json!(n)),
PayloadValue::Float(f) if f.is_finite() => Ok(json!(f)),
PayloadValue::Float(f) => Err(format!(
"Qdrant payload contains non-finite float value: {f}"
)),
PayloadValue::Bool(b) => Ok(json!(b)),
PayloadValue::List(arr) => {
let values: Result<Vec<JsonValue>, String> =
arr.iter().map(payload_value_to_json).collect();
Ok(JsonValue::Array(values?))
}
PayloadValue::Object(obj) => {
let values: Result<serde_json::Map<String, JsonValue>, String> = obj
.iter()
.map(|(k, v)| Ok((k.clone(), payload_value_to_json(v)?)))
.collect();
Ok(JsonValue::Object(values?))
}
PayloadValue::Null => Ok(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 encode_search_request_rejects_non_finite_vector_json() {
let json_bytes = encode_search_request(&[0.1, f32::NAN], 10, None, None, false);
let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
assert!(
json["error"]
.as_str()
.unwrap()
.contains("non-finite vector value")
);
}
#[test]
fn encode_search_request_rejects_non_finite_threshold_json() {
let json_bytes = encode_search_request(&[0.1], 10, None, Some(f32::INFINITY), false);
let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
assert!(json["error"].as_str().unwrap().contains("score threshold"));
}
#[test]
fn encode_upsert_request_rejects_non_finite_payload_json() {
let point = Point::new("test-id", vec![0.5, 0.5])
.with_payload("score", PayloadValue::Float(f64::INFINITY));
let json_bytes = encode_upsert_request(&[point]);
let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
assert!(json["error"].as_str().unwrap().contains("non-finite float"));
}
#[test]
fn encode_multi_vector_request_rejects_non_finite_vector_json() {
let point = crate::point::MultiVectorPoint::new("test-id")
.with_vector("image", vec![0.5, f32::NEG_INFINITY]);
let json_bytes = encode_upsert_multi_vector_request(&[point]);
let json: JsonValue = serde_json::from_slice(&json_bytes).unwrap();
assert!(
json["error"]
.as_str()
.unwrap()
.contains("non-finite vector value")
);
}
#[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());
}
#[test]
fn encode_conditions_to_filter_fails_closed_on_unrepresentable_condition() {
use qail_core::ast::{Condition, Expr, Operator, Value};
let conditions = vec![
Condition {
left: Expr::Named("tenant_id".to_string()),
op: Operator::Eq,
value: Value::String("tenant-a".to_string()),
is_array_unnest: false,
},
Condition {
left: Expr::Literal(Value::String("not-a-field".to_string())),
op: Operator::Eq,
value: Value::String("tenant-b".to_string()),
is_array_unnest: false,
},
];
let filter = encode_conditions_to_filter(&conditions, false);
assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
assert_eq!(filter["must"][0]["range"]["gt"], 1);
assert_eq!(filter["must"][0]["range"]["lt"], 0);
}
#[test]
fn encode_conditions_to_filter_fails_closed_on_bad_in_value() {
use qail_core::ast::{Condition, Expr, Operator, Value};
let conditions = vec![Condition {
left: Expr::Named("category".to_string()),
op: Operator::In,
value: Value::Array(vec![
Value::String("a".to_string()),
Value::Vector(vec![1.0, 2.0]),
]),
is_array_unnest: false,
}];
let filter = encode_conditions_to_filter(&conditions, false);
assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
assert!(filter.to_string().contains("\"gt\":1"));
}
#[test]
fn encode_conditions_to_filter_fails_closed_on_non_finite_float() {
use qail_core::ast::{Condition, Expr, Operator, Value};
let conditions = vec![Condition {
left: Expr::Named("score".to_string()),
op: Operator::Gt,
value: Value::Float(f64::NAN),
is_array_unnest: false,
}];
let filter = encode_conditions_to_filter(&conditions, false);
assert_eq!(filter["must"][0]["key"], "__qail_unrepresentable_filter__");
assert!(!filter.to_string().contains("null"));
}
}