use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use crate::client::Client;
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum VectorMetric {
#[default]
Cosine,
Dot,
L2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum VectorQuantization {
#[default]
None,
Int8,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct VectorItem {
pub id: String,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub vector: Vec<f32>,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub metadata: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct VectorHit {
pub id: String,
#[serde(default)]
pub score: f32,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub metadata: Map<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct VectorSearchResult {
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub results: Vec<VectorHit>,
#[serde(default)]
pub index: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct VectorCollectionSummary {
pub name: String,
#[serde(default)]
pub dimension: usize,
#[serde(default)]
pub metric: VectorMetric,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct VectorCollectionInfo {
pub collection: String,
#[serde(default)]
pub dimension: usize,
#[serde(default)]
pub metric: VectorMetric,
#[serde(default)]
pub count: i64,
#[serde(default)]
pub quantization: VectorQuantization,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct VectorPage {
pub collection: String,
#[serde(default)]
pub count: usize,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub vectors: Vec<VectorItem>,
#[serde(default)]
pub truncated: bool,
#[serde(default)]
pub total: i64,
}
fn op(body: Value) -> Value {
json!({"Vector": body})
}
impl Client {
pub fn vector_create_collection(
&mut self,
collection: &str,
dimension: usize,
metric: VectorMetric,
) -> Result<()> {
self.vector_create_collection_quantized(
collection,
dimension,
metric,
VectorQuantization::None,
)
}
pub fn vector_create_collection_quantized(
&mut self,
collection: &str,
dimension: usize,
metric: VectorMetric,
quantization: VectorQuantization,
) -> Result<()> {
self.send(op(json!({"CreateCollection": {
"collection": collection,
"dimension": dimension,
"metric": metric,
"quantization": quantization,
}})))
.map(|_| ())
}
pub fn vector_drop_collection(&mut self, collection: &str) -> Result<()> {
self.send(op(json!({"DropCollection": {"collection": collection}})))
.map(|_| ())
}
pub fn vector_list_collections(&mut self) -> Result<Vec<VectorCollectionSummary>> {
#[derive(Deserialize)]
struct Details {
#[serde(default)]
details: Vec<VectorCollectionSummary>,
}
let response = self.send(op(Value::String("ListCollections".into())))?;
let out: Details = response.decode("ListCollections")?;
Ok(out.details)
}
pub fn vector_describe_collection(&mut self, collection: &str) -> Result<VectorCollectionInfo> {
let response = self.send(op(
json!({"DescribeCollection": {"collection": collection}}),
))?;
response.decode("DescribeCollection")
}
pub fn vector_upsert(
&mut self,
collection: &str,
id: &str,
vector: &[f32],
metadata: Option<&Map<String, Value>>,
) -> Result<()> {
self.send(op(json!({"Upsert": {
"collection": collection,
"id": id,
"vector": vector,
"metadata": metadata,
}})))
.map(|_| ())
}
pub fn vector_get(&mut self, collection: &str, id: &str) -> Result<Option<VectorItem>> {
let response = self.send(op(json!({"Get": {"collection": collection, "id": id}})))?;
let data = response.expect("Json", "Get")?;
if data.is_null() {
return Ok(None);
}
response.decode("Get").map(Some)
}
pub fn vector_delete(&mut self, collection: &str, id: &str) -> Result<()> {
self.send(op(json!({"Delete": {"collection": collection, "id": id}})))
.map(|_| ())
}
pub fn vector_search(
&mut self,
collection: &str,
vector: &[f32],
top_k: usize,
) -> Result<VectorSearchResult> {
self.vector_search_filtered(collection, vector, top_k, &BTreeMap::new())
}
pub fn vector_search_filtered(
&mut self,
collection: &str,
vector: &[f32],
top_k: usize,
filter: &BTreeMap<String, Value>,
) -> Result<VectorSearchResult> {
let filter = if filter.is_empty() {
Value::Null
} else {
json!(filter)
};
let response = self.send(op(json!({"Search": {
"collection": collection,
"vector": vector,
"top_k": top_k,
"filter": filter,
}})))?;
response.decode("Search")
}
pub fn vector_list_vectors(
&mut self,
collection: &str,
limit: Option<usize>,
offset: Option<usize>,
) -> Result<VectorPage> {
let mut body = Map::new();
body.insert("collection".into(), json!(collection));
if let Some(limit) = limit.filter(|l| *l > 0) {
body.insert("limit".into(), json!(limit));
}
if let Some(offset) = offset.filter(|o| *o > 0) {
body.insert("offset".into(), json!(offset));
}
let response = self.send(op(json!({"ListVectors": Value::Object(body)})))?;
response.decode("ListVectors")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metrics_and_quantizations_use_the_servers_spelling() {
assert_eq!(
serde_json::to_value(VectorMetric::Cosine).unwrap(),
"cosine"
);
assert_eq!(serde_json::to_value(VectorMetric::Dot).unwrap(), "dot");
assert_eq!(serde_json::to_value(VectorMetric::L2).unwrap(), "l2");
assert_eq!(
serde_json::to_value(VectorQuantization::None).unwrap(),
"none"
);
assert_eq!(
serde_json::to_value(VectorQuantization::Int8).unwrap(),
"int8"
);
}
#[test]
fn metrics_round_trip_from_the_wire() {
let metric: VectorMetric = serde_json::from_str("\"l2\"").unwrap();
assert_eq!(metric, VectorMetric::L2);
assert_eq!(VectorMetric::default(), VectorMetric::Cosine);
assert_eq!(VectorQuantization::default(), VectorQuantization::None);
}
#[test]
fn a_hit_decodes_with_its_metadata() {
let hit: VectorHit =
serde_json::from_value(json!({"id": "a", "score": -0.02, "metadata": {"kind": "doc"}}))
.unwrap();
assert_eq!(hit.id, "a");
assert_eq!(hit.metadata["kind"], "doc");
}
}