use crate::error::{QdrantError, QdrantResult};
use crate::point::{Point, PointId, ScoredPoint};
use crate::protocol;
use qail_core::ast::{CageKind, Qail, Value};
use reqwest::Client;
pub struct QdrantDriver {
base_url: String,
client: Client,
}
impl QdrantDriver {
pub async fn connect(host: &str, port: u16) -> QdrantResult<Self> {
let base_url = format!("http://{}:{}", host, port);
let client = Client::new();
let url = format!("{}/collections", base_url);
client
.get(&url)
.send()
.await
.map_err(|e| QdrantError::Connection(e.to_string()))?;
Ok(Self { base_url, client })
}
pub async fn connect_addr(addr: &str) -> QdrantResult<Self> {
let parts: Vec<&str> = addr.split(':').collect();
let host = parts.first().unwrap_or(&"localhost");
let port: u16 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(6333);
Self::connect(host, port).await
}
pub async fn search(&self, cmd: &Qail) -> QdrantResult<Vec<ScoredPoint>> {
let collection = &cmd.table;
let vector = cmd.vector.clone()
.or_else(|| self.extract_vector_from_cages(cmd).ok())
.ok_or_else(|| QdrantError::Encode("No vector found in search command".to_string()))?;
let (limit, offset) = self.extract_limit_offset(cmd);
let filter_conditions = self.extract_filter_conditions(cmd);
let body = if filter_conditions.is_empty() {
protocol::encode_search_request(
&vector,
limit,
offset,
cmd.score_threshold,
cmd.with_vector,
)
} else {
let filter = protocol::encode_conditions_to_filter(&filter_conditions, false);
protocol::encode_search_request_with_filter(
&vector,
limit,
offset,
cmd.score_threshold,
cmd.with_vector,
filter,
)
};
let url = format!("{}/collections/{}/points/search", self.base_url, collection);
let response = self.client
.post(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
let bytes = response.bytes().await
.map_err(|e| QdrantError::Decode(e.to_string()))?;
protocol::decode_search_response(&bytes)
}
pub async fn upsert(&self, collection: &str, points: &[Point]) -> QdrantResult<()> {
let body = protocol::encode_upsert_request(points);
let url = format!("{}/collections/{}/points?wait=true", self.base_url, collection);
let response = self.client
.put(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
if !response.status().is_success() {
let text = response.text().await.unwrap_or_default();
return Err(QdrantError::Grpc(format!("Upsert failed: {}", text)));
}
Ok(())
}
pub async fn delete(&self, collection: &str, ids: &[PointId]) -> QdrantResult<()> {
let body = protocol::encode_delete_request(ids);
let url = format!("{}/collections/{}/points/delete?wait=true", self.base_url, collection);
let response = self.client
.post(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
if !response.status().is_success() {
let text = response.text().await.unwrap_or_default();
return Err(QdrantError::Grpc(format!("Delete failed: {}", text)));
}
Ok(())
}
pub async fn create_collection(
&self,
name: &str,
vector_size: u64,
distance: Distance,
) -> QdrantResult<()> {
let distance_str = match distance {
Distance::Cosine => "Cosine",
Distance::Euclidean => "Euclid",
Distance::Dot => "Dot",
};
let body = protocol::encode_create_collection_request(vector_size, distance_str);
let url = format!("{}/collections/{}", self.base_url, name);
let response = self.client
.put(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
if !response.status().is_success() {
let text = response.text().await.unwrap_or_default();
return Err(QdrantError::Grpc(format!("Create collection failed: {}", text)));
}
Ok(())
}
pub async fn delete_collection(&self, name: &str) -> QdrantResult<()> {
let url = format!("{}/collections/{}", self.base_url, name);
let response = self.client
.delete(&url)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
if !response.status().is_success() {
let text = response.text().await.unwrap_or_default();
return Err(QdrantError::Grpc(format!("Delete collection failed: {}", text)));
}
Ok(())
}
pub async fn list_collections(&self) -> QdrantResult<Vec<String>> {
let url = format!("{}/collections", self.base_url);
let response = self.client
.get(&url)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
let bytes = response.bytes().await
.map_err(|e| QdrantError::Decode(e.to_string()))?;
let json: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| QdrantError::Decode(e.to_string()))?;
let collections = json["result"]["collections"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|c| c["name"].as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
Ok(collections)
}
pub async fn scroll(
&self,
collection: &str,
limit: u64,
offset: Option<PointId>,
filter: Option<serde_json::Value>,
) -> QdrantResult<(Vec<ScoredPoint>, Option<PointId>)> {
let mut request = serde_json::json!({
"limit": limit,
"with_payload": true,
"with_vector": false,
});
if let Some(off) = offset {
request["offset"] = match off {
PointId::Uuid(s) => serde_json::json!(s),
PointId::Num(n) => serde_json::json!(n),
};
}
if let Some(f) = filter {
request["filter"] = f;
}
let url = format!("{}/collections/{}/points/scroll", self.base_url, collection);
let response = self.client
.post(&url)
.header("Content-Type", "application/json")
.json(&request)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
let bytes = response.bytes().await
.map_err(|e| QdrantError::Decode(e.to_string()))?;
let json: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| QdrantError::Decode(e.to_string()))?;
let points = json["result"]["points"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|p| {
let id = protocol::parse_point_id(&p["id"])?;
let payload = protocol::parse_payload(&p["payload"]);
Some(ScoredPoint {
id,
score: 0.0, payload,
vector: None,
})
})
.collect()
})
.unwrap_or_default();
let next_offset = json["result"]["next_page_offset"]
.as_str()
.map(|s| PointId::Uuid(s.to_string()))
.or_else(|| json["result"]["next_page_offset"].as_u64().map(PointId::Num));
Ok((points, next_offset))
}
pub async fn recommend(
&self,
collection: &str,
positive: &[PointId],
negative: &[PointId],
limit: u64,
) -> QdrantResult<Vec<ScoredPoint>> {
let positive_json: Vec<serde_json::Value> = positive.iter().map(|id| {
match id {
PointId::Uuid(s) => serde_json::json!(s),
PointId::Num(n) => serde_json::json!(n),
}
}).collect();
let negative_json: Vec<serde_json::Value> = negative.iter().map(|id| {
match id {
PointId::Uuid(s) => serde_json::json!(s),
PointId::Num(n) => serde_json::json!(n),
}
}).collect();
let request = serde_json::json!({
"positive": positive_json,
"negative": negative_json,
"limit": limit,
"with_payload": true,
});
let url = format!("{}/collections/{}/points/recommend", self.base_url, collection);
let response = self.client
.post(&url)
.header("Content-Type", "application/json")
.json(&request)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
let bytes = response.bytes().await
.map_err(|e| QdrantError::Decode(e.to_string()))?;
protocol::decode_search_response(&bytes)
}
pub async fn get_points(
&self,
collection: &str,
ids: &[PointId],
) -> QdrantResult<Vec<ScoredPoint>> {
let ids_json: Vec<serde_json::Value> = ids.iter().map(|id| {
match id {
PointId::Uuid(s) => serde_json::json!(s),
PointId::Num(n) => serde_json::json!(n),
}
}).collect();
let request = serde_json::json!({
"ids": ids_json,
"with_payload": true,
"with_vector": false,
});
let url = format!("{}/collections/{}/points", self.base_url, collection);
let response = self.client
.post(&url)
.header("Content-Type", "application/json")
.json(&request)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
let bytes = response.bytes().await
.map_err(|e| QdrantError::Decode(e.to_string()))?;
let json: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| QdrantError::Decode(e.to_string()))?;
let points = json["result"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|p| {
let id = protocol::parse_point_id(&p["id"])?;
let payload = protocol::parse_payload(&p["payload"]);
Some(ScoredPoint {
id,
score: 0.0,
payload,
vector: None,
})
})
.collect()
})
.unwrap_or_default();
Ok(points)
}
pub async fn count(
&self,
collection: &str,
filter: Option<serde_json::Value>,
exact: bool,
) -> QdrantResult<u64> {
let mut request = serde_json::json!({
"exact": exact,
});
if let Some(f) = filter {
request["filter"] = f;
}
let url = format!("{}/collections/{}/points/count", self.base_url, collection);
let response = self.client
.post(&url)
.header("Content-Type", "application/json")
.json(&request)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
let bytes = response.bytes().await
.map_err(|e| QdrantError::Decode(e.to_string()))?;
let json: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| QdrantError::Decode(e.to_string()))?;
json["result"]["count"]
.as_u64()
.ok_or_else(|| QdrantError::Decode("Missing count".to_string()))
}
pub async fn collection_info(&self, name: &str) -> QdrantResult<serde_json::Value> {
let url = format!("{}/collections/{}", self.base_url, name);
let response = self.client
.get(&url)
.send()
.await
.map_err(|e| QdrantError::Grpc(e.to_string()))?;
let bytes = response.bytes().await
.map_err(|e| QdrantError::Decode(e.to_string()))?;
serde_json::from_slice(&bytes)
.map_err(|e| QdrantError::Decode(e.to_string()))
}
fn extract_vector_from_cages(&self, cmd: &Qail) -> QdrantResult<Vec<f32>> {
for cage in &cmd.cages {
for cond in &cage.conditions {
if let Value::Vector(v) = &cond.value {
return Ok(v.clone());
}
}
}
Err(QdrantError::Encode("No vector found in cages".to_string()))
}
fn extract_limit_offset(&self, cmd: &Qail) -> (u64, Option<u64>) {
let mut limit = 10u64;
let mut offset = None;
for cage in &cmd.cages {
match cage.kind {
CageKind::Limit(n) => limit = n as u64,
CageKind::Offset(n) => offset = Some(n as u64),
_ => {}
}
}
(limit, offset)
}
fn extract_filter_conditions(&self, cmd: &Qail) -> Vec<qail_core::ast::Condition> {
cmd.cages
.iter()
.filter(|cage| matches!(cage.kind, CageKind::Filter))
.flat_map(|cage| cage.conditions.clone())
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Distance {
Cosine,
Euclidean,
Dot,
}