quiver
Documentation menu

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_exists

The 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.

MethodRoute
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; code is the server's error code, such as not_found or dimension_mismatch.
  • Http(reqwest::Error) — no usable answer: connection refused, a timeout, a body that does not decode.
  • InvalidUrl(String) — Client::new could not use the URL.
  • Tls(String) — TlsOptions held no usable certificate.

The Rust client covers the whole REST API except GET /metrics.