Rust client
quiver-client is an async client for the REST API, built on reqwest with rustls. The terminal UI uses it. It takes and returns the types from quiver-types, so depend on both:
[dependencies]
quiver-client = { git = "https://github.com/HilthonTT/quiver" }
quiver-types = { git = "https://github.com/HilthonTT/quiver" }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Example
use quiver_client::{Client, ClientError, RecommendRequest, SearchRequest, Selector};
use quiver_types::{CollectionConfig, MetadataMode, Metric, PointInput};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), ClientError> {
// An empty key (no QUIVER_API_KEY) sends no Authorization header.
let key = std::env::var("QUIVER_API_KEY").unwrap_or_default();
let client = Client::new("http://localhost:6900")?.with_api_key(key);
let config = CollectionConfig::new(3).metric(Metric::Cosine);
client.create_collection("books", &config).await?;
let point = |id: &str, vector: Vec<f32>, year: u32| PointInput {
id: Some(id.into()),
vector,
vectors: Default::default(),
sparse_vectors: Default::default(),
metadata: Some(json!({ "year": year })),
};
let written = client
.upsert(
"books",
vec![
point("dune", vec![0.9, 0.1, 0.0], 1965),
point("foundation", vec![0.8, 0.2, 0.1], 1951),
point("hobbit", vec![0.1, 0.9, 0.2], 1937),
],
)
.await?;
println!("upserted {} points: {:?}", written.upserted, written.ids);
let mut query = SearchRequest::new(vec![1.0, 0.0, 0.0], 2);
query.filter = Some(json!({ "year": { "$lt": 1960 } }));
for hit in client.search("books", &query).await? {
println!("{} {:.4}", hit.id, hit.distance);
}
let near_dune = client.search("books", &SearchRequest::by_id("dune", 1)).await?;
let liked = RecommendRequest::new(vec!["hobbit".into()], 1);
let recommended = client.recommend("books", &liked).await?;
println!("near dune: {}, like hobbit: {}", near_dune[0].id, recommended[0].id);
let updated = client
.update_metadata("books", &Selector::Ids(vec!["dune".into()]), &json!({ "seen": true }), MetadataMode::Merge)
.await?;
let old = client.count("books", Some(&json!({ "year": { "$lt": 1960 } }))).await?;
println!("updated {updated}, {old} published before 1960");
let page = client.scroll("books", None, 2, false).await?;
println!("first page: {:?}, next cursor {:?}", page.points.iter().map(|p| &p.id).collect::<Vec<_>>(), page.next_cursor);
match client.get_point("books", "nope").await? {
Some(p) => println!("found {}", p.id),
None => println!("no point `nope`"),
}
if let Err(ClientError::Api { status, code, .. }) = client.create_collection("books", &config).await {
println!("{status} {code}");
}
Ok(())
}upserted 3 points: ["dune", "foundation", "hobbit"]
foundation 0.0369
hobbit 0.8922
near dune: foundation, like hobbit: foundation
updated 1, 2 published before 1960
first page: ["dune", "foundation"], next cursor Some("foundation")
no point `nope`
409 Conflict already_existsThe API
Client::new(url) checks the URL (http or https) and sets a 30-second request timeout and a 5-second connect timeout. For a server whose certificate a private CA signed, or one that wants a client certificate, use Client::with_tls(url, &TlsOptions { ca_pem, identity_pem }), with PEM bytes: identity_pem is the client certificate chain followed by its key. with_api_key(key) sends Authorization: Bearer <key> with every request; an empty key sends none. A Client is cheap to clone.
| Method | Route |
|---|---|
info() | GET /info, as a ServerInfo |
list_collections(), get_collection(name) | GET /collections, GET /collections/{name} |
create_collection(name, &config), drop_collection(name) | POST /collections, DELETE /collections/{name} |
update_collection(name, &CollectionUpdate) | PATCH /collections/{name} |
rename_collection(name, new_name) | POST …/rename |
list_aliases(), set_alias(alias, collection), delete_alias(alias) | GET /aliases, PUT /aliases/{alias}, DELETE /aliases/{alias} |
flush(name), compact(name) | POST …/flush, POST …/compact |
create_field_index(collection, field), drop_field_index(collection, field) | POST …/indexes, DELETE …/indexes/{field} |
upsert(collection, points) | POST …/points; returns an UpsertResult with upserted and ids |
get_point(collection, id) | GET …/points/{id}; Ok(None) if the point is missing |
get_points(collection, ids, with_vectors) | POST …/points/lookup; missing ids are skipped |
scroll(collection, cursor, limit, with_vectors) | GET …/points; a ScrollPage |
scroll_with(collection, &ScrollRequest) | POST …/points/scroll, with a filter and an order |
delete_points(collection, ids), delete_where(collection, filter) | POST …/points/delete |
count(collection, filter) | POST …/points/count |
facet(collection, field, filter, limit) | POST …/points/facet; FacetCounts, most common first |
update_metadata(collection, selector, metadata, mode) | POST …/points/metadata |
search(collection, &SearchRequest) | POST …/search |
search_batch(collection, &[SearchRequest]) | POST …/search/batch; one result list per search |
search_groups(collection, &GroupSearchRequest) | POST …/search/groups; HitGroups |
recommend(collection, &RecommendRequest) | POST …/recommend |
discover(collection, &DiscoverRequest) | POST …/discover |
query(collection, &QueryRequest) | POST …/query; ScoredHits, best first |
create_snapshot(collection), list_snapshots(collection), get_snapshot(id) | POST …/snapshots, GET /snapshots, GET /snapshots/{id} |
download_snapshot(id), upload_snapshot(bytes) | GET /snapshots/{id}/download, POST /snapshots; these two allow 30 minutes rather than 30 seconds |
restore_snapshot(id, name, replace), delete_snapshot(id) | POST /snapshots/{id}/restore, DELETE /snapshots/{id} |
SearchRequest::new(vector, k) and SearchRequest::by_id(id, k) return metadata and no vectors; their public fields (filter, ef, max_distance, with_vectors, with_metadata) change the rest. SearchRequest::sparse(using, vector, k) searches a sparse vector, and .using(name) a named dense one. RecommendRequest::new(positive, k) works the same way, with negative and using fields. A hybrid query is QueryRequest::new(vec![Prefetch::dense(vector), Prefetch::sparse("text", sparse).weight(2.0)], k); Prefetch also has .using(name) and .limit(n), and QueryRequest has .fusion(Fusion::Dbsf), .rescore(Rescore { vector, using }) and .offset(n). Searches and recommendations take an offset field (SearchRequest also .offset(n)). Filters are serde_json::Values in the filter syntax.
GroupSearchRequest::new(vector, group_by, groups, group_size), ::by_id(...) and ::sparse(using, vector, ...) take .using(name) and .filter(value). DiscoverRequest::new(target, context, k) takes an optional Example::Id(id) or Example::Vector(v) target and ContextPair { positive, negative }s. ScrollRequest::new(limit) takes .cursor(c), .filter(value), .order_by(OrderBy::desc("year")) and .with_vectors(true).
The ids ., .., lookup, delete, count, scroll, facet and metadata cannot be addressed with GET …/points/{id}, so get_point looks those up with POST …/points/lookup instead.
Errors
ClientError has four variants:
Api { status, code, message }— the server answered with an error;codeis the server's error code, such asnot_foundordimension_mismatch.Http(reqwest::Error)— no usable answer: connection refused, a timeout, a body that does not decode.InvalidUrl(String)—Client::newcould not use the URL.Tls(String)—TlsOptionsheld no usable certificate.
The Rust client covers the whole REST API except GET /metrics.