rs-puff

Crates.iors-puff
lib.rsrs-puff
version0.1.1
created_at2026-01-17 22:32:15.80118+00
updated_at2026-01-19 22:32:45.303832+00
descriptionA modern (unofficial) Rust client for Turbopuffer
homepage
repositoryhttps://github.com/lucasgelfond/rs-puff
max_upload_size
id2051339
size144,059
lucas gelfond (lucasgelfond)

documentation

README

rs-puff

A modern Rust client for Turbopuffer, the serverless vector database.

Development

This is a bit of an experiment in agent-based coding. I have a little Rust-based Turbopuffer project I'd started on, but the only existing client I see, the unofficial ragkit/turbopuffer-client looks unmaintained and untyped. All of the existing clients are open source, making this a pretty agent-tractable problem.

For dev setup, I had Claude write an AGENTS.md that specified where the docs are, and I manually cloned all of the other clients to other-clients/, which I .gitignore-d. Then I just had Claude Opus 4.5 rip it with those instructions.

I had Claude also copy over existing integration tests; only required a few revs to get everything working well. My guess is this will work for most cases, but YMMV / this software comes with no guarantees, etc (particularly because it's all generated!)

Installation

Add to your Cargo.toml:

[dependencies]
rs-puff = "0.1"

Quick Start

use rs_puff::{Client, DistanceMetric, Filter, QueryParams, RankBy, WriteParams};
use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client from environment (reads TURBOPUFFER_API_KEY)
    let client = Client::from_env()?;

    // Get a namespace
    let ns = client.namespace("my-namespace");

    // Write documents
    let mut row = HashMap::new();
    row.insert("id".to_string(), serde_json::json!(1));
    row.insert("vector".to_string(), serde_json::json!([0.1, 0.2, 0.3, 0.4]));
    row.insert("name".to_string(), serde_json::json!("alice"));

    ns.write(WriteParams {
        upsert_rows: Some(vec![row]),
        distance_metric: Some(DistanceMetric::CosineDistance),
        ..Default::default()
    }).await?;

    // Query with vector search
    let results = ns.query(QueryParams {
        rank_by: Some(RankBy::vector("vector", vec![0.1, 0.2, 0.3, 0.4])),
        top_k: Some(10),
        filters: Some(Filter::eq("name", "alice")),
        ..Default::default()
    }).await?;

    for row in results.rows {
        println!("{:?}", row);
    }

    Ok(())
}

Client Configuration

// From environment variable TURBOPUFFER_API_KEY
let client = Client::from_env()?;

// With explicit API key
let client = Client::new("your-api-key");

// With specific region
let client = Client::with_region("your-api-key", "gcp-us-east1");

// With custom base URL
let client = Client::with_base_url("your-api-key", "https://custom.endpoint.com");

Namespace Operations

let ns = client.namespace("my-namespace");

// Write/upsert documents
ns.write(WriteParams { ... }).await?;

// Query documents
ns.query(QueryParams { ... }).await?;

// Multi-query (batch multiple queries)
ns.multi_query(MultiQueryParams { ... }).await?;

// Delete all documents
ns.delete_all().await?;

// Get namespace metadata
ns.metadata().await?;

// Get schema
ns.schema().await?;

// Hint cache warm
ns.hint_cache_warm().await?;

Filters

Filters use a tuple-based format that matches the Turbopuffer API:

use rs_puff::Filter;

// Comparison operators
Filter::eq("name", "alice")           // ["name", "Eq", "alice"]
Filter::not_eq("status", "deleted")   // ["status", "NotEq", "deleted"]
Filter::lt("age", 30)                 // ["age", "Lt", 30]
Filter::lte("age", 30)                // ["age", "Lte", 30]
Filter::gt("score", 0.5)              // ["score", "Gt", 0.5]
Filter::gte("score", 0.5)             // ["score", "Gte", 0.5]

// Set operators
Filter::r#in("status", vec!["active".into(), "pending".into()])
Filter::not_in("status", vec!["deleted".into()])
Filter::contains("tags", "rust")
Filter::contains_any("tags", vec!["rust".into(), "python".into()])

// String operators
Filter::glob("name", "a*")            // Glob pattern matching
Filter::iglob("name", "A*")           // Case-insensitive glob
Filter::regex("email", r".*@.*\.com") // Regex matching

// Logical operators
Filter::and(vec![
    Filter::gte("age", 18),
    Filter::eq("status", "active"),
])
Filter::or(vec![
    Filter::eq("role", "admin"),
    Filter::eq("role", "moderator"),
])
Filter::not(Filter::eq("deleted", true))

Ranking

use rs_puff::RankBy;

// Vector similarity (ANN)
RankBy::vector("vector", vec![0.1, 0.2, 0.3, 0.4])

// Exact k-NN
RankBy::vector_knn("vector", vec![0.1, 0.2, 0.3, 0.4])

// BM25 text search
RankBy::bm25("content", "search query")

// Attribute ordering
RankBy::asc("timestamp")
RankBy::desc("score")

// Combine rankings
RankBy::sum(vec![
    RankBy::bm25("title", "query"),
    RankBy::bm25("content", "query"),
])
RankBy::product(2.0, RankBy::bm25("title", "query"))

Distance Metrics

use rs_puff::DistanceMetric;

DistanceMetric::CosineDistance
DistanceMetric::EuclideanSquared

Listing Namespaces

use rs_puff::NamespacesParams;

let response = client.namespaces(NamespacesParams {
    prefix: Some("prod-".to_string()),
    page_size: Some(100),
    cursor: None,
}).await?;

for ns in response.namespaces {
    println!("{}", ns.id);
}

Environment Variables

  • TURBOPUFFER_API_KEY - Your Turbopuffer API key (required for Client::from_env())
  • TURBOPUFFER_REGION - Optional region (e.g., gcp-us-east1)

License

MIT

Commit count: 16

cargo fmt