# Architecture - DRAM: 300GB/s - NVMe: 10GB/s - S3: <5GB/s - DRAM: ~100ns - NVMe: 100us - S3: ~100ms - DRAM: $2/GB-mo - SSD: $0.08/GB-mo - S3: $0.02/GB-mo **S3 GET LATENCY** - p50: 63ms - p99: 78ms - p999: 118ms **S3 PUT LATENCY** - p50: 100ms - p99: 195ms - p999: 274ms The API routes to a cluster of Rust binaries that access your database on object storage (see [regions](https://turbopuffer.com/docs/regions) for more on routing). After the first query, the namespace's documents are cached on NVMe SSD. Subsequent queries are routed to the same query node for cache locality, but any query node can serve queries from any namespace. The first query to a namespace reads object storage directly and is slow (p50=874ms for 1M documents), but subsequent, cached queries to that node are faster (p50=14ms for 1M documents). Many use-cases can send a [pre-flight query to hint that the client will send latency-sensitive requests in the near future](/docs/warm-cache). turbopuffer is a multi-tenant service, which means each `./tpuf` binary handles requests for multiple tenants. This keeps costs low. Enterprise customers can be isolated on request either through [single-tenancy clusters, or BYOC](/docs/security): ``` ╔═ turbopuffer ════════════════════════════╗ ╔════════════╗ ║ ║░ ║ ║░ ║ ┏━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━┓ ║░ ║ client ║░───API──▶║ ┃ Memory/ ┃────▶┃ Object ┃ ║░ ║ ║░ ║ ┃ SSD Cache ┃ ┃ Storage (S3) ┃ ║░ ╚════════════╝░ ║ ┗━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━┛ ║░ ░░░░░░░░░░░░░░ ║ ║░ ╚══════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` Each namespace has its own prefix on object storage. turbopuffer uses a write-ahead log (WAL) to ensure consistency. Every write adds a new file to the WAL directory inside the namespace's prefix. If a write returns successfully, data is guaranteed to be durably written to object storage. This means high write throughput (~10,000+ vectors/sec), at the cost of high write latency (p50=165ms for 500kB). Each namespace can currently write 1 WAL entry per second. Concurrent writes to the same namespace are batched into the same entry. If a new batch is started within one second of the previous one, it will take up to 1 second to commit. ``` mem buffer ┌──────┐ UPSERT/PATCH/DELETE │░░░░░░│ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ─▶│░░░░░░│ │░░░░░░│ └──┬───┘ WAL │ group commit (<= 1/s) s3://tpuf/{namespace_id}/wal ▼ ╔═════════════════════════════════════════════════════════╗ ║┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ║░ ║│██████│ │██████│ │▓▓▓▓▓▓│ │▓▓▓▓▓▓│ │▓▓▓▓▓▓│ ║░ ║│██████│ │██████│ │▓▓▓▓▓▓│ │▓▓▓▓▓▓│ │▓▓▓▓▓▓│ ║░ ║│██████│ │██████│ │▓▓▓▓▓▓│ │▓▓▓▓▓▓│ │▓▓▓▓▓▓│ ║░ ║└──────┘ └──────┘ └──────┘ └──────┘ └──────┘ ║░ ║ 01.bin 02.bin ▲ 03.bin 04.bin 05.bin ▲ (06.bin) ║░ ╚═════════════════│══════════════════════════│════════════╝░ ░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░ │ │ index cursor CAS commit point █ indexed + committed ▓ committed, unindexed ░ written, not committed ~10ms for consistent read ``` After data is committed to the log, it is asynchronously indexed to enable efficient retrieval (■). Any data that has not yet been indexed is still available to search (◈), with a slower exhaustive search of recent data in the log. turbopuffer provides strong consistency by default, i.e. if you perform a write, a subsequent query will immediately see the write. However, you can configure your queries to be [eventually consistent](/docs/query#param-consistency) for lower warm latency. With eventual consistency, staleness of up to about one hour can be observed in the worst case. Both the approximate nearest neighbour (ANN) index we use for vectors, as well as the inverted [BM25](https://en.wikipedia.org/wiki/Okapi_BM25) index we use for full-text search have been optimized for object storage to provide good cold latency (~500ms on 1M documents). Additionally, we build exact indexes for [metadata filtering](/docs/query#filtering-parameters). ``` ╔═══turbopuffer region═════════════╗ ║ ┌─────────────────────────┐ ╠──┐ ║ │ ./tpuf indexer │ ║░ │ ║ └─────────────────────────┘ ║░ │ ║ ┌─────────────────────────┐ ║░ │ ║ │ ./tpuf indexer │ ║░ │ ║ └─────────────────────────┘ ║░ │ ╔═══Object Storage══════════════╗ ║ ║░ │ ║ ┏━━Indexing Queue━━━━━━━━━━━┓ ║░ ║ ┌─────────────────────────┐ ║░ │ ║ ┃■■■■■■■■■ ┃ ║░ ║ │ ./tpuf query │ ║░ │ ║ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║░ ║ │┌─Memory Cache──────────┐│ ║░ │ ║ ┏━/{org_id}/{namespace}━━━━━┓ ║░ ║ ││■■■■■■■■■■ ││ ║░ │ ║ ┃ ┏━/wal━━━━━━━━━━━━━━━━━━┓ ┃ ║░ ║ ┌─▶│└───────────────────────┘│ ║░ └──▶║ ┃ ┃■■■■■■■■■■■■■■■◈◈◈◈ ┃ ┃ ║░ ║ │ │┌─NVMe Cache────────────┐│ ║░ ║ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ║░ ║ │ ││■■■■■■■■■■■■■■■■■■■■■ ││ ║░ ┌──▶║ ┃ ┏━/index━━━━━━━━━━━━━━━━┓ ┃ ║░ ┌──╩─┐ │ │└───────────────────────┘│ ║░ │ ║ ┃ ┃■■■■■■■■■■■■■■■ ┃ ┃ ║░ ╔══════════╗ │ │ │ └─────────────────────────┘ ║░ │ ║ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ║░ ║ Client ║───▶│ LB │─┤ ┌─────────────────────────┐ ║░ │ ║ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║░ ╚══════════╝░ │ │ │ │ ./tpuf query │ ║░ │ ╚═══════════════════════════════╝░ ░░░░░░░░░░░░ └──╦─┘ │ │┌─Memory Cache──────────┐│ ║░ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ║ │ ││■■■■■■■■■■ ││ ╠──┘ ║ └─▶│└───────────────────────┘│ ║░ ║ │┌─NVMe Cache────────────┐│ ║░ ║ ││■■■■■■■■■■■■■■■■■■■■■ ││ ║░ ║ │└───────────────────────┘│ ║░ ║ └─────────────────────────┘ ║░ ╚══════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` Vector indexes are based on [SPFresh](https://dl.acm.org/doi/10.1145/3600006.3613166). SPFresh is a centroid-based approximate nearest neighbour index. It has a fast index for locating the nearest centroids to the query vector. A centroid-based index works well for object storage as it minimizes roundtrips and write-amplification, compared to graph-based indexes like HNSW or DiskANN. On a cold query, the centroid index is downloaded from object storage. Once the closest centroids are located, we simply fetch each cluster's offset in one, massive roundtrip to object storage. ``` ┌ /{org_id}/{namespace}/index ─────────────────────────────────┐ │ │ │ centroids.bin ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ │ │ ▐█▐█▐█▐█▐█▐█▐█▐█ ╎ Namespace Config ╎ │ │ └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ │ │ │ │ ┌ clusters-1.bin ──────┐ ┌ clusters-2.bin ──────────┐ │ │ │ ▐█▐█▐█▐█ ▐█▐█ ▐█▐█▐█ │ │ ▐█▐█ ▐█▐█▐█▐█ ▐█▐█▐█▐█ │ │ │ └──────────────────────┘ └──────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ ``` In reality, there are more roundtrips required for turbopuffer to support consistent writes and work on large indexes. From first principles, each roundtrip to object storage takes ~100ms. The 3-4 required roundtrips for a cold query often take as little as ~400ms. When the namespace is cached in NVME/memory rather than fetched directly from object storage, the query time drops dramatically to p50=14. The roundtrip to object storage for consistency, which we can relax on request for eventually consistent sub 10ms queries. ``` ┌──────────────┐ ┊ ┌──────────────────┐ ┊ ┌──────────────┐ │ │ ┊ │ Filter index │ ┊ │ │ │ Metadata¹ │ ┊ ├──────────────────┤ ┊ │ Clusters │ │ │ ┊ │ Centroid index │ ┊ │ │ └──────────────┘ ┊ ├──────────────────┤ ┊ └──────────────┘ ┊ │ Unindexed WAL │ ┊ ┊ └──────────────────┘ ┊ ────────────────────────────────────────────────────────────────▶ Roundtrip 1 ┊ Roundtrip 2² ┊ Roundtrip 3 ``` 1. *Metadata is downloaded for the turbopuffer storage engine. The storage engine is optimized for minimizing roundtrips.* 2. *The second roundtrip starts navigating the first level of the indexes. In many cases, only one additional roundtrip is required. But the query planner makes decisions about how to efficiently navigate the indexes. It needs to settle tradeoffs between additional roundtrips and fetching more data in an existing roundtrip.* --- This page: [/docs/architecture.md](https://turbopuffer.com/docs/architecture.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Audit Logs turbopuffer provides audit logs for customers on Scale and Enterprise plans. [Contact us](/contact) to enable audit logs for your organization. ## Retention Audit logs have a 30-day retention period by default, which can be extended on request. ## Log Streams Audit log events can be streamed to an external destination. Supported destinations include SIEM providers (Datadog, Splunk, Microsoft Sentinel), object storage (AWS S3, Google Cloud Storage), and custom HTTPS endpoints. Log streaming is available on [Scale and Enterprise](/pricing) plans and can be configured from the customer settings page. ## Events | Action | Actor | Target | | --- | --- | --- | | `invitation-created` | [User](#user) | [Invitation](#invitation) | | `invitation-accepted` | [User](#user) | [Invitation](#invitation) | | `invitation-revoked` | [User](#user) | [Invitation](#invitation) | | `user-added` | [System](#system) | [User](#user) | | `user-removed` | [User](#user) | [User](#user) | | `api-key-created` | [User](#user) | [API Key](#api-key) | | `api-key-marked-as-expired` | [User](#user) | [API Key](#api-key) | | `session-created` | [User](#user) | [Session](#session) | | `session-revoked` | [User](#user) | [Session](#session) | ### Examples A user creates an API key: ```json { "action": "api-key-created", "occurred_at": "2026-04-13T14:22:08Z", "actor": { "type": "user", "id": "V1StGXR8_Z5jdHi6B-myTq", "name": "ada@example.com" }, "targets": [ { "type": "api-key", "id": "8fW3zNcY6tRo1kGpLvAe2b/production", "name": "production", "metadata": { "suffix": "a1b2" } } ], "context": { "location": "203.0.113.42" }, "metadata": { "session_id": "sess_lK9eT0vB3xYq2pNwA4fJ7" } } ``` A user marks an API key as expired: ```json { "action": "api-key-marked-as-expired", "occurred_at": "2026-04-13T14:25:11Z", "actor": { "type": "user", "id": "V1StGXR8_Z5jdHi6B-myTq", "name": "ada@example.com" }, "targets": [ { "type": "api-key", "id": "8fW3zNcY6tRo1kGpLvAe2b/production", "name": "production", "metadata": { "suffix": "a1b2" } } ], "context": { "location": "203.0.113.42" }, "metadata": { "session_id": "sess_lK9eT0vB3xYq2pNwA4fJ7" } } ``` ## Schemas Each audit log event includes the `action` that was taken, the time it `occurred_at`, the `actor` who performed it, the `targets` that were affected, the client IP in `context.location`, and optional `metadata` about the session. ```typescript type AuditLogEvent = { action: string; occurred_at: string; // ISO 8601 datetime actor: Actor; targets: Target[]; context: { location: string; // client IP address }; metadata: { session_id?: string; impersonator?: string; // email of turbopuffer admin acting on behalf of // the customer (requires customer authorization) impersonation_reason?: string; }; }; type Actor = User | System; type Target = User | ApiKey | Invitation | Session; ``` ### User ```typescript type User = { type: "user"; id: string; name: string; // email address }; ``` ### API Key ```typescript type ApiKey = { type: "api-key"; id: string; name: string; // display name of the key metadata: { suffix: string; // last 4 characters of the key }; }; ``` ### Invitation ```typescript type Invitation = { type: "invitation"; id: string; name: string; // email address of invited user metadata?: { invited_user_id: string; }; }; ``` ### Session ```typescript type Session = { type: "session"; id: string; name: string; // session ID metadata: { user_agent: string; impersonator?: string; // email of turbopuffer admin acting on behalf of // the customer (requires customer authorization) impersonation_reason?: string; }; }; ``` ### System A special actor representing actions performed automatically rather than by a specific user. ```typescript type System = { type: "system"; id: "system"; name: "System"; }; ``` --- This page: [/docs/audit-logs.md](https://turbopuffer.com/docs/audit-logs.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Cross-Region Backups **Copy Throughput** (Measured with a 1GB namespace. Cross-region may be 20-35% slower depending on distance.) - same-region: 72 MB/s - cross-region: 50 MB/s ``` ┌─aws-us-east-1 (source)─────┐ ┌─aws-us-west-2 (dest)───┐ │ │░ │ │░ │ ┌──────────────────────┐ │░ │ ┌──────────────────┐ │░ │ │ my-namespace │ │░ ──────────▶ │ │ my-namespace-copy│ │░ │ └──────────────────────┘ │░ │ └──────────────────┘ │░ │ │░ │ ▲ │░ └────────────────────────────┘░ └────────────┼───────────┘░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░│░░░░░░░░░░░░░ │ ──copy_from_namespace─┘ ``` turbopuffer supports efficient namespace copies across [regions](/docs/regions) via [`copy_from_namespace`](/docs/write#param-copy_from_namespace) for geo-redundancy, disaster recovery, and accidental deletion protection. We don't currently offer automated backups. Historically, customers have rebuilt from their primary data source when needed, but cross-region copies are now often a better option. [Branching](/docs/branching) provides constant-time namespace snapshots, but shares underlying storage with the source namespace. Use `copy_from_namespace` for full data isolation. Copies are performed entirely server-side, so there's no data transfer through your infrastructure. They're billed at up to a 75% write discount and create fully writable namespaces you can use however you like. Cross-region copies also bill returned bytes for the logical size copied. Storage is billed at standard rates, but since you're not querying backup namespaces, they're cheap to keep around, making daily or weekly snapshots practical. Copies work across regions and across cloud providers (e.g., AWS to GCP). ## CMEK encryption To encrypt the backup with a [customer managed encryption key (CMEK)](/docs/encryption), specify an encryption key in the [`encryption` parameter](/docs/write#param-encryption). The key must be available in the destination region. Specifying an encryption key is mandatory if the source namespace has CMEK encryption enabled. ## Running Backups on Schedule To maintain up-to-date backups, run cross-region copies on a regular schedule. Here's an example script (run via cron or any scheduler) that backs up all namespaces matching a prefix. It appends the date to each backup namespace name and automatically cleans up backups older than 7 days: ```python # /// script # requires-python = ">=3.10" # dependencies = ["turbopuffer"] # /// import os import time import turbopuffer # Configuration SOURCE_REGION = "gcp-us-central1" BACKUP_REGION = "gcp-us-west1" SOURCE_PREFIX = "fts-" # Back up all namespaces starting with "fts-" BACKUP_PREFIX = "backup-" # Backup namespaces will be "backup-{name}-{date}" RETENTION_DAYS = 7 source_client = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), region=SOURCE_REGION ) backup_client = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), region=BACKUP_REGION ) timestamp = int(time.time()) # Unix epoch seconds start_time = time.time() # Step 1: Back up each namespace matching the source prefix print("Starting backups...") namespaces = list(source_client.namespaces(prefix=SOURCE_PREFIX)) for ns in namespaces: backup_name = f"{BACKUP_PREFIX}{ns.id}-{timestamp:010d}" print(f" Backing up: {ns.id}") backup_ns = backup_client.namespace(backup_name) backup_ns.copy_from( source_namespace=ns.id, source_region=SOURCE_REGION, # if backing up to a different organization, include source_api_key: # source_api_key="", ) # Step 2: Delete old backups beyond the retention period (after successful backup) print("Cleaning up old backups...") cutoff = int(time.time()) - RETENTION_DAYS * 86400 deleted = 0 for ns in backup_client.namespaces(prefix=BACKUP_PREFIX): # Safety check: only delete namespaces that match our backup prefix assert len(BACKUP_PREFIX) > 0 and ns.id.startswith( BACKUP_PREFIX ), f"Refusing to delete namespace that doesn't match backup prefix: {ns.id}" # Extract timestamp from backup namespace name (e.g., "backup-prod-users-1234567890") if len(ns.id) >= 10: try: backup_time = int(ns.id[-10:]) if backup_time < cutoff: print(f" Deleting: {ns.id}") backup_client.namespace(ns.id).delete_all() deleted += 1 except ValueError: print( f" Skipping {ns.id}: invalid timestamp format", file=__import__("sys").stderr, ) print( f"Done: backed up {len(namespaces)} namespaces, deleted {deleted} old backups in {time.time() - start_time:.1f}s" ) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; // Configuration const SOURCE_REGION = "gcp-us-central1"; const BACKUP_REGION = "gcp-us-west1"; const SOURCE_PREFIX = "fts-"; // Back up all namespaces starting with "fts-" const BACKUP_PREFIX = "backup-"; // Backup namespaces will be "backup-{name}-{date}" const RETENTION_DAYS = 7; const sourceClient = new Turbopuffer({ region: SOURCE_REGION }); const backupClient = new Turbopuffer({ region: BACKUP_REGION }); const timestamp = Math.floor(Date.now() / 1000); // Unix epoch seconds const startTime = Date.now(); // Step 1: Back up each namespace matching the source prefix console.log("Starting backups..."); const namespaces: string[] = []; for await (const ns of sourceClient.namespaces({ prefix: SOURCE_PREFIX })) { namespaces.push(ns.id); const backupName = `${BACKUP_PREFIX}${ns.id}-${String(timestamp).padStart(10, "0")}`; console.log(` Backing up: ${ns.id}`); const backupNs = backupClient.namespace(backupName); await backupNs.copyFrom({ source_namespace: ns.id, source_region: SOURCE_REGION, // if backing up to a different organization, include source_api_key: // source_api_key: "", }); } // Step 2: Delete old backups beyond the retention period (after successful backup) console.log("Cleaning up old backups..."); const cutoff = Math.floor(Date.now() / 1000) - RETENTION_DAYS * 86400; let deleted = 0; for await (const ns of backupClient.namespaces({ prefix: BACKUP_PREFIX })) { // Safety check: only delete namespaces that match our backup prefix if (BACKUP_PREFIX.length === 0 || !ns.id.startsWith(BACKUP_PREFIX)) { throw new Error(`Refusing to delete namespace that doesn't match backup prefix: ${ns.id}`); } // Extract timestamp from backup namespace name (e.g., "backup-prod-users-1234567890") if (ns.id.length >= 10) { const backupTime = parseInt(ns.id.slice(-10), 10); if (!isNaN(backupTime) && backupTime < cutoff) { console.log(` Deleting: ${ns.id}`); await backupClient.namespace(ns.id).deleteAll(); deleted++; } } } const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log( `Done: backed up ${namespaces.length} namespaces, deleted ${deleted} old backups in ${elapsed}s` ); ``` ```go package main import ( "context" "fmt" "os" "strconv" "strings" "time" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) // Configuration const ( SourceRegion = "gcp-us-central1" BackupRegion = "gcp-us-west1" SourcePrefix = "fts-" // Back up all namespaces starting with "fts-" BackupPrefix = "backup-" // Backup namespaces will be "backup-{name}-{date}" RetentionDays = 7 ) func main() { ctx := context.Background() sourceClient := turbopuffer.NewClient(option.WithRegion(SourceRegion)) backupClient := turbopuffer.NewClient(option.WithRegion(BackupRegion)) timestamp := time.Now().Unix() // Unix epoch seconds startTime := time.Now() // Step 1: Back up each namespace matching the source prefix fmt.Println("Starting backups...") var namespaceCount int namespaces := sourceClient.NamespacesAutoPaging(ctx, turbopuffer.NamespacesParams{ Prefix: turbopuffer.String(SourcePrefix), }) for namespaces.Next() { ns := namespaces.Current() namespaceCount++ backupName := fmt.Sprintf("%s%s-%010d", BackupPrefix, ns.ID, timestamp) fmt.Printf(" Backing up: %s\n", ns.ID) backupNs := backupClient.Namespace(backupName) _, err := (&backupNs).CopyFrom(ctx, turbopuffer.NamespaceCopyFromParams{ SourceNamespace: ns.ID, SourceRegion: turbopuffer.String(SourceRegion), // if backing up to a different organization, include source_api_key: // SourceAPIKey: turbopuffer.String(""), }) if err != nil { panic(err) } } if err := namespaces.Err(); err != nil { panic(err) } // Step 2: Delete old backups beyond the retention period (after successful backup) fmt.Println("Cleaning up old backups...") cutoff := time.Now().Unix() - int64(RetentionDays*86400) deleted := 0 backups := backupClient.NamespacesAutoPaging(ctx, turbopuffer.NamespacesParams{ Prefix: turbopuffer.String(BackupPrefix), }) for backups.Next() { ns := backups.Current() // Safety check: only delete namespaces that match our backup prefix if len(BackupPrefix) == 0 || !strings.HasPrefix(ns.ID, BackupPrefix) { panic(fmt.Sprintf("Refusing to delete namespace that doesn't match backup prefix: %s", ns.ID)) } // Extract timestamp from backup namespace name (e.g., "backup-prod-users-1234567890") if len(ns.ID) >= 10 { backupTime, err := strconv.ParseInt(ns.ID[len(ns.ID)-10:], 10, 64) if err != nil { fmt.Fprintf(os.Stderr, " Skipping %s: invalid timestamp format\n", ns.ID) continue } if backupTime < cutoff { fmt.Printf(" Deleting: %s\n", ns.ID) delNs := backupClient.Namespace(ns.ID) if _, err := (&delNs).DeleteAll(ctx, turbopuffer.NamespaceDeleteAllParams{}); err != nil { panic(err) } deleted++ } } } if err := backups.Err(); err != nil { panic(err) } elapsed := time.Since(startTime).Seconds() fmt.Printf("Done: backed up %d namespaces, deleted %d old backups in %.1fs\n", namespaceCount, deleted, elapsed) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.*; import com.turbopuffer.models.namespaces.*; public class BackupNamespaces { // Configuration static final String SOURCE_REGION = "gcp-us-central1"; static final String BACKUP_REGION = "gcp-us-west1"; static final String SOURCE_PREFIX = "fts-"; // Back up all namespaces starting with "fts-" static final String BACKUP_PREFIX = "backup-"; // Backup namespaces will be "backup-{name}-{date}" static final int RETENTION_DAYS = 7; public static void main(String[] args) { var baseClient = TurbopufferOkHttpClient.builder().fromEnv(); var sourceClient = baseClient.region(SOURCE_REGION).build(); var backupClient = baseClient.region(BACKUP_REGION).build(); var timestamp = System.currentTimeMillis() / 1000; // Unix epoch seconds var startTime = System.currentTimeMillis(); // Step 1: Back up each namespace matching the source prefix System.out.println("Starting backups..."); int namespaceCount = 0; var namespaces = sourceClient.namespaces( ClientNamespacesParams.builder().prefix(SOURCE_PREFIX).build() ); for (var ns : namespaces.autoPager()) { namespaceCount++; var backupName = String.format("%s%s-%010d", BACKUP_PREFIX, ns.id(), timestamp); System.out.println(" Backing up: " + ns.id()); var backupNs = backupClient.namespace(backupName); backupNs.copyFrom( NamespaceCopyFromParams.builder() .sourceNamespace(ns.id()) .sourceRegion(SOURCE_REGION) // if backing up to a different organization, include source_api_key: // .sourceApiKey("") .build() ); } // Step 2: Delete old backups beyond the retention period (after successful backup) System.out.println("Cleaning up old backups..."); var cutoff = System.currentTimeMillis() / 1000 - RETENTION_DAYS * 86400L; int deleted = 0; var backups = backupClient.namespaces( ClientNamespacesParams.builder().prefix(BACKUP_PREFIX).build() ); for (var ns : backups.autoPager()) { // Safety check: only delete namespaces that match our backup prefix assert !BACKUP_PREFIX.isEmpty() && ns .id() .startsWith( BACKUP_PREFIX ) : "Refusing to delete namespace that doesn't match backup prefix: " + ns.id(); // Extract timestamp from backup namespace name (e.g., "backup-prod-users-1234567890") var name = ns.id(); if (name.length() >= 10) { try { var backupTime = Long.parseLong(name.substring(name.length() - 10)); if (backupTime < cutoff) { System.out.println(" Deleting: " + ns.id()); backupClient.namespace(ns.id()).deleteAll(); deleted++; } } catch (NumberFormatException e) { System.err.println(" Skipping " + ns.id() + ": invalid timestamp format"); } } } var elapsed = (System.currentTimeMillis() - startTime) / 1000.0; System.out.printf( "Done: backed up %d namespaces, deleted %d old backups in %.1fs%n", namespaceCount, deleted, elapsed ); } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using Turbopuffer.Models; using Turbopuffer.Models.Namespaces; // Configuration const string SOURCE_REGION = "gcp-us-central1"; const string BACKUP_REGION = "gcp-us-west1"; const string SOURCE_PREFIX = "fts-"; // Back up all namespaces starting with "fts-" const string BACKUP_PREFIX = "backup-"; // Backup namespaces will be "backup-{name}-{date}" const int RETENTION_DAYS = 7; using var sourceClient = new TurbopufferClient { Region = SOURCE_REGION }; using var backupClient = new TurbopufferClient { Region = BACKUP_REGION }; var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // Unix epoch seconds var startTime = DateTimeOffset.UtcNow; // Step 1: Back up each namespace matching the source prefix Console.WriteLine("Starting backups..."); int namespaceCount = 0; var namespaces = await sourceClient.Namespaces( new ClientNamespacesParams { Prefix = SOURCE_PREFIX } ); await foreach (var ns in namespaces.Paginate()) { namespaceCount++; var backupName = $"{BACKUP_PREFIX}{ns.ID}-{timestamp:D10}"; Console.WriteLine($" Backing up: {ns.ID}"); var backupNs = backupClient.Namespace(backupName); await backupNs.CopyFrom( new NamespaceCopyFromParams { SourceNamespace = ns.ID, SourceRegion = SOURCE_REGION, // if backing up to a different organization, include source_api_key: // SourceApiKey = "", } ); } // Step 2: Delete old backups beyond the retention period (after successful backup) Console.WriteLine("Cleaning up old backups..."); var cutoff = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - RETENTION_DAYS * 86400L; int deleted = 0; var backups = await backupClient.Namespaces( new ClientNamespacesParams { Prefix = BACKUP_PREFIX } ); await foreach (var ns in backups.Paginate()) { // Safety check: only delete namespaces that match our backup prefix if (BACKUP_PREFIX.Length == 0 || !ns.ID.StartsWith(BACKUP_PREFIX)) { throw new InvalidOperationException( $"Refusing to delete namespace that doesn't match backup prefix: {ns.ID}" ); } // Extract timestamp from backup namespace name (e.g., "backup-prod-users-1234567890") var name = ns.ID; if (name.Length >= 10) { if (long.TryParse(name.Substring(name.Length - 10), out var backupTime)) { if (backupTime < cutoff) { Console.WriteLine($" Deleting: {ns.ID}"); await backupClient.Namespace(ns.ID).DeleteAll(new NamespaceDeleteAllParams()); deleted++; } } else { Console.Error.WriteLine($" Skipping {ns.ID}: invalid timestamp format"); } } } var elapsed = (DateTimeOffset.UtcNow - startTime).TotalSeconds; Console.WriteLine( $"Done: backed up {namespaceCount} namespaces, deleted {deleted} old backups in {elapsed:F1}s" ); ``` ```ruby require "turbopuffer" # Configuration SOURCE_REGION = "gcp-us-central1" BACKUP_REGION = "gcp-us-west1" SOURCE_PREFIX = "fts-" # Back up all namespaces starting with "fts-" BACKUP_PREFIX = "backup-" # Backup namespaces will be "backup-{name}-{date}" RETENTION_DAYS = 7 source_client = Turbopuffer::Client.new(region: SOURCE_REGION) backup_client = Turbopuffer::Client.new(region: BACKUP_REGION) timestamp = Time.now.to_i # Unix epoch seconds start_time = Time.now # Step 1: Back up each namespace matching the source prefix puts "Starting backups..." namespace_count = 0 source_client.namespaces(prefix: SOURCE_PREFIX).auto_paging_each do |ns| namespace_count += 1 backup_name = "#{BACKUP_PREFIX}#{ns.id}-#{format("%010d", timestamp)}" puts " Backing up: #{ns.id}" backup_ns = backup_client.namespace(backup_name) backup_ns.copy_from( source_namespace: ns.id, source_region: SOURCE_REGION, # if backing up to a different organization, include source_api_key: # source_api_key: "", ) end # Step 2: Delete old backups beyond the retention period (after successful backup) puts "Cleaning up old backups..." cutoff = Time.now.to_i - (RETENTION_DAYS * 86400) deleted = 0 backup_client.namespaces(prefix: BACKUP_PREFIX).auto_paging_each do |ns| # Safety check: only delete namespaces that match our backup prefix raise "Refusing to delete namespace that doesn't match backup prefix: #{ns.id}" unless BACKUP_PREFIX.length > 0 && ns.id.start_with?(BACKUP_PREFIX) # Extract timestamp from backup namespace name (e.g., "backup-prod-users-1234567890") next unless ns.id.length >= 10 begin backup_time = Integer(ns.id[-10..]) if backup_time < cutoff puts " Deleting: #{ns.id}" backup_client.namespace(ns.id).delete_all deleted += 1 end rescue ArgumentError warn " Skipping #{ns.id}: invalid timestamp format" end end elapsed = (Time.now - start_time).round(1) puts "Done: backed up #{namespace_count} namespaces, deleted #{deleted} old backups in #{elapsed}s" ``` See [Limits](/docs/limits) for copy throughput estimates. ## Recovering a Namespace Backup namespaces are fully functional. You can either point your application to the namespace in the backup region directly, or copy it to a new namespace in your preferred region as shown below: ```python # /// script # requires-python = ">=3.10" # dependencies = ["turbopuffer"] # /// import os import time import turbopuffer # Configuration SOURCE_REGION = "gcp-us-central1" BACKUP_REGION = "gcp-us-west1" BACKUP_PREFIX = "backup-" source_client = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), region=SOURCE_REGION ) backup_client = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), region=BACKUP_REGION ) # Find latest backup timestamp (last 10 chars = Unix epoch seconds) backups = list(backup_client.namespaces(prefix=BACKUP_PREFIX)) timestamps: set[int] = set() for ns in backups: if len(ns.id) >= 10: try: timestamps.add(int(ns.id[-10:])) except ValueError: pass latest = max(timestamps) print(f"Recovering from backup: {latest}") start_time = time.time() recovered = 0 latest_suffix = f"{latest:010d}" for ns in backups: if not ns.id.endswith(latest_suffix): continue original_name = ns.id[len(BACKUP_PREFIX) : -11] # -11 for "-" + 10 digits recovered_name = f"recovered-py-{original_name}" print(f" {ns.id} -> {recovered_name}") source_client.namespace(recovered_name).copy_from( source_namespace=ns.id, source_region=BACKUP_REGION ) recovered += 1 print(f"Done: recovered {recovered} namespaces in {time.time() - start_time:.1f}s") ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; // Configuration const SOURCE_REGION = "gcp-us-central1"; const BACKUP_REGION = "gcp-us-west1"; const BACKUP_PREFIX = "backup-"; const sourceClient = new Turbopuffer({ region: SOURCE_REGION }); const backupClient = new Turbopuffer({ region: BACKUP_REGION }); // Find latest backup timestamp (last 10 chars = Unix epoch seconds) const backups: { id: string }[] = []; for await (const ns of backupClient.namespaces({ prefix: BACKUP_PREFIX })) backups.push(ns); const timestamps = backups .map((ns) => (ns.id.length >= 10 ? parseInt(ns.id.slice(-10), 10) : NaN)) .filter((t) => !isNaN(t)); const latest = Math.max(...timestamps); console.log(`Recovering from backup: ${latest}`); const startTime = Date.now(); let recovered = 0; const latestSuffix = String(latest).padStart(10, "0"); for (const ns of backups) { if (!ns.id.endsWith(latestSuffix)) continue; const originalName = ns.id.slice(BACKUP_PREFIX.length, -11); // -11 for "-" + 10 digits const recoveredName = `recovered-mts-${originalName}`; console.log(` ${ns.id} -> ${recoveredName}`); await sourceClient.namespace(recoveredName).copyFrom({ source_namespace: ns.id, source_region: BACKUP_REGION, }); recovered++; } console.log(`Done: recovered ${recovered} namespaces in ${((Date.now() - startTime) / 1000).toFixed(1)}s`); ``` ```go package main import ( "context" "fmt" "os" "sort" "strconv" "time" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) const ( SourceRegion = "gcp-us-central1" BackupRegion = "gcp-us-west1" BackupPrefix = "backup-" ) func main() { ctx := context.Background() sourceClient := turbopuffer.NewClient(option.WithRegion(SourceRegion)) backupClient := turbopuffer.NewClient(option.WithRegion(BackupRegion)) // Find latest backup timestamp (last 10 chars = Unix epoch seconds) var backups []string timestamps := make(map[int64]bool) for iter := backupClient.NamespacesAutoPaging(ctx, turbopuffer.NamespacesParams{Prefix: turbopuffer.String(BackupPrefix)}); iter.Next(); { id := iter.Current().ID backups = append(backups, id) if len(id) >= 10 { if ts, err := strconv.ParseInt(id[len(id)-10:], 10, 64); err == nil { timestamps[ts] = true } } } var tsList []int64 for ts := range timestamps { tsList = append(tsList, ts) } sort.Slice(tsList, func(i, j int) bool { return tsList[i] < tsList[j] }) latest := tsList[len(tsList)-1] fmt.Printf("Recovering from backup: %d\n", latest) startTime := time.Now() recovered := 0 latestSuffix := fmt.Sprintf("%010d", latest) for _, nsID := range backups { if len(nsID) < 10 || nsID[len(nsID)-10:] != latestSuffix { continue } originalName := nsID[len(BackupPrefix) : len(nsID)-11] // -11 for "-" + 10 digits recoveredName := "recovered-go-" + originalName + "" fmt.Printf(" %s -> %s\n", nsID, recoveredName) targetNs := sourceClient.Namespace(recoveredName) if _, err := (&targetNs).CopyFrom(ctx, turbopuffer.NamespaceCopyFromParams{ SourceNamespace: nsID, SourceRegion: turbopuffer.String(BackupRegion), }); err != nil { panic(err) } recovered++ } fmt.Printf("Done: recovered %d namespaces in %.1fs\n", recovered, time.Since(startTime).Seconds()) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class RecoverNamespace { // Configuration static final String SOURCE_REGION = "gcp-us-central1"; static final String BACKUP_REGION = "gcp-us-west1"; static final String BACKUP_PREFIX = "backup-"; public static void main(String[] args) { var baseClient = TurbopufferOkHttpClient.builder().fromEnv(); var sourceClient = baseClient.region(SOURCE_REGION).build(); var backupClient = baseClient.region(BACKUP_REGION).build(); // Find the latest backup timestamp (last 10 chars = Unix epoch seconds) var backupTimestamps = new TreeSet(); for (var ns : backupClient .namespaces(ClientNamespacesParams.builder().prefix(BACKUP_PREFIX).build()) .autoPager()) { var name = ns.id(); if (name.length() >= 10) { try { backupTimestamps.add(Long.parseLong(name.substring(name.length() - 10))); } catch (NumberFormatException e) { // Skip invalid timestamps } } } var latestTimestamp = backupTimestamps.last(); var latestSuffix = String.format("%010d", latestTimestamp); System.out.println("Recovering from backup timestamp: " + latestTimestamp); var startTime = System.currentTimeMillis(); int recovered = 0; for (var ns : backupClient .namespaces(ClientNamespacesParams.builder().prefix(BACKUP_PREFIX).build()) .autoPager()) { if (!ns.id().endsWith(latestSuffix)) continue; var originalName = ns.id().substring(BACKUP_PREFIX.length(), ns.id().length() - 11); // -11 for "-" + 10 digits var recoveredName = "recovered-java-" + originalName + ""; System.out.println(" " + ns.id() + " -> " + recoveredName); sourceClient .namespace(recoveredName) .copyFrom( NamespaceCopyFromParams.builder() .sourceNamespace(ns.id()) .sourceRegion(BACKUP_REGION) .build() ); recovered++; } var elapsed = (System.currentTimeMillis() - startTime) / 1000.0; System.out.printf("Done: recovered %d namespaces in %.1fs%n", recovered, elapsed); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models; using Turbopuffer.Models.Namespaces; // Configuration const string SOURCE_REGION = "gcp-us-central1"; const string BACKUP_REGION = "gcp-us-west1"; const string BACKUP_PREFIX = "backup-"; using var sourceClient = new TurbopufferClient { Region = SOURCE_REGION }; using var backupClient = new TurbopufferClient { Region = BACKUP_REGION }; // Find the latest backup timestamp (last 10 chars = Unix epoch seconds) var backupTimestamps = new SortedSet(); var backupsForTimestamps = await backupClient.Namespaces( new ClientNamespacesParams { Prefix = BACKUP_PREFIX } ); await foreach (var ns in backupsForTimestamps.Paginate()) { var name = ns.ID; if (name.Length >= 10) { if (long.TryParse(name.Substring(name.Length - 10), out var ts)) { backupTimestamps.Add(ts); } // Skip invalid timestamps } } var latestTimestamp = backupTimestamps.Max; var latestSuffix = $"{latestTimestamp:D10}"; Console.WriteLine($"Recovering from backup timestamp: {latestTimestamp}"); var startTime = DateTimeOffset.UtcNow; int recovered = 0; var backups = await backupClient.Namespaces( new ClientNamespacesParams { Prefix = BACKUP_PREFIX } ); await foreach (var ns in backups.Paginate()) { if (!ns.ID.EndsWith(latestSuffix)) continue; var originalName = ns.ID.Substring( BACKUP_PREFIX.Length, ns.ID.Length - 11 - BACKUP_PREFIX.Length ); // -11 for "-" + 10 digits var recoveredName = "recovered-csharp-" + originalName + ""; Console.WriteLine($" {ns.ID} -> {recoveredName}"); await sourceClient .Namespace(recoveredName) .CopyFrom( new NamespaceCopyFromParams { SourceNamespace = ns.ID, SourceRegion = BACKUP_REGION, } ); recovered++; } var elapsed = (DateTimeOffset.UtcNow - startTime).TotalSeconds; Console.WriteLine($"Done: recovered {recovered} namespaces in {elapsed:F1}s"); ``` ```ruby require "turbopuffer" # Configuration SOURCE_REGION = "gcp-us-central1" BACKUP_REGION = "gcp-us-west1" BACKUP_PREFIX = "backup-" source_client = Turbopuffer::Client.new(region: SOURCE_REGION) backup_client = Turbopuffer::Client.new(region: BACKUP_REGION) # Find the latest backup timestamp (last 10 chars = Unix epoch seconds) backup_timestamps = Set.new backup_client.namespaces(prefix: BACKUP_PREFIX).auto_paging_each do |ns| next unless ns.id.length >= 10 begin backup_timestamps.add(Integer(ns.id[-10..])) rescue ArgumentError # Skip invalid timestamps end end latest_timestamp = backup_timestamps.max puts "Recovering from backup timestamp: #{latest_timestamp}" start_time = Time.now recovered = 0 latest_suffix = format("%010d", latest_timestamp) backup_client.namespaces(prefix: BACKUP_PREFIX).auto_paging_each do |ns| next unless ns.id.end_with?(latest_suffix) original_name = ns.id[BACKUP_PREFIX.length..-12] # -12 for "-" + 10 digits + 1 (ruby range is inclusive) recovered_name = "recovered-rb-#{original_name}" puts " #{ns.id} -> #{recovered_name}" source_client.namespace(recovered_name).copy_from( source_namespace: ns.id, source_region: BACKUP_REGION, ) recovered += 1 end puts "Done: recovered #{recovered} namespaces in #{(Time.now - start_time).round(1)}s" ``` For more details on `copy_from_namespace`, see the [write documentation](/docs/write#param-copy_from_namespace). --- This page: [/docs/backups.md](https://turbopuffer.com/docs/backups.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Namespace Branching **Branch Latency** (constant-time regardless of namespace size) - p50: 440ms - p90: 557ms - p99: 1034ms Branching creates an instant, copy-on-write clone of a namespace via `branch_from`. - **Constant-time** regardless of namespace size - **Fully independent** — reads, writes, queries, and deletes on either namespace don't affect the other - **Branch from branches** — multi-level workflows like per-developer branches from staging - **Unlimited** — no limit on child branches per namespace (A→B, A→C, A→D, ...) nor on length of branch chains (A→B, B→C, C→D, ...) ``` ┏━━━━━━━━━┓ ┏━━━━┓ ┃namespace┃ ┃data┃ ┗━━━━━━━━━┛ ┗━━━━┛ ┌──────┐ ┌────────────┐ │source│─────────▶│source/1.bin│ └──────┘ ▲ └────────────┘ │ │ │ ┌──────┐ │ ┌────────────┐ │branch│─────┴───▶│branch/1.bin│ └──────┘ └────────────┘ ``` ## Pricing Branching is billed at a flat rate of $0.032. Storage of a branched namespace is billed on logical bytes at standard rates — each branch is billed as if it were an independent namespace. We plan to reduce this once we've observed branching in production and learned what the reuse rates are. ## When to use branching vs copy_from_namespace Use [`copy_from_namespace`](/docs/write#param-copy_from_namespace) when you need a backup with full data isolation (branching shares underlying storage), when copying across regions or organizations (see the [cross-region backups guide](/docs/backups)), or when re-encrypting a namespace with a different [CMEK key](/docs/encryption). Use branching otherwise. ## Use cases - **Codebase indexing.** Embed a codebase once; branch per local checkout so only changed files need re-indexing. - **Test pipelines.** Branch a production namespace, run tests against real data, tear it down when done. - **Development environments.** Give each developer a sandbox of a shared dataset. - **Snapshots.** Capture the state of a changing namespace at a point in time, query the immutable snapshot many times, discard when finished. ## Deleting branches Both the source and branched namespaces are fully independent after creation. Deleting a branch does not affect the source namespace, and deleting the source does not affect any branches. Use the standard [delete namespace](/docs/delete-namespace) endpoint to remove either one. ## Example ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/branching-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d "{ \"branch_from_namespace\": \"branching-example-source-curl\" }" ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) source = tpuf.namespace(f'branching-example-source-py') source.write(upsert_rows=[{'id': 1, 'title': 'Hello'}, {'id': 2, 'title': 'World'}]) ns = tpuf.namespace(f'branching-example-py') ns.branch_from(source_namespace=f'branching-example-source-py') # Write to the branch ns.write(upsert_rows=[{'id': 3, 'title': 'New'}]) # Branch has source data + new write result = ns.query(rank_by=('id', 'asc'), top_k=10, include_attributes=['title']) print(result.rows) # Source is unaffected result = source.query(rank_by=('id', 'asc'), top_k=10, include_attributes=['title']) print(result.rows) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const source = tpuf.namespace(`branching-example-source-ts`); await source.write({ upsert_rows: [{ id: 1, title: "Hello" }, { id: 2, title: "World" }] }); const ns = tpuf.namespace(`branching-example-ts`); await ns.branchFrom({ source_namespace: `branching-example-source-ts` }); // Write to the branch await ns.write({ upsert_rows: [{ id: 3, title: "New" }] }); // Branch has source data + new write const branchResult = await ns.query({ rank_by: ["id", "asc"], top_k: 10, include_attributes: ["title"] }); console.log(branchResult.rows); // Source is unaffected const sourceResult = await source.query({ rank_by: ["id", "asc"], top_k: 10, include_attributes: ["title"] }); console.log(sourceResult.rows); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) source := tpuf.Namespace("branching-example-source-go") _, err := source.Write(ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{{"id": 1, "title": "Hello"}, {"id": 2, "title": "World"}}, }) if err != nil { panic(err) } ns := tpuf.Namespace("branching-example-go") _, err = ns.BranchFrom(ctx, turbopuffer.NamespaceBranchFromParams{ SourceNamespace: "branching-example-source-go", }) if err != nil { panic(err) } // Write to the branch _, err = ns.Write(ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{{"id": 3, "title": "New"}}, }) if err != nil { panic(err) } // Branch has source data + new write branchResult, err := ns.Query(ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAttribute("id", turbopuffer.RankByAttributeOrderAsc), TopK: turbopuffer.Int(10), IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"title"}, }, }) if err != nil { panic(err) } fmt.Println(turbopuffer.PrettyPrint(branchResult.Rows)) // Source is unaffected sourceResult, err := source.Query(ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAttribute("id", turbopuffer.RankByAttributeOrderAsc), TopK: turbopuffer.Int(10), IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"title"}, }, }) if err != nil { panic(err) } fmt.Println(turbopuffer.PrettyPrint(sourceResult.Rows)) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class Branch { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var source = tpuf.namespace("branching-example-source-java"); source.write( NamespaceWriteParams.builder() .upsertRows( List.of( Row.builder().put("id", 1).put("title", "Hello").build(), Row.builder().put("id", 2).put("title", "World").build() ) ) .build() ); var ns = tpuf.namespace("branching-example-java"); ns.branchFrom( NamespaceBranchFromParams.builder() .sourceNamespace("branching-example-source-java") .build() ); // Write to the branch ns.write( NamespaceWriteParams.builder() .upsertRows(List.of(Row.builder().put("id", 3).put("title", "New").build())) .build() ); // Branch has source data + new write var branchResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.attribute("id", RankByAttributeOrder.ASC)) .topK(10) .includeAttributes(List.of("title")) .build() ); System.out.println(branchResult.rows()); // Source is unaffected var sourceResult = source.query( NamespaceQueryParams.builder() .rankBy(RankBy.attribute("id", RankByAttributeOrder.ASC)) .topK(10) .includeAttributes(List.of("title")) .build() ); System.out.println(sourceResult.rows()); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var source = tpuf.Namespace("branching-example-source-csharp"); await source.Write( new NamespaceWriteParams { UpsertRows = [ new Row().Set("id", 1).Set("title", "Hello"), new Row().Set("id", 2).Set("title", "World"), ], } ); var ns = tpuf.Namespace("branching-example-csharp"); await ns.BranchFrom( new NamespaceBranchFromParams { SourceNamespace = "branching-example-source-csharp", } ); // Write to the branch await ns.Write( new NamespaceWriteParams { UpsertRows = [new Row().Set("id", 3).Set("title", "New")], } ); // Branch has source data + new write var branchResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Attribute("id", RankByAttributeOrder.ASC), Limit = 10, IncludeAttributes = new List { "title" }, } ); foreach (var row in branchResult.GetRows()) { Console.WriteLine(row); } // Source is unaffected var sourceResult = await source.Query( new NamespaceQueryParams { RankBy = RankBy.Attribute("id", RankByAttributeOrder.ASC), Limit = 10, IncludeAttributes = new List { "title" }, } ); foreach (var row in sourceResult.GetRows()) { Console.WriteLine(row); } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) source = tpuf.namespace("branching-example-source-rb") source.write(upsert_rows: [{ id: 1, title: "Hello" }, { id: 2, title: "World" }]) ns = tpuf.namespace("branching-example-rb") ns.branch_from(source_namespace: "branching-example-source-rb") # Write to the branch ns.write(upsert_rows: [{ id: 3, title: "New" }]) # Branch has source data + new write branch_result = ns.query(rank_by: ["id", "asc"], top_k: 10, include_attributes: ["title"]) puts branch_result.rows # Source is unaffected source_result = source.query(rank_by: ["id", "asc"], top_k: 10, include_attributes: ["title"]) puts source_result.rows ``` --- This page: [/docs/branching.md](https://turbopuffer.com/docs/branching.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Configuration turbopuffer is configurable by modifying a Kubernetes ConfigMap in the `turbopuffer` namespace of your deployment. The turbopuffer team works with you to manage your deployment, e.g. propose ConfigMap changes to your cluster, e.g. tuning cache sizes, LSM settings, or recall. To update the ConfigMap, you can use the Helm chart with the `values.yaml` you maintain for the cluster: Change `values.yaml` in the `byoc-kit` directory and run the following: #### GCP ```bash helm upgrade -n default turbopuffer \ oci://us-central1-docker.pkg.dev/turbopuffer-onprem/charts/tpuf \ --values=values.yaml \ --values=values.secret.yaml \ --values=metrics-keys.yaml ``` You may need to log in to the helm registry first: ```bash helm registry login us-central1-docker.pkg.dev ``` #### AWS ```bash helm upgrade -n default turbopuffer \ oci://961341552108.dkr.ecr.us-west-2.amazonaws.com/turbopuffer/turbopuffer/charts/tpuf \ --values=values.yaml \ --values=values.secret.yaml \ --values=metrics-keys.yaml ``` You may need to log in first: ```bash aws ecr get-login-password --region us-west-2 | \ docker login --username AWS --password-stdin 961341552108.dkr.ecr.us-west-2.amazonaws.com ``` #### Azure ```bash helm upgrade -n default turbopuffer \ oci://turbopuffer.azurecr.io/turbopuffer/charts/tpuf \ --values=values.yaml \ --values=values.secret.yaml \ --values=metrics-keys.yaml ``` You may need to log in first: ```bash az login --tenant 398cc17e-41b3-44de-929a-dc4048da9592 az acr login --name turbopuffer ``` ## cloud specific configurations The following configuration settings are under `kubernetes`: #### AWS **kubernetes.ec2_preferred_zone** object The preferred availability zone to be used for query and index nodes. **Note**: Setting this to multiple availability zones will incur an additional networking charge from AWS. Example: ```yaml kubernetes: ec2_preferred_zone: query: "us-west-2a" index: "us-west-2a" ``` --- **kubernetes.ec2_instance_tags** object Tags to be added to the EC2 instances in the cluster. Example: ```yaml kubernetes: ec2_instance_tags: my-tag: my-value ``` --- **kubernetes.nodepool_labels** object Additional labels to be applied to the node pools. Example: ```yaml kubernetes: nodepool_labels: yourcorp-billing-code: xyz ``` #### Azure **kubernetes.azure_preferred_zone** object The preferred availability zone to be used for query and index nodes. Example: ```yaml kubernetes: azure_preferred_zone: query: "1" index: "1" ``` --- **kubernetes.azure_os_disk_size_gb** object Optional per-pool override for the OS disk size (in GiB) on Azure AKSNodeClass. Set each value larger than the local NVMe capacity of that node type to force Azure to provision a managed OS disk instead of consuming the local NVMe. Maintenance and default nodes are not affected. Example: ```yaml kubernetes: azure_os_disk_size_gb: index: 881 query: 950 ``` --- **kubernetes.nodepool_labels** object Additional labels to be applied to the node pools. Example: ```yaml kubernetes: nodepool_labels: yourcorp-billing-code: xyz ``` #### GCP **kubernetes.preferred_zone** object The preferred availability zone to be used for query and index nodes. Example: ```yaml kubernetes: preferred_zone: query: "us-central1-a" index: "us-central1-a" ``` --- **kubernetes.nodepool_labels** object Additional labels to be applied to the node pools. Example: ```yaml kubernetes: nodepool_labels: yourcorp-billing-code: xyz ``` The following are configurations settings under `ingress`: **internal** boolean If true, the turbopuffer ingress will be exposed on an internal IP. Example: ```yaml ingress: internal: false ``` --- **ingress.certificates.mode** string Configures how certificates will be handled in the cluster. * `manual` - if using, you must also set `manual.secretName:` to the name of the secret containing the TLS cert * `disabled` - needed if using Google Managed Certificates or if you wish to not use TLS * `letsencrypt` * `aws` - use an AWS managed certificate. You must also set `aws.certificate_arn:` Example: ```yaml ingress: certificates: mode: 'letsencrypt' ``` ## turbopuffer specific settings **authentication.allowed_api_keys_sha256** object A mapping of org ids to API keys. Each API key is expected by be a 44 character base 64 encoded SHA-256 key. See [Org Configuration](/docs/byoc/operations#securely-partitioning-your-data) for details on generating org ids and API keys. **Note**: Currently all BYOC keys are generated as _admin_ keys for their organization. To partition your data securely we recommend [creating multiple organizations](/docs/byoc/operations#adding-additional-organizations). Example: ```yaml tpuf_config: authentication: allowed_api_keys_sha256: "5x8olkguh1l2jvtjrpgnvlcm": # Org id (24 chars, lowercase alphanumeric) - "IaG0JUcIiCXKwqhIWH8Qr0incF2xsbRZRRJJxznl0GM=" # base64(sha256("tpuf_...")) ``` --- **remote_settings_overrides** boolean When `true`, the service loads an additional settings overlay from your blob bucket (S3/GCS) at the path **`system/settings/default.yaml`**. This lets you change settings (e.g. add orgs or API keys) by updating that file in object storage without redeploying the ConfigMap. The file is re-fetched on an interval controlled by **`dynamic_poll_remote_ms`** (milliseconds). Only settings that are "dynamic" (e.g. `authentication.allowed_api_keys_sha256`) are applied on each reload without a restart. Example: ```yaml tpuf_config: remote_settings_overrides: true dynamic_poll_remote_ms: 300000 # optional; re-fetch remote overlay every 5 minutes ``` --- **fairness.query_concurrency_per_namespace** number Maximum concurrent queries to a single namespace allowed. This protects the node against a single namespace being overloaded. 429s will be returned from queries if there is not enough capacity to handle them. Example: ```yaml tpuf_config: fairness: query_concurrency_per_namespace: 16 # default ``` --- **fairness.query_bulkhead_wait_ms** number Maximum milliseconds to wait if the query concurrency limit is reached. Example: ```yaml tpuf_config: fairness: query_bulkhead_wait_ms: 800 # default ``` --- **search.max_topk** number Maximum number of documents that can be requested in a single query via the `limit.total` parameter. Example: ```yaml tpuf_config: search: max_topk: 10000 # default ``` --- **cache.prewarm.keep_warm_orgs** object A set of org_ids to keep warm in cache. On node startup, machines will prewarm namespaces for these orgs to ensure their cache is hot. Not recommended for most users. Example: ```yaml tpuf_config: cache: prewarm: keep_warm_orgs: - '' - '' ``` --- **cache.disk_budget_bytes** number The absolute number of bytes or percentage of local SSD capacity to use as a cache. Not recommended changing for most users. Example: ```yaml tpuf_config: cache: disk_budget_bytes: 0.95 # default, leaves headroom for the filesystem ``` --- **indexing.cache_fill_concurrency** number Number of cache fills to allow concurrently in the background per node. These are fired after a a cold query. We prioritize cache fills for more important files (i.e. to get faster queries sooner), e.g. centroids. Example: ```yaml tpuf_config: indexing: cache_fill_concurrency: 2 # default ``` --- **indexing.reindex_unindexed_bytes_max** number The maximum number of unindexed bytes allowed in the WAL before a reindex is triggered. Example: ```yaml tpuf_config: indexing: reindex_unindexed_bytes_max: 64000000 # default ``` --- **indexing.reindex_unindexed_rows_max** number The maximum number of rows we'll allow to remain unindexed. If the namespace has at least this many unindexed rows, a /index call will always trigger an index operation. Example: ```yaml tpuf_config: indexing: reindex_unindexed_rows_max: 50000 # default ``` --- **indexing.reindex_unindexed_wal_entries** number The maximum number of unindexed WAL entries allowed before a reindex is triggered. Example: ```yaml tpuf_config: indexing: reindex_unindexed_wal_entries: 512 # default ``` --- **indexing.batch_size_bytes** number During indexing, the number of document bytes to process at a given time before flushing. An indexing run can be composed of multiple batches, where we flush our progress incrementally after each bach. Example: ```yaml tpuf_config: indexing: batch_size_bytes: 1250000000 # 1.25 GB, default ``` --- **ingress.read_replicas** object Configures additional query replicas for specific (org, namespace) pairs. By default a namespace is served by a single query replica. Configuring read replicas spreads query load across more nodes, which is useful for read-heavy or high-QPS namespaces. The `namespace` field supports glob wildcards (`*`, `?`). The first matching entry wins if multiple patterns match the same namespace. Example: ```yaml tpuf_config: ingress: read_replicas: # Spread load for a high-QPS namespace across 4 replicas. - org_id: "5x8olkguh1l2jvtjrpgnvlcm" namespace: "high-qps-namespace" count: 4 # Glob patterns match many namespaces with a single entry. - org_id: "5x8olkguh1l2jvtjrpgnvlcm" namespace: "prod.shard-*" count: 2 ``` --- **tracing.otlp_endpoint** string The OTLP endpoint to emit traces to, if any. Should end with `/v1/traces`. If empty, traces won't be emitted. Example: ```yaml tpuf_config: tracing: otlp_endpoint: "http://localhost:4318/v1/traces" ``` --- **stats_export** object A statsd endpoint to emit metrics to. If present, all three subfields are required. Example: ```yaml tpuf_config: stats_export: prefix: "foocorp.turbopuffer" # do not include trailing dot host: "foocorp-statsd" port: 8125 ``` --- **blob.max_concurrent_requests** number The maximum number of concurrent requests in flight to object storage at one given time. Example: ```yaml tpuf_config: blob: max_concurrent_requests: 4000 # default ``` --- **storage.lsm_ttl_seconds** number The amount of time data can live in the LSM tree before being force-compacted. This setting serves two purposes: - Compaction speeds up queries. By compacting more frequently, queries will be more efficient. - For compliance, i.e. if a customer requires that deletes (via the API) are properly deleted within X days, setting this to a value < X days will ensure that the index doesn't still contain any residual data from the deleted documents. Example: ```yaml tpuf_config: storage: lsm_ttl_seconds: 1728000 # 20 days, default ``` --- This page: [/docs/byoc/configuration.md](https://turbopuffer.com/docs/byoc/configuration.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Control Plane turbopuffer BYOC includes our control plane agent which we can use to apply adjustments to your cluster. You may also choose to [manually approve](#configuring-manual-approval-for-your-cluster) certain operations, though doing so will limit our team's ability to manage your cluster and respond to incidents. See [our documentation](/docs/byoc/#control-plane) on the operational model of our control plane agent. ## Currently supported operations The control plane agent is source-available upon request, and supports a limited amount of hardcoded operations to operate your cluster. The currently supported operations are listed below: - **Restart pods:** Allows us to rapidly restart and pods we see behaving problematically via telemetrics. - **Scale Pods:** Allows us to change the desired replica count for your index and query nodes. - **Trigger reindex:** Allows us to force a reindex of a namespace. - **Trigger compaction:** Triggers a compaction of the LSM for a given namespace, which can improve query performance. - **Trigger gc:** Trigger a manual run of our garbage collector, which will reduce object storage usage. - **Upgrade tpuf:** Updates the image digest for your turbopuffer deployment triggering a rollout for the new version of turbopuffer. We typically find our BYOC customers operate best allowing us to perform routine maintenance operations (restarting pods, triggering indexing/compaction/gc). These operations may need to be performed unpredictably to react to shifting workloads or recent upgrades to your turbopuffer deployment. ## Configuring manual approval for your cluster By default all control plane operations are automatically approved. If you'd like one or more operations to require manual approval instead, set `approvals` in your Helm `values.yaml`: ```yaml control_plane: additional_config: approvals: upgrade: auto # can set to `manual`, but weakly discouraged restart: auto # can set to `manual`, but strongly discouraged scale: auto # can set to `manual`, but strongly discouraged reindex: auto # can set to `manual`, but strongly discouraged compact: auto # can set to `manual`, but strongly discouraged gc: auto # can set to `manual`, but strongly discouraged ``` Any operation you omit keeps its default of `auto`. ## Restricting upgrades to a maintenance window By default, the control plane applies new turbopuffer versions as soon as they're available. If you'd rather upgrades land only during predictable off-hours, configure a `maintenance_window`. Only upgrades are gated by the window — routine operations (restarting pods, scaling, reindex, compaction, gc) still run whenever they're needed. Add the window to your Helm `values.yaml`: ```yaml control_plane: additional_config: maintenance_window: schedule: "0 2 * * *" # 5-field cron in UTC: opens every day at 02:00 duration: "8h" # how long the window stays open (default: "8h") ``` - `schedule` is a standard 5-field cron expression evaluated in UTC, and defines when each window opens. - `duration` is how long the window stays open after each trigger. It accepts compound values such as `4h`, `30m`, or `10h5m`, and defaults to `8h`. The example above keeps the window open every day from 02:00 to 10:00 UTC. When an upgrade becomes available outside the window, the control plane holds it rather than applying it immediately. The operation waits in the `AWAITING_MAINTENANCE_WINDOW` state — annotated with the next window's start and end — and is applied automatically the next time the window opens: ```text $ kubectl get turbopufferoperations -n turbopuffer --sort-by=.metadata.creationTimestamp NAME APPROVED STATE AGE DETAILS qjh4uts8557qxly3 true AWAITING_MAINTENANCE_WINDOW 3m Upgrade to 46bea73f769ed2e4... ``` The window doesn't apply to upgrades configured for [external execution](#configuring-external-execution-for-upgrades), since your own tooling controls when those roll out. If an upgrade also requires manual approval, the window applies after you approve it. ## Configuring external execution for upgrades For customers managing their Helm deployments through external tooling (e.g., ArgoCD, Terraform, or other GitOps/CI tools), the standard upgrade flow can conflict with your deployment process. By default, our Helm chart uses [the `lookup` function](https://helm.sh/docs/chart_template_guide/functions_and_pipelines/#using-the-lookup-function) to retain the current version across deployments. However, when your tooling doesn't support Helm lookups, any upgrades we perform through the control plane may be overwritten when your tooling re-applies the Helm chart. To solve this, you can configure `external_execution` mode for upgrades. In this mode, the turbopuffer control plane will not perform upgrades directly. Instead, when a new version is available, you'll receive an automated notification to your Slack channel with the version to deploy. You can then update the `turbopuffer.initial_tag` value in your Helm values and apply it through your deployment pipeline. To enable external execution for upgrades, you can set the following in your Helm `values.yaml`: ```yaml control_plane: additional_config: approvals: upgrade: external_execution turbopuffer: image: initial_tag: force_initial_tag: true ``` ### Retrieving the target version programmatically If you want to automate this process, we expose the target version via an API endpoint that you can query from your deployment tooling: ```bash curl -H "Authorization: Bearer " \ -H "Accept: application/json" \ "https://control.turbopuffer.com/api/clusters//target_version" ``` Replace `` with your cluster's API key and `` with your cluster identifier. The response includes both the git commit SHA and the image digest: ```json { "target_version": "", "target_digest": "sha256:" } ``` You can use this endpoint in your Terraform configurations, CI/CD pipelines, or any other automation to retrieve and apply the latest target version. #### Terraform example ```hcl data "http" "turbopuffer_target_version" { url = "https://control.turbopuffer.com/api/clusters/${var.cluster_id}/target_version" request_headers = { Accept = "application/json" Authorization = "Bearer ${var.turbopuffer_api_key}" } } locals { turbopuffer_version = jsondecode(data.http.turbopuffer_target_version.response_body) } # Use in your Helm release configuration: # local.turbopuffer_version.target_version -> git commit SHA # local.turbopuffer_version.target_digest -> image digest (sha256:...) ``` ## Viewing proposed operations in your cluster When turbopuffer proposes a modification to your cluster, the agent will create an associated Kubernetes resource for this operation: ```text $ kubectl get turbopufferoperations -n turbopuffer --sort-by=.metadata.creationTimestamp NAME APPROVED STATE AGE DETAILS qjh4uts8557qxly3 true SUCCESS 7m58s Upgrade to 46bea73f769ed2e4... jsilyy4wu8skamb9 true SUCCESS 7m57s Upgrade to c7ce48b6be870806... kqc3ltskmsx32nr7 true SUCCESS 7m46s Upgrade to 60b44ccd78eaf20c... pnblpzmh5gp5fwsy true SUCCESS 7m46s Upgrade to 32c171415362b200... 0a8n5w41jyj8i35v false REQUIRES_APPROVAL 7m46s Restart pods: turbopuffer-query-0* ``` You can approve an operation by editing the Kubernetes configuration, for example, we approve `0a8n5w41jyj8i35v` by running: ```text kubectl patch turbopufferoperation -n turbopuffer 0a8n5w41jyj8i35v --type=merge -p '{"spec": {"approved": true}}' ``` Custom Resource Definition transitions are generally logged by your cloud provider's Kubernetes cluster, so your standard audit logging tools should support turbopuffer's operations. --- This page: [/docs/byoc/control-plane.md](https://turbopuffer.com/docs/byoc/control-plane.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # BYOC Deployment Runlist For each cluster in your turbopuffer [BYOC](/docs/byoc) deployment you will be provided with a 'BYOC kit' containing all the files required to configure your cluster. This document provides guidance to successfully deploy a new turbopuffer BYOC cluster. ## Your kit contents ```txt byoc-kit ├── README.md ├── aws │ ├── main.tf │ └── turbopuffer.tfvars ├── cosign.pub ├── gcp │ ├── main.tf │ └── turbopuffer.tfvars ├── azure │ ├── main.tf │ └── turbopuffer.tfvars ├── scripts │ ├── generate_secrets.py │ └── sanity.sh ├── values.yaml (generated) # configuration file generated by terraform ├── values.secret.yaml (generated) # sensitive configuration file generated by terraform ├── compute_classes.yaml (generated, GCP only) # GKE ComputeClass manifest generated by terraform └── metrics-keys.yaml # configuration file provided by turbopuffer ``` ## Runlist 0. [ ] **Mise en place:** Check you have all prerequisites: - [ ] Verify you have both `terraform`, `kubectl` and `helm` installed. - [ ] Provision a **fresh sub-account / project** for your new cluster. - [ ] Enable pulling your image from our registries: **GCP:** Provide the **service account email** used for pulling images to the turbopuffer team. Either the default compute service account for the sub-account, or a custom service account e.g. used for replicating images into your own registry or [configured in K8s](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-multiple-service-accounts). **AWS:** Provide us with your **AWS account id**. **Azure:** Provide the **multi-tenant application (client) id** that [will be used for your azure clusters](https://learn.microsoft.com/en-us/azure/container-registry/authenticate-aks-cross-tenant#pull-images-from-a-container-registry-to-an-aks-cluster-in-a-different-microsoft-entra-tenant). 3. [ ] **Cluster configuration:** Apply terraform configuration to setup Kubernetes cluster and bucket - [ ] **GCP:** `cd gcp` **AWS:** `cd aws` **Azure:** `cd azure` - [ ] Run `terraform init` to setup required providers - [ ] Fill in the required values in `turbopuffer.tfvars`**GCP:** . The `query_skus`, `index_skus`, and `maintenance_machine_type` variables control which GCP machine types back the query, index, and maintenance node pools. The defaults work for most deployments. - [ ] Apply terraform configuration: `terraform apply -var-file=turbopuffer.tfvars` 4. [ ] **`kubectl`.** Add your new cluster context to kubectl. - [ ] Run **GCP:** `gcloud container clusters get-credentials CLUSTER_NAME --project PROJECT_ID --region REGION` **AWS:** `aws eks update-kubeconfig --region REGION --name CLUSTER_NAME` **Azure:** `az aks get-credentials --name=CLUSTER_NAME --resource-group=RESOURCE_GROUP` - [ ] Run `kubectl config get-contexts` and confirm the cluster is correct. - [ ] Run `kubectl get pods` and confirm the command succeeds (no output). - [ ] **GCP:** **Apply ComputeClass manifests:** Terraform writes a `compute_classes.yaml` at the kit root that defines GKE `ComputeClass` priorities across the query and index node pools. Apply it before installing Helm so the autoscaler has the priority order in place when the first pods schedule: `kubectl apply -f compute_classes.yaml` 5. [ ] **Configure Helm:** The terraform command will have output a `values.yaml` file in the `byoc-kit` directory, which contains values for Helm. Edit this file and set any other necessary values. Refer to `values.schema.json` for a description of valid configurations. - To use provider managed TLS certificates, see [using cloud provider managed TLS certificates](#using-cloud-provider-managed-tls-certificates). - `tpuf_config` configuration values suggested by turbopuffer for your BYOC deployment. You can find information about these settings and more in our [BYOC configuration](/docs/byoc/configuration) documentation. 6. [ ] **Generate API keys:** Run `./scripts/generate-secrets.py` to generate `values.secret.yaml`. This file will generate an Org Id and API key, along with an token for intra-cluster communication. 7. [ ] **Deploy turbopuffer:** - [ ] **Log in to Helm registry:** Run **GCP:** `helm registry login us-central1-docker.pkg.dev` **AWS:** `aws ecr get-login-password --region us-west-2 | helm registry login --username AWS --password-stdin 961341552108.dkr.ecr.us-west-2.amazonaws.com` **Azure:** `az login --tenant 398cc17e-41b3-44de-929a-dc4048da9592 && az acr login --name turbopuffer` - [ ] **Install the Helm chart:** Run **GCP:** `helm install -n default turbopuffer oci://us-central1-docker.pkg.dev/turbopuffer-onprem/charts/tpuf --values=values.yaml --values=values.secret.yaml --values=metrics-keys.yaml` **AWS:** `helm install -n default turbopuffer oci://961341552108.dkr.ecr.us-west-2.amazonaws.com/turbopuffer/turbopuffer/charts/tpuf --values=values.yaml --values=values.secret.yaml --values=metrics-keys.yaml` **Azure:** `helm install -n default turbopuffer oci://turbopuffer.azurecr.io/turbopuffer/charts/tpuf --values=values.yaml --values=values.secret.yaml --values=metrics-keys.yaml` - [ ] **For subsequent updates**, run **GCP:** `helm upgrade -n default turbopuffer oci://us-central1-docker.pkg.dev/turbopuffer-onprem/charts/tpuf --values=values.yaml --values=values.secret.yaml --values=metrics-keys.yaml` **AWS:** `helm upgrade -n default turbopuffer oci://961341552108.dkr.ecr.us-west-2.amazonaws.com/turbopuffer/turbopuffer/charts/tpuf --values=values.yaml --values=values.secret.yaml --values=metrics-keys.yaml` **Azure:** `helm upgrade -n default turbopuffer oci://turbopuffer.azurecr.io/turbopuffer/charts/tpuf --values=values.yaml --values=values.secret.yaml --values=metrics-keys.yaml` 8. [ ] Run post-deployment sanity checks - [ ] `TURBOPUFFER_API_KEY= scripts/sanity.sh` will query your turbopuffer cluster directly, verifying that core operations function. It will not verify certicates, and may encounter a 500 error if the nodes aren't routeable yet. ## Using a custom registry for your turbopuffer cluster By default turbopuffer will pull from one of several turbopuffer managed image registries, as configured in our included terraform. However, there are many reasons you may want to host our images in a registry you control. Our Helm chart fully supports this through the following settings: ```yaml image.registry: YOUR_REGISTRY_URL control_plane.image.registry: YOUR_REGISTRY_URL ``` We expect two registries found there one called `turbopuffer` and one called `tpuf-ctl-cluster` holding the images for turbopuffer and our control plane agent respectively. For customers on AWS, we can configure ECR Replication to automatically push the latest images into your registry. ## Using cloud provider managed TLS certificates Our helm chart allows managing TLS termination internally to your cluster using either `cert-manager` or the native kubernetes apis. Your organization may already be managing their certificates through your cloud providers' managed certificates offerings, in which case you will need to handle termination yourselves. Regardless of your cloud provider, you will want to deploy turbopuffer internally, by setting: ```yaml ingress.internal: true ``` ### GCP To get started set the following in `values.yaml` and re-run `helm upgrade ...` as described in step 7. ```yaml certicates.mode: disabled ``` Adding [Google Managed Certificates](https://cloud.google.com/kubernetes-engine/docs/how-to/managed-certs) to your GKE cluster is as simple as deploying the following Kubernetes manifest alongside your turbopuffer helm deployment. All that is required is to insert the correct value for `YOUR_DOMAIN`. ```yaml apiVersion: cloud.google.com/v1 kind: BackendConfig metadata: name: ingress-nginx-svc-config namespace: ingress-nginx spec: healthCheck: checkIntervalSec: 10 timeoutSec: 10 port: 80 type: HTTP requestPath: /healthz --- apiVersion: v1 kind: Service metadata: name: ingress-nginx-svc namespace: ingress-nginx annotations: cloud.google.com/backend-config: '{"default": "ingress-nginx-svc-config"}' spec: ports: - appProtocol: http name: http port: 80 protocol: TCP targetPort: http - appProtocol: https name: https port: 443 protocol: TCP targetPort: https selector: app.kubernetes.io/component: controller app.kubernetes.io/instance: ingress-nginx app.kubernetes.io/name: ingress-nginx type: ClusterIP --- apiVersion: networking.gke.io/v1 kind: ManagedCertificate metadata: name: managed-cert namespace: ingress-nginx spec: domains: - YOUR_DOMAIN --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ingress-nginx-ing namespace: ingress-nginx annotations: networking.gke.io/managed-certificates: managed-cert spec: ingressClassName: "gce" defaultBackend: service: name: ingress-nginx-svc port: number: 80 ``` ### AWS You will need to provision your certificate externally to your cluster using the AWS console or CLI. Then update your Helm `values.yaml` with the following configuration values and re-run `helm upgrade ...` as described in step 7. ```yaml ingress: certificates: mode: aws aws: certificate_arn: YOUR_CERTIFICATE_ARN ``` ## Networking If you want to disable outgoing connections for the cluster, you can allowlist the following IPs: + Polar Signals (CPU and Heap profiling) - `35.234.93.182` (`api.polarsignals.com`) + Control Plane (Cluster Heartbeats) - `76.76.21.0/24` + Datadog (Telemetry) - `curl -s https://ip-ranges.datadoghq.com/ | jq -r '(.apm.prefixes_ipv4 + .global.prefixes_ipv4 + .logs.prefixes_ipv4 + .agents.prefixes_ipv4) | unique[]'` ## Upgrading turbopuffer versions If you have manual approvals enabled, the turbopuffer team will provide you with a command to upgrade the cluster when a new version is available. Otherwise, upgrades will happen automatically through the control plane. --- This page: [/docs/byoc/deployment.md](https://turbopuffer.com/docs/byoc/deployment.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Common Operations While turbopuffer strives to be as low interaction as possible, there are certain manual operations you will have to perform in your BYOC deployment. ## Securely partitioning your data turbopuffer BYOC allows you to configure multiple organizations, each with their own set of API keys which you can use to scope data access. Currently, we only support creating admin API keys, that will apply to all namespaces in their organization. For this reason, if you need to ensure data is isolated we recommend creating multiple organizations instead. If this is a limitation, we recommend you contact us on Slack. ### Generating org IDs and API keys You can generate valid org ids and API keys using any tooling that produces cryptographically random values. The format requirements are: **Org id** - 24 character random string - Alphabet: `[a-z0-9]` (lowercase alphanumeric only) **API Key** - Prefix: `tpuf_` - Followed by: 32 character random string from alphabet `[a-zA-Z0-9]` - Full format: `tpuf_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` **Stored API Key Hash** - Format: `base64(sha256(full_api_key))` - The hash is computed over the complete API key including the `tpuf_` prefix Your BYOC kit includes a `generate_secrets.py` script that generates these values for you. ### Storing org configuration ```yaml authentication: allowed_api_keys_sha256: "your24charorgidhere1234": - "YourBase64EncodedSHA256HashHere+40CharactersTotal=" - "AnotherBase64EncodedSHA256HashHere+40CharactersTot=" ``` ### Adding additional organizations To add more organizations, generate new org id and API key pairs and add them to the configuration: ```yaml authentication: allowed_api_keys_sha256: "existingorgid123456789012": - "ExistingOrgKeyHash..." - "AnotherExistingOrgKeyHash..." "neworgid0987654321abcdef": - "NewOrgKeyHash..." - "AnotherNewOrgKeyHash..." ``` ### Adding a new API key to an existing organization Each organization can have multiple API keys for key rotation, different services, or other access patterns. To add a new API key, use the `api_key.py` script from your BYOC kit and append the generated hash to the organization's key list: ```yaml authentication: allowed_api_keys_sha256: "existingorgid123456789012": - "ExistingOrgKeyHash..." - "NewlyAddedKeyHash..." ``` ### Applying configuration changes After updating the configuration, apply the changes using the [helm upgrade command](/docs/byoc/configuration). --- This page: [/docs/byoc/operations.md](https://turbopuffer.com/docs/byoc/operations.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Requirements The following are the technical requirements for running turbopuffer in your cloud via BYOC. ## Instance Types The following are the instance types that turbopuffer uses by default. While CPU counts can be adjusted within these families, the specific instance type families listed below are required for optimal performance and compatibility. ### AWS - m7gd.\{32,48\}xlarge - m8gd.\{32,48\}xlarge - i8g.\{32,48\}xlarge - i7i.\{32,48\}xlarge - i8ge.\{32,64\}xlarge - i7ie.\{24,48\}xlarge ### GCP - c4a-highmem-\{32,48,64\}-lssd - c4a-standard-\{32,48\}-lssd - c4-standard-\{32,48\}-lssd - c4-highmem-\{32,48\}-lssd - c3-standard-\{44,88\}-lssd ### Azure - Standard_L\{32,48\}as_v4 - Standard_L\{32,48\}s_v4 - Standard_L\{32,48\}s_v3 - Standard_L\{32,48\}as_v3 - Standard_D\{32,48\}pds_v6 - Standard_D\{32,48\}plds_v6 - Standard_D\{32,48\}ds_v6 - Standard_D\{32,48\}lds_v6 ## Dedicated Kubernetes Cluster To guarantee the Service Level Agreements (SLAs) for your turbopuffer cluster, the turbopuffer cluster must run on its own dedicated Kubernetes cluster, isolated to prevent resource contention and ensure predictable performance. This Kubernetes cluster must reside within its own isolated member account in AWS, or its own isolated project in GCP. --- This page: [/docs/byoc/requirements.md](https://turbopuffer.com/docs/byoc/requirements.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # turbopuffer BYOC Deploy turbopuffer into your Kubernetes cluster on AWS, GCP, or Azure with turbopuffer BYOC (Bring Your Own Cloud). The turbopuffer team is on-call for your cluster, and help you operate it through our secure control plane without direct access to your VPC. Handle billions of vectors without worrying about operations. ``` ┌─Customer Account───────────────────────────┐ ┌─turbopuffer Account───┐ │ │ │ │ │┌──turbopuffer sub-account─────────────────┐│ │ ┌───────────────────┐ │ ││ ││ │ │ Telemetry │ │ ││ ┌──Kubernetes──────────────┐││ │ └───────────────────┘ │ ││ ┌─────────┐ │ ┌─────────┐ ┌─────────┐ │││ │ ┌───────────────────┐ │ ││ │ Bucket │ │ │ Compute │ │ Control │ │││ │ │ Usage │ │ ││ │ (AES) │──┼─│ │ │ Plane ├─┼┼──TLS──▶│ └───────────────────┘ │ ││ └─────────┘ │ └─────▲───┘ └─────────┘ │││ │ ┌───────────────────┐ │ ││ └───────┼──────────────────┘││ │ │ Dashboard │ │ │└──────────────────────┼───────────────────┘│ │ └───────────────────┘ │ │ │ │ │ ┌───────────────────┐ │ │ │ │ │ │ Control Plane │ │ │┌──Customer sub-account┴───────────────────┐│ │ └───────────────────┘ │ ││ ┌──────┐ ┌──────┐ ┌──────┐ ││ │ ┌───────────────────┐ │ ││ │App 1 │ │App 2 │ │App 3 │ ││ │ │ Container Images │ │ ││ └──────┘ └──────┘ └──────┘ ││ │ └───────────────────┘ │ │└──────────────────────────────────────────┘│ └───────────────────────┘ └────────────────────────────────────────────┘ ``` ## Security * Data is encrypted in transit with TLS1.2+, * Data is encrypted at rest with AES-256 in Google Cloud Storage, Amazon S3, or Azure Blob Storage. * Data is cached in memory and on NVMe SSD disks. SSD cache is encrypted with AES-256. * In AWS, SSD cache is implemented with instance stores, and the encryption key is ephemeral and destroyed when the VM shuts down. * No customer data leaves the customer's VPC * turbopuffer does not have access to the customer's VPC See more details in the [Security](/docs/security) page. ## Setup Customer receives an "BYOC kit" that holds Terraform and Kubernetes configuration files and provisions the VPC and Kubernetes cluster. See [Deployment](/docs/byoc/deployment). You can get a sense of the Terraform and Kubernetes configuration files with this [scrubbed example](https://gist.github.com/xldenis/46ce17c286ec64fe8a8e434c21b9132e). ## Control Plane The control plane has two components: 1. **Customer Component** runs inside the customer's cluster and executes operations. The customer can approve each audit logged operation. The customer component is source-available by request. 2. **Vendor Component** runs inside turbopuffer's VPC and allows the turbopuffer team to propose operations. The types of operations the control plane can perform are: * Upgrades of turbopuffer or the Kubernetes configuration * Change turbopuffer configuration (cache sizes, tuning, recall) * Trigger manual tasks (compaction, rebuild indexes, GC, consistency checks, cache flush) * Trigger horizontal or vertical scaling * Exposing additional debugging information (e.g. copy of metadata file without customer data from object storage) The control plane is not required for the data plane to operate, i.e. turbopuffer can accept writes and queries if the control plane is down. ## Operation Approval Model & SLAs turbopuffer will provide support under the terms of the Service Level Agreement in your Master Subscription Agreement (MSA). The turbopuffer team is on-call for your cluster. The turbopuffer team does not require access to your VPC. All operations are performed securely through the control plane with audit logs. You can choose two models for accepting operations: **Secure Operations (Push Model).** The turbopuffer on-call is allowed to execute audit logged operations through the control plane at any time. This means we can uphold higher SLAs as we don't require working with your oncall. **Human Approved Operations (Pull Model).** All operations must go through manual approval from the customer. The turbopuffer team requires access to the customer's on-call responsible for the turbopuffer cluster to grant access within SLAs. This access model affects pricing and the SLAs we can provide. Operations are implemented as Kubernetes Custom Resource Definitions (CRDs). This allows you to use your existing tools to manage audit logs of cluster operations. ## Upgrades turbopuffer can be upgraded, scaled up, or scaled down without downtime by the control plane. Any node can accept traffic for any namespace at any time. Therefore, zero downtime upgrades can be performed with a simple rolling restart. Container images can optionally be replicated into the customer sub-account. This allows the customer to enable extra security features such as enabling image tag immutability. All images are signed by `cosign` and can be verified by the customer. ## Network Isolation turbopuffer does not require any external incoming connections through the firewall. The customer can choose to disable all incoming connections, and access turbopuffer through their private VPC networking. turbopuffer does require outgoing connections for telemetry, usage data, and to receive commands from the control plane. By default, these connections are routed over the Internet. The external IPs can be allowlisted, see [Deployment](/docs/byoc/deployment#networking). ## Telemetry Telemetry is emitted to Datadog (traces, metrics, and logs) and [Polar Signals](https://www.polarsignals.com/) (CPU and heap profiling) directly from the customer's account for turbopuffer to monitor. No customer data is ever emitted in the telemetry. This allows continous monitoring by the turbopuffer team. Telemetry includes: - performance and infrastructure health metrics (no customer data) - request logs and error logs (no customer data) - performance tracing data (no customer data) - CPU and heap profiling (no customer data) The customer has access to a shared Datadog dashboard with Telemetry. ## Usage & Dashboard Aggregated usage metrics are reported to the turbopuffer usage database. These metrics are used for billing, and are displayed on the turbopuffer dashboard. ## Compliance turbopuffer maintains System and Organization Controls (SOC) 2 Type 2 compliance, with continuous auditing by an AICPA certified auditor. To receive a copy of our latest report, please contact us. For healthcare customers, turbopuffer can also provide a HIPAA compliant BAA. --- This page: [/docs/byoc.md](https://turbopuffer.com/docs/byoc.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Chunking Chunking is context engineering for your embedding model. It is a critical, and often under-appreciated, component of your retrieval pipeline. Your chunking strategy, good or bad, will set the ceiling for recall. This guide provides rules of thumb to tune your chunking strategy for the best possible recall. ## Not too long Even if your embedding model technically allows up to 32k token inputs, you won't get the best results that way, for two reasons: 1. **Attention**: The model can only effectively attend to so many tokens at once. Quality degrades before you hit the model's enforced maximum input length. 2. **Compression**: You're asking the embedding model to compress many tokens into one embedding. The longer the chunk, the lossier the compression. ## Not too short You need to provide enough context for the embedding model to understand the input as a standalone string. Most embedding models cannot see the full document when producing an embedding ([exception below!](#use-a-contextual-embedding-model)). If your chunks are too small, most of them will reference concepts, people, places, and things described elsewhere in the document. For example, consider these two chunks from a single document: ```text chunk 1: Dory recently moved from The Great Barrier Reef to a new home in Sydney Harbor. Her new address is 42 Wallaby Way. chunk 2: She got a good deal from P. Sherman. He sold her the house for only $200,000. ``` For the query `How much was Dory's new house?`, neither chunk contains the necessary context to fully answer. The answer is contained in chunk 2, but chunk 2 lacks the context that "she" means Dory and "the house" means 42 Wallaby Way. If the two chunks were combined into a single chunk, all the necessary information would be contained (at the expense of compression loss). ## Respect obvious chunk boundaries Some corpora have obvious chunking boundaries. Chunking code files, for example, should respect function boundaries. If you split a function definition down the middle, each chunk loses the information needed to interpret it correctly, so the embedding tends to represent a syntactically broken fragment rather than the semantics of the function. Regardless of whether you use a traditional or contextual embedding model, you should use tools like [tree-sitter](https://github.com/tree-sitter/tree-sitter) for code or markdown splitters to chunk documents with known boundaries. ## Start with ~300 token chunks with overlap If you are using a traditional embedding model, we recommend starting at **~300 token chunks with two-sentence overlap between chunks** for text documents. This balances the tradeoff between good context and good compression. From that starting point, iteratively tweak your chunk length and overlap to achieve the desired results. The Python code sample demonstrates using the `blingfire` sentence splitter to performantly split large documents (books) into chunks of configurable size and overlap. ```python import requests from livekit import blingfire from tokenizers import Tokenizer # TARGET_TOKENS is a soft floor: keep adding whole sentences until crossing it. TARGET_TOKENS = 300 # SENTENCE_OVERLAP carries context from the end of one chunk into the next. SENTENCE_OVERLAP = 2 # Use the same tokenizer as your embedding model so token counts match what # the model sees. Caches tokenizer files (not weights) on first use. TOKENIZER = Tokenizer.from_pretrained("Qwen/Qwen3-Embedding-4B") # Pride and Prejudice, Moby-Dick. GUTENBERG_BOOK_IDS = [1342, 2701] def fetch_book(book_id: int) -> str: # Download the book text from Project Gutenberg url = f"https://www.gutenberg.org/cache/epub/{book_id}/pg{book_id}.txt" response = requests.get(url, timeout=30) response.raise_for_status() return response.text def pack_sentences_into_chunks(text: str, spans: list[tuple[int, int]], token_counts: list[int]) -> list[str]: chunks: list[str] = [] start = 0 while start < len(spans): end = start + 1 tokens = token_counts[start] # Keep at least SENTENCE_OVERLAP + 1 sentences per chunk so the next # chunk's start advances, then grow until we cross the token target. while end < len(spans) and ( end - start <= SENTENCE_OVERLAP or tokens < TARGET_TOKENS ): tokens += token_counts[end] end += 1 chunks.append(text[spans[start][0] : spans[end - 1][1]]) if end == len(spans): break start = end - SENTENCE_OVERLAP return chunks def chunk_documents(texts: list[str]) -> list[list[str]]: doc_spans: list[list[tuple[int, int]]] = [] for text in texts: # BlingFire removes whitespace between sentences. Use offsets to build # chunks from the untouched original text. _, spans = blingfire.text_to_sentences_with_offsets(text) doc_spans.append(spans) # One encode_batch over all sentences is much faster than per-document calls. flat_sentences = [text[s:e] for text, spans in zip(texts, doc_spans) for s, e in spans] encoded = TOKENIZER.encode_batch(flat_sentences, add_special_tokens=False) flat_token_counts = [len(e.ids) for e in encoded] chunks_per_doc: list[list[str]] = [] offset = 0 for text, spans in zip(texts, doc_spans): sentence_count = len(spans) token_counts = flat_token_counts[offset : offset + sentence_count] chunks_per_doc.append(pack_sentences_into_chunks(text, spans, token_counts)) offset += sentence_count assert offset == len(flat_token_counts) return chunks_per_doc def chunk_corpus() -> list[list[str]]: texts = [fetch_book(book_id) for book_id in GUTENBERG_BOOK_IDS] return chunk_documents(texts) if __name__ == "__main__": chunks_per_doc = chunk_corpus() chunk_count = sum(len(chunks) for chunks in chunks_per_doc) print(f"{chunk_count} chunks across {len(chunks_per_doc)} documents") ``` ## Use a contextual embedding model Contextual embedding models such as `voyage-context-3` or `pplx-embed-context-v1-{0.6b, 4b}` provide a practical means of improving recall without significantly adding cost, latency, or complexity to your embedding pipeline. With these models, you pass the full document to the model as a list of arbitrary-length text chunks. The model attends to the _entire_ document and produces contextualized embeddings for each chunk in one forward pass. Consider again the example: ```text chunk 1: Dory recently moved from The Great Barrier Reef to a new home in Sydney Harbor. Her new address is 42 Wallaby Way. chunk 2: She got a good deal from P. Sherman. He sold her the house for only $200,000. ``` A contextual embedding model can understand from chunk 1 that "she" in chunk 2 refers to Dory and "the house" in chunk 2 refers to 42 Wallaby Way. Thus, for the query `How much was Dory's new house?`, we would likely retrieve the embedding for chunk 2. Further, contextual embedding models make it possible to create smaller chunks without losing context. We could split the example into 4 chunks, one per sentence: ```text chunk 1: Dory recently moved from The Great Barrier Reef to a new home in Sydney Harbor. chunk 2: Her new address is 42 Wallaby Way. chunk 3: She got a good deal from P. Sherman. chunk 4: He sold her the house for only $200,000. ``` We would retrieve chunk 4 to answer the query, as its embedding includes the context of the other chunks. ### Tradeoffs of contextual embedding models Contextual embedding models make it possible to create smaller chunks that still retain the semantic context of the entire document, but they are not a silver bullet, and the tradeoffs should be considered: 1. **Storage costs**: Smaller chunks will result in more embeddings, and thus higher storage costs. 2. **Few models**: There aren't many contextual embedding models currently available, and their performance may vary considerably. 3. **Reduced benefit for long documents**: Longer documents stress the model's attention, making it harder for the model to decide which context should influence the individual chunk embeddings. For documents that approach the model's context limit, the benefits of contextual embedding may become negligible. In this case, consider truncation or a sliding window to break up the document. You may also consider using an LLM to append contextualized prefixes to each chunk (what Anthropic calls [contextual retrieval](https://www.anthropic.com/engineering/contextual-retrieval)), though this adds significant inference cost and embedding latency. --- This page: [/docs/chunking.md](https://turbopuffer.com/docs/chunking.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Concepts ## Approximate Nearest Neighbor (ANN) Most vector search applications benefit from trading off a small amount of search accuracy (recall) for a large gain in performance. turbopuffer uses [SPFresh](#spfresh) to implement ANN search, enabling this tradeoff at low cost, while maintaining >90-95% recall@10 even in large namespaces. See [Recall](#recall) and the [vector search guide](/docs/vector) for more details. ## Attribute Index Attribute indexes are inverted indexes built for filterable attributes, enabling fast filtering and sorting operations. These indexes are aware of the primary vector index and understand the clustering hierarchy, allowing them to work together for high-recall filtered vector searches. See the [native filtering blog post](/blog/native-filtering) for details on how attribute indexes enable high-recall filtered queries. ## Branch A branch is an instant, copy-on-write clone of a namespace. Both namespaces are fully independent after creation — writes to one are never visible in the other. See the [branching guide](/docs/branching) for details and examples. ## Cache Hierarchy turbopuffer uses a multi-tier cache hierarchy: object storage (source of truth), NVMe SSD cache (for recently queried namespaces), and memory cache (for frequently accessed namespaces). After a cold query, data is cached on NVMe SSD, and frequently accessed namespaces are stored in memory. The storage engine is designed to perform small, ranged reads directly from object storage for fast cold queries without needing to load entire namespaces. See the [architecture documentation](/docs/architecture) and [warm cache documentation](/docs/warm-cache) for more details. ## Compute-Storage Separation All durable state in turbopuffer is stored in object storage. Compute nodes are stateless. This means any node can serve queries for any namespace. If a node fails, another can immediately serve queries for any namespaces previously served by the failed node. Data is cached on NVMe SSDs and in memory for performance, but the storage engine is designed to perform efficient reads directly from object storage when needed. This architecture enables cost-effective scaling and high availability without additional cost. See the [architecture documentation](/docs/architecture) and [guarantees](/docs/guarantees) for more details. ## Filtering Filtering allows queries to restrict results to documents matching specific attribute conditions. Filters can be simple (equality, comparison) or complex (nested AND/OR expressions, glob patterns, regex). Filterable attributes are indexed into inverted indexes for fast evaluation. See the [query documentation](/docs/query#filtering-parameters) for filter syntax and the [native filtering blog post](/blog/native-filtering) for how filtering works with vector search. ## FTS/BM25 Full-Text Search (FTS) in turbopuffer uses the BM25 (Best Matching 25) ranking function, a classic text search algorithm that considers query term frequency and document length. BM25 scores documents based on how well they match query terms, with higher scores for documents that contain more relevant terms. Full-text search is enabled on a per-attribute basis, and turbopuffer builds a BM25 index for each enabled attribute. BM25 results can be combined with vector search results client-side for [hybrid search](/docs/hybrid). See the [full-text search guide](/docs/fts) for details on using BM25. ## Group Commit Group commit is a write batching technique that combines multiple pending writes into a single I/O operation. When a write is in flight, incoming writes are buffered in memory. As soon as the current write completes, all buffered writes are flushed together in the next write. This decouples write throughput from I/O latency, making it possible to achieve high throughput even when individual writes are slow (e.g., ~200ms for object storage writes). turbopuffer uses group commit for batching writes to the [WAL](#wal) and for its internal [indexing job queue](/blog/object-storage-queue). ## Hybrid Search Hybrid search combines multiple search strategies to improve search quality. turbopuffer supports vector search (for semantic relevance) and BM25 full-text search (for exact keyword matching). To implement hybrid search, send multiple queries (which can be batched in a single API call using [multi-query](/docs/query#multi-queries)) and combine results client-side using techniques like reciprocal-rank fusion. See the [hybrid search guide](/docs/hybrid) for examples and best practices. ## Indexing After data is committed to the WAL, it is asynchronously indexed by separate indexing nodes to enable efficient retrieval. This compute-compute separation means expensive indexing operations don't impact query performance. Unindexed data is still searched exhaustively for strongly consistent queries. Indexing progress can be tracked through the `unindexed_bytes` field in the [metadata endpoint](/docs/metadata). By default, attributes are indexed for filtering and sorting, but you can disable indexing for attributes you don't need to filter on using the [schema](/docs/write#schema). See the [architecture documentation](/docs/architecture) for more details on the indexing process. ## Log-structured merge (LSM) tree A log-structured merge (LSM) tree is a data structure that buffers writes in memory, flushes them to storage as immutable sorted runs, and periodically merges (compacts) those runs together. Reads check across runs and merge the results into a single ordered stream. Most LSM trees are built for local disk. turbopuffer's is built natively on object storage. When a write is committed to the [WAL](#wal), it triggers an asynchronous [indexing](#indexing) job that builds the index on the LSM tree. Because object storage is the source of truth, compute nodes are stateless. Any [query node](#query-and-indexing-nodes) can serve queries for any namespace, and any [indexing node](#indexing) can run compaction. The storage engine and query planner work together to minimize roundtrips to object storage, targeting sub-second cold query latency. Compaction runs periodically to merge sorted runs, improving query performance and ensuring deleted data is eventually removed. See the [architecture documentation](/docs/architecture) for more details on how the storage engine works. ## Multi-Tenancy Multi-tenancy can refer to two things in the turbopuffer context. First, turbopuffer is a multi-tenant service, meaning each binary handles requests for multiple tenants (organizations). This keeps costs low while maintaining isolation between tenants. Enterprise customers can be isolated on request through single-tenancy clusters or BYOC (Bring Your Own Cloud) deployments. Second, turbopuffer's architecture is particularly well-suited for multi-tenancy use cases. You can create unlimited namespaces, and each namespace has its own vector index, full-text search index, attribute index, or a combination. This means you can scale to support unlimited tenants, datasets, or applications, each with their own isolated indexes, without architectural constraints. See the [security documentation](/docs/security) for more details on multi-tenancy and isolation options. ## Namespace A namespace is an isolated container for documents and vectors in turbopuffer. Each namespace has its own prefix on object storage and is implicitly created when the first document is inserted. We recommend creating one namespace per set of documents that are expected to be returned in the same query rather than using filters to separate data. Smaller namespaces generally provide better query performance. See the [write documentation](/docs/write) for details on creating namespaces and the [namespaces API](/docs/namespaces) for listing them. ## Primary Key The primary key in turbopuffer is the document id, which uniquely identifies each document within a namespace. Document ids can be unsigned 64-bit integers, 128-bit UUIDs, or strings up to 64 bytes. The primary key is used to reference documents for updates, patches, and deletes. See the [write documentation](/docs/write) for details on document ids. ## Query A query reads data from a namespace. Queries can be used to retrieve documents by vector similarity, full-text search score, attribute value conditions, and more. Queries can also be used to compute aggregations. See the [query documentation](/docs/query) for details on query syntax and options. ## Query and Indexing Nodes turbopuffer uses compute-compute separation with two types of nodes: query nodes and indexing nodes. Query nodes handle API requests, such as reads and writes, while indexing nodes maintain the indexes asynchronously, writing to object storage new index states that query nodes discover. This separation ensures that indexing operations don't impact query performance. Both node types auto-scale with demand. See the [guarantees documentation](/docs/guarantees) for more details on compute-compute separation. ## Read Consistency turbopuffer provides strong consistency by default: if you perform a write, a subsequent query will immediately see the write. Strong consistency ensures queries see all data written before the query started, with a ~10ms latency floor due to object storage checks for the latest writes. For workloads requiring sub-10ms latency, you can configure queries to use [eventual consistency](/docs/query#param-consistency), which trades consistency for lower latency. Eventual consistency searches up to 128 MiB of unindexed data and allows data to be up to about one hour stale in the worst case. Over 99.8% of queries return consistent data even with eventual consistency. See the [query documentation](/docs/query#param-consistency) for details on configuring consistency levels and the [guarantees documentation](/docs/guarantees) for more details on consistency guarantees. ## Recall Recall is a metric that measures the accuracy of approximate nearest-neighbor (ANN) search by comparing results against a brute-force exhaustive search. Specifically, recall@k is the ratio of results returned by the ANN search that also appear in the top k results of an exhaustive search. turbopuffer automatically measures recall on 1% of live query traffic and aims for 90-95% recall@10 for all queries, including filtered queries. You can evaluate recall for your namespace using the [recall endpoint](/docs/recall). See the [continuous recall blog post](/blog/continuous-recall) for more details on how turbopuffer ensures high recall. ## Regex / Glob Index When `glob: true` or `regex: true` is enabled in the [schema](/docs/write#schema) for an attribute, we'll build a trigram-based index to accelerate [Glob](/docs/query#param-Glob) and [Regex](/docs/query#param-Regex) filters. The index allows us to first narrow the set of possibly matching candidates for a given Glob/Regex before doing exhaustive evaluation. ## Schema turbopuffer maintains a schema for each namespace that defines the type and indexing behavior for each attribute, including vectors. Within a namespace, attributes must have consistent types across all documents. Within each vector column, all vectors must have the same number of dimensions. By default, data types for attributes are automatically inferred and all attributes are indexed for filtering and sorting. You can customize indexing behavior or specify types that cannot be automatically inferred (e.g., `uuid`, `datetime`) by passing a schema object in a write request. To inspect the schema for a namespace, use the [metadata endpoint](/docs/metadata). See the [write documentation](/docs/write#schema) for details on configuring schemas. ## SPFresh SPFresh is a centroid-based approximate nearest neighbor (ANN) index that turbopuffer uses for vector search. It allows turbopuffer to efficiently locate vectors nearby the query vector by navigating clusters of vectors at a time. SPFresh incrementally updates clusters as vectors change, while maintaining high recall. This avoids expensive full index rebuilds and efficiently enables large scale namespaces. SPFresh works well for object storage as it minimizes roundtrips compared to graph-based indexes. See the [architecture documentation](/docs/architecture) for more details. ## Vectors and Documents Documents are the basic unit of data in turbopuffer. Each document has a unique ID (unsigned 64-bit integer, 128-bit UUID, or string up to 64 bytes) within a namespace and can contain vectors and attributes. Vectors are arrays of floating-point numbers used for vector similarity search. A namespace may or may not have vector indexes; if it does, all documents must include all vector attributes. Attributes are key-value pairs that can be used for filtering, sorting, and full-text search. Within each namespace, both attributes and vectors must have consistent types. See the [write documentation](/docs/write) for details on creating documents and the [vector search guide](/docs/vector) for using vectors. ## WAL The Write-Ahead Log (WAL) is turbopuffer's mechanism for ensuring data consistency and durability. Write operations for a given namespace are batched together for up to a second, with concurrent writes to the same namespace automatically combined into the same WAL entry. When a write returns successfully, the data is guaranteed to be durably written to a new file in the WAL directory inside the namespace's prefix on object storage, providing [durable writes](/docs/guarantees). After data is committed to the log, it is asynchronously indexed to enable efficient retrieval. This design enables high write throughput (~10,000+ vectors/sec) while maintaining durability guarantees. See the [architecture documentation](/docs/architecture) for more details on how WAL works. --- This page: [/docs/concepts.md](https://turbopuffer.com/docs/concepts.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Delete namespace DELETE /v2/namespaces/:namespace Delete a namespace. Deletes the namespace and all its documents entirely. There is no way to recover a deleted namespace. After the delete operation returns `HTTP 200`, you can reuse the same namespace name by writing to it again. ## Examples ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/delete-namespace-example-curl \ -X DELETE --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" # Response payload # { # "status": "ok" # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'delete-namespace-example-py') # If an error occurs, this call raises a turbopuffer.APIError if a retry was not successful. ns.delete_all() ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`delete-namespace-example-ts`); await ns.deleteAll(); ``` ```go package main import ( "context" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("delete-namespace-example-go") _, err = ns.DeleteAll(ctx, turbopuffer.NamespaceDeleteAllParams{}) if err != nil { // Returns an error if the deletion is not successful even after // retries. panic(err) } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; public class DeleteNamespace { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("delete-namespace-example-java"); // If an error occurs, this call raises a TurbopufferServiceException if // a retry was not successful. ns.deleteAll(); } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("delete-namespace-example-csharp"); // If an error occurs, this call raises a TurbopufferApiException if // a retry was not successful. await ns.DeleteAll(new NamespaceDeleteAllParams()); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("delete-namespace-example-rb") # If an error occurs, this call raises a Turbopuffer::Errors::APIError if a retry was not successful. ns.delete_all ``` --- This page: [/docs/delete-namespace.md](https://turbopuffer.com/docs/delete-namespace.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Encryption with Customer Managed Keys (CMEK) ``` ┌─────tpuf bucket────────────┐ │ ┌────────────────────┐ │░ │ │ namespace a │ │░ ┌───your cloud───────────┐ ──────write────────┼─▶│ (AES-256, Cloud │ │░ │ ┌──EKM-A─────────────┐ │ │ │ managed key) │ │░ │ │ ╔══════╗ ┌──────┐ │ │ │ └────────────────────┘ │░ ┌──────┼─┼▶║key-1 ║ │key-2 │ │ │ write │ ┌────────────────────┐ │░ │ │ │ ╚══════╝ └──────┘ │ │ ──/EKM-A/key-1─────┼─▶│ namespace b │ │░ │ │ └────────────────────┘ │ │ │(AES-256, Your Key) │◀───┼─────┘ └────────────────────────┘ │ │ │ │░ │ └────────────────────┘ │░ ┌───your customer's cloud┐ │ ┌────────────────────┐ │░ │ ┌─EKM-B──────────────┐ │ write │ │ namespace C │ │░ │ │ ╔══════╗ ┌──────┐ │ │ ──/EKM-B/key-3─────┼─▶│ (AES-256, Your │◀───┼────────────┼─┼▶║key-3 ║ │key-4 │ │ │ │ │ Customer's Key) │ │░ │ │ ╚══════╝ └──────┘ │ │ │ └────────────────────┘ │░ │ └────────────────────┘ │ └────────────────────────────┘░ └────────────────────────┘ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` By default, all data at rest is encrypted using AES-256 using the cloud provider's managed keys. turbopuffer supports [customer managed encryption keys](/docs/write#param-encryption) (CMEK) for [enterprise](/pricing) customers. CMEK encryption allows customer and customer's customer the similar control of their data as if it was in their own bucket. CMEK can often be used in place of self-hosting with simpler operational requirements. When using CMEK, writes provide a key name (GCP resource id or AWS ARN) identifying an encryption key in the customer's key management system (customer KMS) also known as External Key Manager (EKM). All namespace objects will then be encrypted with this customer provided key, which can be revoked at any time. ## Enabling CMEK 1. Ensure you are on the [enterprise](/pricing) plan. 2. Open your cloud Provider's Console and create a KMS/EKM in the same region as the turbopuffer region(s) you're using. 3. Ask turbopuffer support to get the turbopuffer Service Account email (GCP) or account ARN (AWS). 4. Grant turbopuffer access to the key: - On GCP, edit the *Key Ring* and grant the Permission `Cloud KMS CryptoKey Encrypter/Decrypter` to the turbopuffer service account email. - On AWS, edit the *Key Policy* to add the following statement: ```json { "Sid": "KeyUsage", "Effect": "Allow", "Principal": { "AWS": "" }, "Action": [ "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:Encrypt", "kms:DescribeKey", "kms:Decrypt" ], "Resource": "*" } ``` 5. Use the key name to [write](/docs/write#param-encryption) to your turbopuffer namespace. ## When do I provide the encryption key? The encryption key name only needs to be provided on [writes](/docs/write#param-encryption). All future writes will use the previously sent encryption key name, which cannot be changed after the first upsert. Queries do not need to provide the encryption key name; the underlying object store will transparently decrypt objects so long as turbopuffer maintains permission to use your keys. ## Does CMEK impact latency or availability? No, CMEK does not impact either availability or performance of turbopuffer. ## What does it cost? On the turbopuffer side, there is no additional cost to using CMEK on top of your plan. Your cloud provider will charge you based on the number of encryption operations and the number of keys. ## Who is doing the encryption? Encryption of the data at rest is handled entirely by the cloud object store. * AWS S3 - data is stored with [Server-Side Encryption using AWS KMS-managed keys](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html) * Google Cloud Storage - data is stored with GCS's [CMEK](https://cloud.google.com/storage/docs/encryption/customer-managed-keys). ## How quickly does key revocation take effect? Key revocation is subject to a small propagation delay governed by the underlying cloud provider: * On AWS, key revocation typically propagates in a few seconds, but in some cases can take several minutes ([docs](https://docs.aws.amazon.com/kms/latest/developerguide/grants.html#terms-eventual-consistency)). * On GCP, key revocation typically propagates within one minute, but in exceptional cases can take several hours ([docs](https://docs.cloud.google.com/kms/docs/consistency)). ## Does turbopuffer support key rotation? When you rotate your cloud KMS key, turbopuffer will automatically use the latest active key version for new writes. However, turbopuffer does not automatically re-encrypt existing data. This means: - Data written before rotation remains encrypted with the previous key version - New data will be encrypted with the latest key version - You must keep all previously used key versions active to maintain access to older data - Revoking previous key versions will make that namespace permanently inaccessible If you need to migrate all data to a new key version, you have two options: 1. Use the [export](/docs/export) API to re-upsert your data into a new namespace with the desired encryption configuration 2. Use [`copy_from_namespace`](/docs/write#param-copy_from_namespace) with a different `encryption` parameter to copy the namespace with a new CMEK key The second option is faster and more cost-effective, with up to a 75% write discount. It also works for upgrading an namespace from default to CMEK encryption, or for downgrading from CMEK to default encryption by setting [`encryption`](/docs/write#param-encryption) to `{"mode": "default"}`. ## How is a branched namespace encrypted? A branched namespace inherits the encryption configuration of the source namespace. If the source uses [CMEK](/docs/encryption), the branch will use the same key. Branching to a different encryption key is not supported — use [`copy_from_namespace`](/docs/write#param-copy_from_namespace) instead. **Should you find this limiting, [contact us](/contact)** --- This page: [/docs/encryption.md](https://turbopuffer.com/docs/encryption.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Enterprise ``` ╔═══(0): Multitenancy (default)═════════════════╗ ║┏━tpuf's cloud━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ║░ ║┃ ┌──────────────┐ ┌──────────────┐┃ ║░ ║┃ │ shared │ │ shared │┃ ║░ ─nw fees──╬╋▶│ compute │──────────▶│ bucket │┃ ║░ ║┃ └──────────────┘ └──────────────┘┃ ║░ ║┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║░ ╚═══════════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` ``` ╔═══(1): Bring Your Own Bucket (BYOB)═══════════╗ ║┏━tpuf's cloud━━━━┓ ┏━your cloud━━━━━┓ ║░ ║┃ ┌──────────────┐ ┃ ┃ ┃ ║░ ║┃ │ shared │ ┃ ┃┌──────────────┐┃ ║░ ──nw fees──╬╋▶│ compute │◀╋────────▶│ bucket │┃ ║░ ║┃ └──────────────┘ ┃ ┃└──────────────┘┃ ║░ ║┗━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━┛ ║░ ╚═══════════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` ``` ╔═══(2): Single-Tenancy Hosted══════════════════╗ ║┏━tpuf's cloud━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ║░ ║┃ ┌──────────────┐ ┌──────────────┐┃ ║░ ║┃ │ isolated │ │ isolated │┃ ║░ ───nw fees─╬╋─▶ compute │──────────▶│ bucket │┃ ║░ ║┃ └──────────────┘ └──────────────┘┃ ║░ ║┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║░ ╚═══════════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` ``` ╔═══(3): Bring Your Own Cloud (BYOC)══════════════════════╗ ║┏━tpuf's cloud━┓ ┏━your cloud (we are oncall)━━━━━━━━━━┓║░ ║┃ ┃ ┃ ┃║░ ║┃┌────────────┐┃ ┃ ┌──────────────┐ ┌──────────────┐┃║░ ║┃│ telemetry │◀──╋─│ compute │───▶│ bucket │┃║░ ║┃└────────────┘┃ ┃ └──────────────┘ └──────────────┘┃║░ ║┗━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛║░ ╚═════════════════════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` ## PoC Process 1. **Suitability.** You will meet with the team to discuss your use case and determine if it is a good fit for the PoC. We will review the [Limits together.](https://turbopuffer.com/docs/limits) If there's a good fit, we will do a follow-up kick-off meeting. 2. **Pricing.** If it is a good match and you want to move forward with a PoC, we will send you a ballpark quote estimate that we will update further as we have data from the PoC. 3. **PoC Kick-off.** We will meet with you to discuss the details of the PoC, including the following: 1. What is the scope of the PoC? 2. What metrics are required to hit? 3. Timeline for the PoC 4. Can we do the PoC without extensive security review, e.g. with scrubbed data? Otherwise, we will need to do a security review and legal review. 4. **Weekly PoC Meetings.** We will meet with you weekly to discuss progress of the PoC. 5. **Procurement.** We send you an MSA, DPA, and final order form to sign. 6. **Launch.** We're in production! --- This page: [/docs/enterprise.md](https://turbopuffer.com/docs/enterprise.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Export documents To export all documents in a namespace, use the [query](/docs/query) API to page through documents by advancing a filter on the `id` attribute. Documents inserted while the export is in progress will be included. A common use-case for this is to copy your all documents to a different namespace after some client-side transformation. To copy documents without transformation, use [copy_from_namespace](/docs/write#param-copy_from_namespace) for a more efficient server-side copy (follow with [delete_by_filter](/docs/write/#param-delete_by_filter) to copy only a subset of documents). ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'export-example-py') last_id = None while True: result = ns.query( rank_by=('id', 'asc'), limit=10_000, filters=('id', 'Gt', last_id) if last_id is not None else turbopuffer.omit, ) # Do something with the page of results. print(result) if len(result.rows) < 10_000: break last_id = result.rows[-1].id ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`export-example-ts`); let lastId: string | number | null = null; while (true) { const result = await ns.query({ rank_by: ["id", "asc"], limit: 10_000, filters: lastId !== null ? ["id", "Gt", lastId] : undefined, }); // Do something with the page of results. console.log(result.rows); if (result.rows!.length < 10_000) break; lastId = result.rows![result.rows!.length - 1].id; } ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("export-example-go") var lastID any for { var filters turbopuffer.Filter if lastID != nil { filters = turbopuffer.NewFilterGt("id", lastID) } result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAttribute("id", turbopuffer.RankByAttributeOrderAsc), Limit: turbopuffer.LimitParam{ Total: 10_000, }, Filters: filters, }, ) if err != nil { panic(err) } // Do something with the page of results. fmt.Print(turbopuffer.PrettyPrint(result.Rows)) if len(result.Rows) < 10_000 { break } lastID = result.Rows[len(result.Rows)-1]["id"] } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; public class Export { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("export-example-java"); JsonValue lastId = null; while (true) { var queryParams = NamespaceQueryParams.builder() .rankBy(RankBy.attribute("id", RankByAttributeOrder.ASC)) .limit(10_000); if (lastId != null) { queryParams = queryParams.filters(Filter.gt("id", lastId)); } var result = ns.query(queryParams.build()); var rows = result.rows().get(); // Do something with the page of results. System.out.println(rows); if (rows.size() < 10_000) { break; } lastId = rows.get(rows.size() - 1).get("id"); } } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Text.Json; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("export-example-csharp"); JsonElement? lastId = null; while (true) { var queryParams = new NamespaceQueryParams { RankBy = RankBy.Attribute("id", RankByAttributeOrder.ASC), Limit = 10_000, }; if (lastId != null) { queryParams = queryParams with { Filters = Filter.Gt("id", lastId) }; } var result = await ns.Query(queryParams); var rows = result.GetRows(); // Do something with the page of results. foreach (var row in rows) { Console.WriteLine(row); } if (rows.Count < 10_000) { break; } lastId = rows[^1]["id"]; } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("export-example-rb") last_id = nil loop do filters = last_id ? ["id", "Gt", last_id] : nil result = ns.query( rank_by: ["id", "asc"], limit: 10_000, filters: filters, ) # Do something with the page of results. puts result.rows break if result.rows.length < 10_000 last_id = result.rows.last.id end ``` --- This page: [/docs/export.md](https://turbopuffer.com/docs/export.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Full-Text Search Guide **Full-Text Search** (BM25, 1M docs, ~300MB. Strongly consistent.) - warm (1M docs): p50=13ms, p90=18ms, p99=29ms - cold (1M docs): p50=316ms, p90=381ms, p99=559ms turbopuffer supports BM25 full-text search for [string and []string types](/docs/write#schema). This guide shows how to configure and use full-text search with different options. turbopuffer's full-text search engine has been written from the ground up for the turbopuffer storage engine for low latency searches directly on object storage. For hybrid search combining both vector and BM25 results, see [Hybrid Search](/docs/hybrid-search). For all available full-text search options, see the [Schema documentation](/docs/write#schema). ## Basic example The simplest form of full-text search is on a single field of type `string`. ```bash # Write some documents with a simple text field called "content". # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/fts-basic-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ { "id": 1, "content": "turbopuffer is a fast search engine with FTS, filtering, and vector search support" }, { "id": 2, "content": "turbopuffer can store billions and billions of documents cheaper than any other search engine" }, { "id": 3, "content": "turbopuffer will support many more types of queries as it evolves. turbopuffer will only get faster." } ], "schema": { "content": { "type": "string", "full_text_search": true } } }' # Basic FTS search. # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/fts-basic-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": ["content", "BM25", "turbopuffer"], "limit": 10, "include_attributes": ["content"] }' # [3, 1, 2] is the default BM25 ranking based on document length and # term frequency # Simple phrase matching, to limit results to documents that contain the terms # "search" and "engine" # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/fts-basic-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": ["content", "BM25", "turbopuffer"], "filters": ["content", "ContainsAllTokens", "search engine"], "limit": 10, "include_attributes": ["content"] }' # [1, 2] (same as above, but without document 3) # To combine with vector search, see: # https://turbopuffer.com/docs/hybrid-search ``` ```python # $ pip install turbopuffer import turbopuffer import os tpuf = turbopuffer.Turbopuffer( # API tokens are created in the dashboard: https://turbopuffer.com/dashboard api_key=os.getenv("TURBOPUFFER_API_KEY"), region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'fts-basic-example-py') ns.write( upsert_rows=[ { 'id': 1, 'content': 'turbopuffer is a fast search engine with FTS, filtering, and vector search support' }, { 'id': 2, 'content': 'turbopuffer can store billions and billions of documents cheaper than any other search engine' }, { 'id': 3, 'content': 'turbopuffer will support many more types of queries as it evolves. turbopuffer will only get faster.' } ], schema={ 'content': { 'type': 'string', # Enable BM25 with default settings # For all config options, see https://turbopuffer.com/docs/write#schema 'full_text_search': True } } ) # Basic FTS search. results = ns.query( rank_by=('content', 'BM25', 'turbopuffer'), limit=10, include_attributes=['content'] ) # [3, 1, 2] is the default BM25 ranking based on document length and # term frequency print(results) # Simple phrase matching filter, to limit results to documents that contain the # terms "search" and "engine" results = ns.query( rank_by=('content', 'BM25', 'turbopuffer'), filters=('content', 'ContainsAllTokens', 'search engine'), limit=10, include_attributes=['content'] ) # [1, 2] (same as above, but without document 3) print(results) # To combine with vector search, see: # https://turbopuffer.com/docs/hybrid-search ``` ```typescript // $ npm install @turbopuffer/turbopuffer import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ // API tokens are created in the dashboard: https://turbopuffer.com/dashboard apiKey: process.env.TURBOPUFFER_API_KEY, region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`fts-basic-example-ts`); await ns.write({ upsert_rows: [ { id: 1, content: "turbopuffer is a fast search engine with FTS, filtering, and vector search support", }, { id: 2, content: "turbopuffer can store billions and billions of documents cheaper than any other search engine", }, { id: 3, content: "turbopuffer will support many more types of queries as it evolves. turbopuffer will only get faster.", }, ], schema: { content: { type: "string", // Enable BM25 with default settings // For all config options, see https://turbopuffer.com/docs/write#schema full_text_search: true, }, }, }); // Basic FTS search, to combine with vector search, see https://turbopuffer.com/docs/hybrid-search let results = await ns.query({ rank_by: ["content", "BM25", "turbopuffer"], limit: 10, include_attributes: ["content"], }); // [3, 1, 2] is the default BM25 ranking based on document length and term frequency console.log(results); // Simple phrase matching filter, to limit results to documents that contain the terms "search" and "engine" results = await ns.query({ rank_by: ["content", "BM25", "turbopuffer"], limit: 10, filters: ["content", "ContainsAllTokens", "search engine"], include_attributes: ["content"], }); // [1, 2] (same as above, but without document 3) console.log(results); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( // API tokens are created in the dashboard: https://turbopuffer.com/dashboard option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("fts-basic-example-go") _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "content": "turbopuffer is a fast search engine with FTS, filtering, and vector search support", }, { "id": 2, "content": "turbopuffer can store billions and billions of documents cheaper than any other search engine", }, { "id": 3, "content": "turbopuffer will support many more types of queries as it evolves. turbopuffer will only get faster.", }, }, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "content": { Type: "string", // Enable BM25 with default settings // For all config options, see https://turbopuffer.com/docs/write#schema FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, }, }, }, ) if err != nil { panic(err) } // Basic FTS search. results, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByTextBM25("content", "turbopuffer"), Limit: turbopuffer.LimitParam{ Total: 10, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"content"}, }, }, ) if err != nil { panic(err) } // [3, 1, 2] is the default BM25 ranking based on document length and // term frequency fmt.Print(turbopuffer.PrettyPrint(results)) // Simple phrase matching filter, to limit results to documents that contain the // terms "search" and "engine" results, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByTextBM25("content", "turbopuffer"), Filters: turbopuffer.NewFilterContainsAllTokens("content", "search engine"), Limit: turbopuffer.LimitParam{ Total: 10, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"content"}, }, }, ) if err != nil { panic(err) } // [1, 2] (same as above, but without document 3) fmt.Print(turbopuffer.PrettyPrint(results)) // To combine with vector search, see: // https://turbopuffer.com/docs/hybrid-search } ``` ```java // dependencies { // implementation("com.turbopuffer:turbopuffer-java:+") // } package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class FtsBasic { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() // API tokens are created in the dashboard: https://turbopuffer.com/dashboard .apiKey(System.getenv("TURBOPUFFER_API_KEY")) .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("fts-basic-example-java"); ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put( "content", "turbopuffer is a fast search engine with FTS, filtering, and vector search support" ) .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put( "content", "turbopuffer can store billions and billions of documents cheaper than any other search engine" ) .build() ) .addUpsertRow( Row.builder() .put("id", 3) .put( "content", "turbopuffer will support many more types of queries as it evolves. turbopuffer will only get faster." ) .build() ) .schema( Schema.builder() .put( "content", AttributeSchemaConfig.builder() .type("string") // Enable BM25 with default settings // For all config options, see https://turbopuffer.com/docs/write#schema .fullTextSearch(FullTextSearchConfig.defaults()) .build() ) .build() ) .build() ); // Basic FTS search. var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankByText.bm25("content", "turbopuffer")) .limit(10) .includeAttributes("content") .build() ); // [3, 1, 2] is the default BM25 ranking based on document length and // term frequency System.out.println(queryResult.rows().get()); // Simple phrase matching filter, to limit results to documents that contain the // terms "search" and "engine" queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankByText.bm25("content", "turbopuffer")) .filters(Filter.containsAllTokens("content", "search engine")) .limit(10) .includeAttributes("content") .build() ); // [1, 2] (same as above, but without document 3) System.out.println(queryResult.rows().get()); // To combine with vector search, see: // https://turbopuffer.com/docs/hybrid-search } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // API tokens are created in the dashboard: https://turbopuffer.com/dashboard // Loaded from TURBOPUFFER_API_KEY env var by default. Override if necessary: // ApiKey = "...", // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("fts-basic-example-csharp"); await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set( "content", "turbopuffer is a fast search engine with FTS, filtering, and vector search support" ), new Row() .Set("id", 2) .Set( "content", "turbopuffer can store billions and billions of documents cheaper than any other search engine" ), new Row() .Set("id", 3) .Set( "content", "turbopuffer will support many more types of queries as it evolves. turbopuffer will only get faster." ), ], Schema = new Dictionary { ["content"] = new AttributeSchemaConfig { Type = "string", // Enable BM25 with default settings // For all config options, see https://turbopuffer.com/docs/write#schema FullTextSearch = true, }, }, } ); // Basic FTS search. var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankByText.BM25("content", "turbopuffer"), Limit = 10, IncludeAttributes = new List { "content" }, } ); // [3, 1, 2] is the default BM25 ranking based on document length and // term frequency foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } // Simple phrase matching filter, to limit results to documents that contain the // terms "search" and "engine" queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankByText.BM25("content", "turbopuffer"), Filters = Filter.ContainsAllTokens("content", "search engine"), Limit = 10, IncludeAttributes = new List { "content" }, } ); // [1, 2] (same as above, but without document 3) foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } // To combine with vector search, see: // https://turbopuffer.com/docs/hybrid-search ``` ```ruby # $ gem install turbopuffer require "turbopuffer" tpuf = Turbopuffer::Client.new( # API tokens are created in the dashboard: https://turbopuffer.com/dashboard api_key: ENV["TURBOPUFFER_API_KEY"], region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("fts-basic-example-rb") ns.write( upsert_rows: [ { id: 1, content: "turbopuffer is a fast search engine with FTS, filtering, and vector search support", }, { id: 2, content: "turbopuffer can store billions and billions of documents cheaper than any other search engine", }, { id: 3, content: "turbopuffer will support many more types of queries as it evolves. turbopuffer will only get faster.", }, ], schema: { content: { type: "string", # Enable BM25 with default settings # For all config options, see https://turbopuffer.com/docs/write#schema full_text_search: true, }, }, ) # Basic FTS search. results = ns.query( rank_by: ["content", "BM25", "turbopuffer"], limit: 10, include_attributes: ["content"], ) # [3, 1, 2] is the default BM25 ranking based on document length and # term frequency puts results.rows # Simple phrase matching filter, to limit results to documents that contain the # terms "search" and "engine" results = ns.query( rank_by: ["content", "BM25", "turbopuffer"], filters: ["content", "ContainsAllTokens", "search engine"], limit: 10, include_attributes: ["content"], ) # [1, 2] (same as above, but without document 3) puts results.rows # To combine with vector search, see: # https://turbopuffer.com/docs/hybrid-search ``` ## Advanced example You can use full-text search operators like [Sum] and [Product] to perform a full-text search across multiple attributes simultaneously. ```bash # Write some documents with a rich set of attributes. # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/fts-advanced-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ { "id": 1, "title": "Getting Started with Python", "content": "Learn Python basics including variables, functions, and classes", "tags": ["python", "programming", "beginner"], "language": "en", "publish_date": 1709251200 }, { "id": 2, "title": "Advanced TypeScript Tips", "content": "Discover advanced TypeScript features and type system tricks", "tags": ["typescript", "javascript", "advanced"], "language": "en", "publish_date": 1709337600 }, { "id": 3, "title": "Python vs JavaScript", "content": "Compare Python and JavaScript for web development", "tags": ["python", "javascript", "comparison"], "language": "en", "publish_date": 1709424000 } ], "schema": { "title": { "type": "string", "full_text_search": { "language": "english", "stemming": true, "remove_stopwords": true, "case_sensitive": false } }, "content": { "type": "string", "full_text_search": { "language": "english", "stemming": true, "remove_stopwords": true } }, "tags": { "type": "[]string", "full_text_search": { "stemming": false, "remove_stopwords": false, "case_sensitive": true } } } }' # Advanced FTS search. # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/fts-advanced-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": ["Sum", [ ["Product", 3, ["title", "BM25", "python beginner"]], ["Product", 2, ["content", "BM25", "python beginner"]], ["tags", "BM25", "python beginner"] ]], "filters": ["And", [ ["publish_date", "Gte", 1709251200], ["language", "Eq", "en"] ]], "limit": 10, "include_attributes": ["title", "content", "tags"] }' # To combine with vector search, see: https://turbopuffer.com/docs/hybrid-search ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'fts-advanced-example-py') # Write some documents with a rich set of attributes. ns.write( upsert_rows=[ { 'id': 1, 'title': 'Getting Started with Python', 'content': 'Learn Python basics including variables, functions, and classes', 'tags': ['python', 'programming', 'beginner'], 'language': 'en', 'publish_date': 1709251200 }, { 'id': 2, 'title': 'Advanced TypeScript Tips', 'content': 'Discover advanced TypeScript features and type system tricks', 'tags': ['typescript', 'javascript', 'advanced'], 'language': 'en', 'publish_date': 1709337600 }, { 'id': 3, 'title': 'Python vs JavaScript', 'content': 'Compare Python and JavaScript for web development', 'tags': ['python', 'javascript', 'comparison'], 'language': 'en', 'publish_date': 1709424000 } ], schema={ 'title': { 'type': 'string', 'full_text_search': { # See all FTS indexing options at # https://turbopuffer.com/docs/write#param-full_text_search 'language': 'english', 'stemming': True, 'remove_stopwords': True, 'case_sensitive': False } }, 'content': { 'type': 'string', 'full_text_search': { 'language': 'english', 'stemming': True, 'remove_stopwords': True } }, 'tags': { 'type': '[]string', 'full_text_search': { 'stemming': False, 'remove_stopwords': False, 'case_sensitive': True } } } ) # Advanced FTS search. # In this example, hits on `title` and `tags` are weighted / boosted higher than # hits on `content`. result = ns.query( # See all FTS query options at https://turbopuffer.com/docs/query rank_by=('Sum', ( ('Product', 3, ('title', 'BM25', 'python beginner')), ('Product', 2, ('tags', 'BM25', 'python beginner')), ('content', 'BM25', 'python beginner') )), filters=('And', ( ('publish_date', 'Gte', 1709251200), ('language', 'Eq', 'en'), )), limit=10, include_attributes=['title', 'content', 'tags'] ) print(result.rows) # To combine with vector search, see: # https://turbopuffer.com/docs/hybrid-search ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`fts-advanced-example-ts`); // Write some documents with a rich set of attributes. await ns.write({ upsert_rows: [ { id: 1, title: "Getting Started with Python", content: "Learn Python basics including variables, functions, and classes", tags: ["python", "programming", "beginner"], language: "en", publish_date: 1709251200, }, { id: 2, title: "Advanced TypeScript Tips", content: "Discover advanced TypeScript features and type system tricks", tags: ["typescript", "javascript", "advanced"], language: "en", publish_date: 1709337600, }, { id: 3, title: "Python vs JavaScript", content: "Compare Python and JavaScript for web development", tags: ["python", "javascript", "comparison"], language: "en", publish_date: 1709424000, }, ], schema: { title: { type: "string", full_text_search: { // See all FTS indexing options at // https://turbopuffer.com/docs/write#param-full_text_search language: "english", stemming: true, remove_stopwords: true, case_sensitive: false, }, }, content: { type: "string", full_text_search: { language: "english", stemming: true, remove_stopwords: true, }, }, tags: { type: "[]string", full_text_search: { stemming: false, remove_stopwords: false, case_sensitive: true, }, }, }, }); // Advanced FTS search. // In this example, hits on `title` and `tags` are weighted / boosted higher // than hits on `content`. const results = await ns.query({ // See all FTS query options at https://turbopuffer.com/docs/query rank_by: [ "Sum", [ ["Product", 3, ["title", "BM25", "python beginner"]], ["Product", 2, ["tags", "BM25", "python beginner"]], ["content", "BM25", "python beginner"], ], ], filters: [ "And", [ ["publish_date", "Gte", 1709251200], ["language", "Eq", "en"], ], ], limit: 10, include_attributes: ["title", "content", "tags"], }); console.log(results); // To combine with vector search, see: // https://turbopuffer.com/docs/hybrid-search ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("fts-advanced-example-go") // Write some documents with a rich set of attributes. _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "title": "Getting Started with Python", "content": "Learn Python basics including variables, functions, and classes", "tags": []string{"python", "programming", "beginner"}, "language": "en", "publish_date": 1709251200, }, { "id": 2, "title": "Advanced TypeScript Tips", "content": "Discover advanced TypeScript features and type system tricks", "tags": []string{"typescript", "javascript", "advanced"}, "language": "en", "publish_date": 1709337600, }, { "id": 3, "title": "Python vs JavaScript", "content": "Compare Python and JavaScript for web development", "tags": []string{"python", "javascript", "comparison"}, "language": "en", "publish_date": 1709424000, }, }, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "title": { Type: "string", // See all FTS indexing options at // https://turbopuffer.com/docs/write#param-full_text_search FullTextSearch: &turbopuffer.FullTextSearchConfigParam{ Language: turbopuffer.LanguageEnglish, Stemming: turbopuffer.Bool(true), RemoveStopwords: turbopuffer.Bool(true), CaseSensitive: turbopuffer.Bool(false), }, }, "content": { Type: "string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{ Language: turbopuffer.LanguageEnglish, Stemming: turbopuffer.Bool(true), RemoveStopwords: turbopuffer.Bool(true), }, }, "tags": { Type: "[]string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{ Stemming: turbopuffer.Bool(false), RemoveStopwords: turbopuffer.Bool(false), CaseSensitive: turbopuffer.Bool(true), }, }, }, }, ) if err != nil { panic(err) } // Advanced FTS search. // In this example, hits on `title` and `tags` are weighted / boosted higher than // hits on `content`. result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ // See all FTS query options at https://turbopuffer.com/docs/query RankBy: turbopuffer.NewRankByTextSum([]turbopuffer.RankByText{ turbopuffer.NewRankByTextProduct(3, turbopuffer.NewRankByTextBM25("title", "python beginner")), turbopuffer.NewRankByTextProduct(2, turbopuffer.NewRankByTextBM25("tags", "python beginner")), turbopuffer.NewRankByTextBM25("content", "python beginner"), }), Filters: turbopuffer.NewFilterAnd([]turbopuffer.Filter{ turbopuffer.NewFilterGte("publish_date", 1709251200), turbopuffer.NewFilterEq("language", "en"), }), Limit: turbopuffer.LimitParam{ Total: 10, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"title", "content", "tags"}, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) // To combine with vector search, see: // https://turbopuffer.com/docs/hybrid-search } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class FtsAdvanced { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("fts-advanced-example-java"); ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("title", "Getting Started with Python") .put("content", "Learn Python basics including variables, functions, and classes") .put("tags", List.of("python", "programming", "beginner")) .put("language", "en") .put("publish_date", 1709251200) .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("title", "Advanced TypeScript Tips") .put("content", "Discover advanced TypeScript features and type system tricks") .put("tags", List.of("typescript", "javascript", "advanced")) .put("language", "en") .put("publish_date", 1709337600) .build() ) .addUpsertRow( Row.builder() .put("id", 3) .put("title", "Python vs JavaScript") .put("content", "Compare Python and JavaScript for web development") .put("tags", List.of("python", "javascript", "comparison")) .put("language", "en") .put("publish_date", 1709424000) .build() ) .schema( Schema.builder() .put( "title", AttributeSchemaConfig.builder() .type("string") .fullTextSearch( FullTextSearchConfig.builder() .language(Language.ENGLISH) .stemming(true) .removeStopwords(true) .caseSensitive(false) .build() ) .build() ) .put( "content", AttributeSchemaConfig.builder() .type("string") .fullTextSearch( FullTextSearchConfig.builder() .language(Language.ENGLISH) .stemming(true) .removeStopwords(true) .build() ) .build() ) .put( "tags", AttributeSchemaConfig.builder() .type("[]string") .fullTextSearch( FullTextSearchConfig.builder() .stemming(false) .removeStopwords(false) .caseSensitive(true) .build() ) .build() ) .build() ) .build() ); // Advanced FTS search. // In this example, hits on `title` and `tags` are weighted / boosted higher than // hits on `content`. var result = ns.query( NamespaceQueryParams.builder() .rankBy( RankByText.sum( RankByText.product(3, RankByText.bm25("title", "python beginner")), RankByText.product(2, RankByText.bm25("tags", "python beginner")), RankByText.bm25("content", "python beginner") ) ) .filters(Filter.and(Filter.gte("publish_date", 1709251200), Filter.eq("language", "en"))) .limit(10) .includeAttributes("title", "content", "tags") .build() ); System.out.println(result.rows().get()); // To combine with vector search, see: // https://turbopuffer.com/docs/hybrid-search } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("fts-advanced-example-csharp"); await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("title", "Getting Started with Python") .Set("content", "Learn Python basics including variables, functions, and classes") .Set("tags", new[] { "python", "programming", "beginner" }) .Set("language", "en") .Set("publish_date", 1709251200), new Row() .Set("id", 2) .Set("title", "Advanced TypeScript Tips") .Set("content", "Discover advanced TypeScript features and type system tricks") .Set("tags", new[] { "typescript", "javascript", "advanced" }) .Set("language", "en") .Set("publish_date", 1709337600), new Row() .Set("id", 3) .Set("title", "Python vs JavaScript") .Set("content", "Compare Python and JavaScript for web development") .Set("tags", new[] { "python", "javascript", "comparison" }) .Set("language", "en") .Set("publish_date", 1709424000), ], Schema = new Dictionary { ["title"] = new AttributeSchemaConfig { Type = "string", FullTextSearch = new FullTextSearchConfig { Language = Language.English, Stemming = true, RemoveStopwords = true, CaseSensitive = false, }, }, ["content"] = new AttributeSchemaConfig { Type = "string", FullTextSearch = new FullTextSearchConfig { Language = Language.English, Stemming = true, RemoveStopwords = true, }, }, ["tags"] = new AttributeSchemaConfig { Type = "[]string", FullTextSearch = new FullTextSearchConfig { Stemming = false, RemoveStopwords = false, CaseSensitive = true, }, }, }, } ); // Advanced FTS search. // In this example, hits on `title` and `tags` are weighted / boosted higher than // hits on `content`. var result = await ns.Query( new NamespaceQueryParams { RankBy = RankByText.Sum( RankByText.Product(3, RankByText.BM25("title", "python beginner")), RankByText.Product(2, RankByText.BM25("tags", "python beginner")), RankByText.BM25("content", "python beginner") ), Filters = Filter.And( Filter.Gte("publish_date", 1709251200), Filter.Eq("language", "en") ), Limit = 10, IncludeAttributes = new List { "title", "content", "tags" }, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } // To combine with vector search, see: // https://turbopuffer.com/docs/hybrid-search ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("fts-advanced-example-rb") # Write some documents with a rich set of attributes. ns.write( upsert_rows: [ { id: 1, title: "Getting Started with Python", content: "Learn Python basics including variables, functions, and classes", tags: ["python", "programming", "beginner"], language: "en", publish_date: 1709251200, }, { id: 2, title: "Advanced TypeScript Tips", content: "Discover advanced TypeScript features and type system tricks", tags: ["typescript", "javascript", "advanced"], language: "en", publish_date: 1709337600, }, { id: 3, title: "Python vs JavaScript", content: "Compare Python and JavaScript for web development", tags: ["python", "javascript", "comparison"], language: "en", publish_date: 1709424000, }, ], schema: { title: { type: "string", full_text_search: { # See all FTS indexing options at # https://turbopuffer.com/docs/write#param-full_text_search language: "english", stemming: true, remove_stopwords: true, case_sensitive: false, }, }, content: { type: "string", full_text_search: { language: "english", stemming: true, remove_stopwords: true, }, }, tags: { type: "[]string", full_text_search: { stemming: false, remove_stopwords: false, case_sensitive: true, }, }, }, ) # Advanced FTS search. # In this example, hits on `title` and `tags` are weighted / boosted higher than # hits on `content`. result = ns.query( # See all FTS query options at https://turbopuffer.com/docs/query rank_by: ["Sum", [ ["Product", 3, ["title", "BM25", "python beginner"]], ["Product", 2, ["tags", "BM25", "python beginner"]], ["content", "BM25", "python beginner"], ]], filters: ["And", [ ["publish_date", "Gte", 1709251200], ["language", "Eq", "en"], ]], limit: 10, include_attributes: ["title", "content", "tags"], ) puts result.rows # To combine with vector search, see: # https://turbopuffer.com/docs/hybrid-search ``` ## Custom tokenization When turbopuffer's built-in tokenizers aren't sufficient, use the `pre_tokenized_array` [tokenizer](/docs/fts/#tokenizers) to perform client side tokenization using arbitrary logic. ```python import turbopuffer from typing import List tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) # A simple word tokenizer that preserves hyphens instead of splitting on them. def tokenize(text: str) -> List[str]: # Replace all characters besides alphanumeric and '-' with spaces. cleaned = "" for ch in text: if ch.isalnum() or ch in "-": cleaned += ch else: cleaned += str(" ") # Lowercase and split on spaces. return cleaned.lower().split() # Write some sample data. ns = tpuf.namespace(f'fts-custom-tokenization-example-py') ns.write( upsert_rows=[ {"id": 1, "content": tokenize("We hold these truths to be self-evident.")}, {"id": 2, "content": tokenize("For my own self, it seemed evident.")}, ], schema={ 'content': { 'type': '[]string', 'full_text_search': {'tokenizer': 'pre_tokenized_array'} } } ) # Query for "self" and "evident". results = ns.query( # Notice that the BM25 operator now expects a list of tokens, not a string. rank_by=('content', 'BM25', ['self', 'evident']), limit=10, ) # Only document 2 is matched, because document 1 has the token "self-evident" # but neither the token "self" nor "evident". print(results) # Query for "self-evident". results = ns.query( rank_by=('content', 'BM25', ['self-evident']), limit=10, ) # Now only document 1 is matched. print(results) # To accept string queries, simply apply the tokenizer to the query string # before passing it to the `BM25` operator. def query_string(query: str): return ns.query( rank_by=('content', 'BM25', tokenize(query)), limit=10, ) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; // A simple word tokenizer that preserves hyphens instead of splitting on them. function tokenize(text: string): string[] { return text.toLowerCase().split(/[^a-z0-9-]+/).filter(s => s.length > 0); } const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); // Write some sample data. const ns = tpuf.namespace(`fts-custom-tokenization-example-ts`); await ns.write({ upsert_rows: [ { id: 1, content: tokenize("We hold these truths to be self-evident.") }, { id: 2, content: tokenize("For my own self, it seemed evident.") }, ], schema: { content: { type: "[]string", full_text_search: { tokenizer: "pre_tokenized_array" } } } }); // Query for "self" and "evident". let results = await ns.query({ // Notice that the BM25 operator now expects a list of tokens, not a string rank_by: ["content", "BM25", ["self", "evident"]], limit: 10, }); // Only document 2 is matched, because document 1 has the token "self-evident" // but neither the token "self" nor "evident". console.log(results); // Query for "self-evident". results = await ns.query({ rank_by: ["content", "BM25", ["self-evident"]], limit: 10, }); // Now only document 1 is matched. console.log(results); // To handle string queries, simply apply the tokenizer to the query // string before passing it to the `BM25` operator. async function queryString(query: string) { return await ns.query({ rank_by: ["content", "BM25", tokenize(query)], limit: 10, }); } ``` ```go package main import ( "context" "fmt" "os" "regexp" "strings" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) // A simple word tokenizer that preserves hyphens instead of splitting on them. func tokenize(text string) []string { // Replace all characters besides alphanumeric and '-' with spaces. re := regexp.MustCompile(`[^a-zA-Z0-9-]+`) cleaned := re.ReplaceAllString(text, " ") // Lowercase and split on spaces. tokens := strings.Fields(strings.ToLower(cleaned)) return tokens } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) // Write some sample data. ns := tpuf.Namespace("fts-custom-tokenization-example-go") _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "content": tokenize("We hold these truths to be self-evident."), }, { "id": 2, "content": tokenize("For my own self, it seemed evident."), }, }, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "content": { Type: "[]string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{ Tokenizer: turbopuffer.TokenizerPreTokenizedArray, }, }, }, }, ) if err != nil { panic(err) } // Query for "self" and "evident". results, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ // Notice that the BM25 operator now expects a list of tokens, not a string. RankBy: turbopuffer.NewRankByTextBM25Array("content", []string{"self", "evident"}), Limit: turbopuffer.LimitParam{ Total: 10, }, }, ) if err != nil { panic(err) } // Only document 2 is matched, because document 1 has the token "self-evident" // but neither the token "self" nor "evident". fmt.Print(turbopuffer.PrettyPrint(results)) // Query for "self-evident". results, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByTextBM25Array("content", []string{"self-evident"}), Limit: turbopuffer.LimitParam{ Total: 10, }, }, ) if err != nil { panic(err) } // Now only document 1 is matched. fmt.Print(turbopuffer.PrettyPrint(results)) } // To accept string queries, simply apply the tokenizer to the query // string before passing it to the `BM25` operator. func queryString( ctx context.Context, ns turbopuffer.Namespace, query string, ) (*turbopuffer.NamespaceQueryResponse, error) { return ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByTextBM25Array("content", tokenize(query)), Limit: turbopuffer.LimitParam{ Total: 10, }, }, ) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.*; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class FtsCustomTokenization { // A simple word tokenizer that preserves hyphens instead of splitting on them. public static List tokenize(String text) { // Replace all characters besides alphanumeric and '-' with spaces. String cleaned = text.replaceAll("[^a-zA-Z0-9-]", " "); // Lowercase and split on spaces. return List.of(cleaned.toLowerCase().split(" ")); } public static void main(String[] args) { var client = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = client.namespace("fts-custom-tokenization-example-java"); // Write some sample data. ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("content", tokenize("We hold these truths to be self-evident.")) .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("content", tokenize("For my own self, it seemed evident.")) .build() ) .schema( Schema.builder() .put( "content", AttributeSchemaConfig.builder() .type("[]string") .fullTextSearch( FullTextSearchConfig.builder().tokenizer(Tokenizer.PRE_TOKENIZED_ARRAY).build() ) .build() ) .build() ) .build() ); // Query for "self" and "evident". var results = ns.query( // Notice that the BM25 operator now expects a list of tokens, not a // string. NamespaceQueryParams.builder() .rankBy(RankByText.bm25("content", List.of("self", "evident"))) .limit(10) .build() ); // Only document 2 is matched, because document 1 has the token // "self-evident" but neither the token "self" nor "evident". System.out.println(results.rows().get()); // Query for "self-evident". results = ns.query( NamespaceQueryParams.builder() .rankBy(RankByText.bm25("content", List.of("self-evident"))) .limit(10) .build() ); // Now only document 1 is matched. System.out.println(results.rows().get()); } // To accept string queries, simply apply the tokenizer to the query string // before passing it to the `BM25` operator. public static void queryString(Namespace ns, String query) { ns.query( NamespaceQueryParams.builder() .rankBy(RankByText.bm25("content", tokenize(query))) .limit(10) .build() ); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Turbopuffer; using Turbopuffer.Models.Namespaces; using Turbopuffer.Services; using var client = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = client.Namespace("fts-custom-tokenization-example-csharp"); // Write some sample data. await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row().Set("id", 1).Set("content", Tokenize("We hold these truths to be self-evident.")), new Row().Set("id", 2).Set("content", Tokenize("For my own self, it seemed evident.")), ], Schema = new Dictionary { ["content"] = new AttributeSchemaConfig { Type = "[]string", FullTextSearch = new FullTextSearchConfig { Tokenizer = Tokenizer.PreTokenizedArray, }, }, }, } ); // Query for "self" and "evident". var results = await ns.Query( // Notice that the BM25 operator now expects a list of tokens, not a // string. new NamespaceQueryParams { RankBy = RankByText.BM25("content", new[] { "self", "evident" }), Limit = 10, } ); // Only document 2 is matched, because document 1 has the token // "self-evident" but neither the token "self" nor "evident". foreach (var row in results.GetRows()) { Console.WriteLine(row); } // Query for "self-evident". results = await ns.Query( new NamespaceQueryParams { RankBy = RankByText.BM25("content", new[] { "self-evident" }), Limit = 10, } ); // Now only document 1 is matched. foreach (var row in results.GetRows()) { Console.WriteLine(row); } // A simple word tokenizer that preserves hyphens instead of splitting on them. static List Tokenize(string text) { // Replace all characters besides alphanumeric and '-' with spaces. string cleaned = Regex.Replace(text, "[^a-zA-Z0-9-]", " "); // Lowercase and split on spaces. return cleaned.ToLowerInvariant().Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); } // To accept string queries, simply apply the tokenizer to the query string // before passing it to the `BM25` operator. static async Task QueryString(INamespaceService ns, string query) { await ns.Query( new NamespaceQueryParams { RankBy = RankByText.BM25("content", Tokenize(query)), Limit = 10, } ); } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) # A simple word tokenizer that preserves hyphens instead of splitting on them. def tokenize(text) text.downcase.split(/[^a-zA-Z0-9\-]+/).filter { |s| s.length > 0 } end # Write some sample data. ns = tpuf.namespace("fts-custom-tokenization-example-rb") ns.write( upsert_rows: [ { id: 1, content: tokenize("We hold these truths to be self-evident.") }, { id: 2, content: tokenize("For my own self, it seemed evident.") }, ], schema: { content: { type: "[]string", full_text_search: { tokenizer: "pre_tokenized_array" }, }, }, ) # Query for "self" and "evident". results = ns.query( # Notice that the BM25 operator now expects a list of tokens, not a string. rank_by: ["content", "BM25", ["self", "evident"]], limit: 10, ) # Only document 2 is matched, because document 1 has the token "self-evident" # but neither the token "self" nor "evident". puts results.rows # Query for "self-evident". results = ns.query( rank_by: ["content", "BM25", ["self-evident"]], limit: 10, ) # Now only document 1 is matched. puts results.rows # To accept string queries, simply apply the tokenizer to the query string # before passing it to the `BM25` operator. def query_string(ns, query) ns.query( rank_by: ["content", "BM25", tokenize(query)], limit: 10, ) end ``` [Sum]: /docs/query#fts-operators [Product]: /docs/query#field-weightsboosts ## Supported languages turbopuffer currently supports language-aware stemming and stopword removal for full-text search. The following languages are supported: arabic danish dutch english (default) finnish french german greek hungarian italian norwegian portuguese romanian russian spanish swedish tamil turkish For latin-script languages with diacritics (e.g. french, spanish), consider enabling [`ascii_folding`](/docs/write#param-full_text_search) in your BM25 params. Other languages can be supported by [contacting us](/contact). ## Tokenizers - `word_v4` (default for new namespaces) - `word_v3` - `word_v2` - `word_v1` - `word_v0` - `pre_tokenized_array` The default tokenizer is periodically upgraded. If your application relies on specific tokenization behavior, you should explicitly specify a tokenizer in the [schema](/docs/write#param-full_text_search). The `word_v4` and `word_v3` tokenizers use [Unicode v17.0 text segmentation rules](https://www.unicode.org/reports/tr29/tr29-47.html) (UAX #29) for accurate segmentation across most languages, scripts, and emojis. `word_v4` is the current default for new namespaces; it behaves like `word_v3`, but is roughly 3x faster and fixes a few tokenization edge cases. It's powered by our open-source [alyze](https://github.com/turbopuffer/alyze) library. The `word_v2` tokenizer forms tokens from ideographic codepoints, contiguous sequences of alphanumeric codepoints, and sequences of emoji codepoints that form a single glyph. Codepoints that are not alphanumeric, ideographic, or an emoji are discarded. Codepoints are classified according to Unicode v16.0. The `word_v1` tokenizer works like the `word_v2` tokenizer, except that ideographic codepoints are treated as alphanumeric codepoint. Codepoints are classified according to Unicode v10.0. The `word_v0` tokenizer works like the `word_v1` tokenizer, except that emoji codepoints are discarded. The `pre_tokenized_array` tokenizer is a special tokenizer that indicates that you want to perform your own tokenization. This tokenizer can only be used on attributes of type `[]string`; each string in the array is interpreted as a token. When this tokenizer is active, queries using the `BM25` or `ContainsAllTokens` operators must supply a query operand of type `[]string` rather than `string`; each string in the array is interpreted as a token. Tokens are always matched case sensitively, without stemming or stopword removal. You cannot specify `language`, `stemming: true`, `remove_stopwords: true`, or `case_sensitive: false` when using this tokenizer. New tokenizers can be requested by [contacting us](/contact). ## Fuzzy matching turbopuffer supports fuzzy string matching within a specified edit distance via the [Fuzzy filter](/docs/query#param-Fuzzy). Fuzzy filters require the [`fuzzy`](/docs/write#param-fuzzy) schema parameter to be set to `true` on the queried attribute. The `max_edit_distance` parameter determines the maximum allowable number of edits for a query string of specified number of characters to match the filter. For example: ```python "max_edit_distance": [ # Queries >= 6 characters match on substrings within 1 edit # Queries >= 9 characters match on substrings within 2 edits # Queries < 6 characters match nothing { "min_query_chars": 6, "distance": 1 }, { "min_query_chars": 9, "distance": 2 } ], ``` A missing or added character, incorrect character, missing or added diacritic (e.g. ü), or case difference will add 1 to the edit distance by default. If the `case_sensitive` parameter is set to `false`, case differences do not count toward the edit distance. Fuzzy matching can be used as a filter directly, or within a `rank_by` expression as a [Rank by filter](/docs/query#rank-by-filter), possibly in conjunction with other expressions: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/fts-fuzzy-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ {"id": 1, "company_name": "turbopuffer"}, {"id": 2, "company_name": "turbopufer inc"} ], "schema": { "company_name": { "type": "string", "fuzzy": true, "glob": true } } }' # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/fts-fuzzy-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d ' { "rank_by": ["Sum", [ ["Product", 3, ["company_name", "Glob", "*turbopufer*"]], ["company_name", "Fuzzy", "turbopufer", { "max_edit_distance": [ { "min_query_chars": 6, "distance": 1 } ], "case_sensitive": false }] ]], "include_attributes": ["company_name"], "limit": 10 } ' ``` ```python # $ pip install turbopuffer import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'fts-fuzzy-example-py') ns.write( upsert_rows=[ {'id': 1, 'company_name': 'turbopuffer'}, {'id': 2, 'company_name': 'turbopufer inc'}, ], schema={ 'company_name': { 'type': 'string', 'fuzzy': True, 'glob': True, }, }, ) result = ns.query( rank_by=('Sum', ( ('Product', 3, ('company_name', 'Glob', '*turbopufer*')), ('company_name', 'Fuzzy', 'turbopufer', { 'max_edit_distance': [ {'min_query_chars': 6, 'distance': 1}, ], "case_sensitive": False }), )), include_attributes=["company_name"], limit=10 ) print(result.rows) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`fts-fuzzy-example-ts`); await ns.write({ upsert_rows: [ { id: 1, company_name: "turbopuffer" }, { id: 2, company_name: "turbopufer inc" }, ], schema: { company_name: { type: "string", fuzzy: true, glob: true, }, }, }); const result = await ns.query({ rank_by: [ "Sum", [ ["Product", 3, ["company_name", "Glob", "*turbopufer*"]], [ "company_name", "Fuzzy", "turbopufer", { max_edit_distance: [{ min_query_chars: 6, distance: 1 }], case_sensitive: false, }, ], ], ], include_attributes: ["company_name"], limit: 10, }); console.log(result.rows); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("fts-fuzzy-example-go") _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "company_name": "turbopuffer", }, { "id": 2, "company_name": "turbopufer inc", }, }, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "company_name": { Type: "string", Fuzzy: turbopuffer.Bool(true), Glob: turbopuffer.Bool(true), }, }, }, ) if err != nil { panic(err) } result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByTextSum([]turbopuffer.RankByText{ turbopuffer.NewRankByTextProduct(3, turbopuffer.NewFilterGlob("company_name", "*turbopufer*")), turbopuffer.NewFilterFuzzy( "company_name", "turbopufer", turbopuffer.FuzzyParams{ MaxEditDistance: []turbopuffer.FuzzyMaxEditDistanceParam{ {MinQueryChars: 6, Distance: 1}, }, }, ), }), Limit: turbopuffer.LimitParam{ Total: 10, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"company_name"}, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.JsonValue; import com.turbopuffer.models.namespaces.*; import java.util.*; public class FtsFuzzy { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("fts-fuzzy-example-java"); ns.write( NamespaceWriteParams.builder() .addUpsertRow(Row.builder().put("id", 1).put("company_name", "turbopuffer").build()) .addUpsertRow(Row.builder().put("id", 2).put("company_name", "turbopufer inc").build()) .schema( Schema.builder() .put( "company_name", AttributeSchemaConfig.builder().type("string").fuzzy(true).glob(true).build() ) .build() ) .build() ); var result = ns.query( NamespaceQueryParams.builder() .rankBy( RankByText.sum( RankByText.product(3, Filter.glob("company_name", "*turbopufer*")), Filter.fuzzy( "company_name", "turbopufer", FuzzyParams.builder() .addMaxEditDistance( FuzzyMaxEditDistance.builder().minQueryChars(6L).distance(1L).build() ) .putAdditionalProperty("case_sensitive", JsonValue.from(false)) .build() ) ) ) .includeAttributes("company_name") .limit(10) .build() ); System.out.println(result.rows().get()); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using System.Text.Json; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("fts-fuzzy-example-csharp"); await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row().Set("id", 1).Set("company_name", "turbopuffer"), new Row().Set("id", 2).Set("company_name", "turbopufer inc"), ], Schema = new Dictionary { ["company_name"] = new AttributeSchemaConfig { Type = "string", Fuzzy = true, Glob = true, }, }, } ); var result = await ns.Query( new NamespaceQueryParams { RankBy = RankByText.Sum( RankByText.Product(3, Filter.Glob("company_name", "*turbopufer*")), Filter.Fuzzy( "company_name", "turbopufer", FuzzyParams.FromRawUnchecked( new Dictionary { ["max_edit_distance"] = JsonSerializer.SerializeToElement( new[] { new FuzzyMaxEditDistance { MinQueryChars = 6, Distance = 1 }, } ), ["case_sensitive"] = JsonSerializer.SerializeToElement(false), } ) ) ), IncludeAttributes = new List { "company_name" }, Limit = 10, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("fts-fuzzy-example-rb") ns.write( upsert_rows: [ { id: 1, company_name: "turbopuffer" }, { id: 2, company_name: "turbopufer inc" }, ], schema: { company_name: { type: "string", fuzzy: true, glob: true, }, }, ) result = ns.query( rank_by: [ "Sum", [ ["Product", 3, ["company_name", "Glob", "*turbopufer*"]], [ "company_name", "Fuzzy", "turbopufer", { max_edit_distance: [ { min_query_chars: 6, distance: 1 }, ], case_sensitive: false, }, ], ], ], include_attributes: ["company_name"], limit: 10, ) puts result.rows ``` This query will prioritize values that contain exactly "turbopufer" as a substring, while simultaneously ensuring that values that contain a substring within 1 edit are returned (since the query has >= 6 characters). ## Advanced tuning The [BM25 scoring algorithm](https://en.wikipedia.org/wiki/Okapi_BM25) involves three parameters that can be tuned for your workload: - `k1` controls how quickly the impact of term frequency saturates. When `k1` is close to zero, term frequency is effectively ignored when scoring a document. When `k1` is close to infinity, term frequency contributes nearly linearly to the score. The default value, `1.2`, means that increasing term frequency in a document boosts heavily to start but quickly results in diminishing returns. - `b` controls document length normalization. When `b` is `0.0`, documents are treated equally regardless of length, which allows long articles to dominate due to sheer volume of terms. When `b` is `1.0`, documents are boosted or penalized based on the ratio of their length to the average document length in the corpus. The default value, `0.75`, controls for length bias without eliminating it entirely (long documents are often legitimately more relevant). - `k3` controls the saturation point for query term frequency. When a query contains repeated terms, `k3` determines how much additional weight each repetition contributes. When `k3` is close to zero, query term repetition is effectively ignored. When `k3` is large, repeated query terms contribute nearly linearly to the score. The default value, `8.0`, allows repeated query terms to have a meaningful impact on scoring while still applying diminishing returns. The default values are suitable for most applications. Tuning `k1` and `b` is typically required only if your corpus consists of extremely short texts like tweets (decrease `k1` and `b`) or extremely long texts like legal documents (increase `k1` and `b`). To tune these parameters, we recommend an empirical approach: build a set of evals, and choose the parameter values that maximize performance on those evals. --- This page: [/docs/fts.md](https://turbopuffer.com/docs/fts.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Guarantees This document serves as a brief reference of turbopuffer's guarantees: * **Durable Writes.** Writes are committed to object storage upon successful return by turbopuffer's API. * **Consistent Reads.** Queries return the latest data by default but can be [configured](/docs/query) for performance at the cost of read consistency. Most updates are visible immediately since queries usually hit the writing node. [Over 99.8% of queries return consistent data](/docs/query#param-consistency), however staleness of about 100ms can be observed during (rare) scaling/failover operations, and staleness of up to about one hour can be observed after significant writes. Specifically, once a namespace has more than 128MiB of outstanding writes, further writes are not visible until they are indexed and loaded into cache. For small namespaces indexing and cache warming takes tens of seconds; for large namespaces this process can take tens of minutes. * **Atomic Conditional Writes.** [Conditional writes](/docs/write#conditional-writes) evaluate their condition atomically with writing. * **Atomic Batches.** All writes in an upsert are applied simultaneously. * **Branch isolation.** Branched namespaces are point-in-time snapshots that are fully independent after creation. Writes to one namespace are never visible in the other. Deleting either namespace does not affect the other. * **Any node can serve queries for any namespace.** HA does not come as a cost/reliability trade-off. Our HA is the number of query nodes we run. * **Object storage is the only stateful dependency.** This means there is no separate consensus plane that needs to be maintained and scaled independently, simplifying the system's operations and thus reliability. All concurrency control is delegated to object storage. * **Compute-Compute Separation.** Query nodes handle queries and writes to object storage and the write-through cache. All expensive computation happens on separate, auto-scaled indexing nodes. * **Smart Caching.** After a cold query, data is cached on NVMe SSD and frequently accessed namespaces are stored in memory. turbopuffer does not need to load the entire namespace into cache, and then query it. The storage engine is designed to perform small, ranged reads directly to object storage for fast cold queries. [Cache warming hints](/docs/warm-cache) can eliminate user-visible cold query latency in many applications. * **Autoscaling.** Query and indexing nodes automatically scale with demand. Regarding ACID properties: turbopuffer provides Atomicity, Consistency, and Durability. Isolation is not broadly applicable, as general purpose read-write transactions are not supported. However, limited read-write semantics are available through [conditional writes](/docs/write#conditional-writes), [`patch_by_filter`](/docs/write#patch-by-filter), and [`delete_by_filter`](/docs/write#delete-by-filter). Conditional writes evaluate their reads and writes atomically. This prevents concurrency anomalies such as Lost Updates and ensures they behave as if run under Serializable isolation, the strongest isolation level. `patch_by_filter` and `delete_by_filter` execute in two phases: 1. They evaluate their filter against a namespace snapshot to identify matching document ids. 2. They atomically re-evaluate their filter against those matching ids and modify documents that still satisfy the condition. Re-evaluation ensures that documents are never modified that no longer satisfy the condition. However, newly qualifying documents that were inserted or updated between the two phases will be missed. As a result, `patch_by_filter` and `delete_by_filter` behave as if run under Read Committed isolation, similar to `UPDATE ... SET ... WHERE ...` and `DELETE FROM ... WHERE ...` in [PostgreSQL](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED). ## CAP Theorem turbopuffer prioritizes consistency over availability when object storage is unreachable. You can adjust this to favor availability through [query configuration](/docs/query). For more details, see [Tradeoffs](/docs/tradeoffs), [Limits](/docs/limits), and [Architecture](/docs/architecture). --- This page: [/docs/guarantees.md](https://turbopuffer.com/docs/guarantees.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Hybrid Search **Hybrid Search** (Max of vector (768d, ~3GB) and BM25 (~300MB) latency on 1M docs. Strongly consistent.) - warm (1M docs): p50=14ms, p90=18ms, p99=29ms - cold (1M docs): p50=874ms, p90=1214ms, p99=1686ms **Vector Query** (768 dimensions, f16, 10M docs, ~15GB. Strongly consistent.) - warm (10M docs): p50=14ms, p90=17ms, p99=27ms - cold (10M docs): p50=874ms, p90=1214ms, p99=1686ms **Full-Text Search** (BM25, 1M docs, ~300MB. Strongly consistent.) - warm (1M docs): p50=13ms, p90=18ms, p99=29ms - cold (1M docs): p50=316ms, p90=381ms, p99=559ms ``` ┌─{search.py,search.ts}─────────────────────────────────────────────────┐ │ ┌─turbopuffer queries────┐ │ │ │ ┌───────────────────┐ │ │ │ ├─▶│ Vector Query 1 │─┤ │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ │ └───────────────────┘ │ ┌──────┐ │ ┌──────────┐ │ Query Rewriting │ │ ┌───────────────────┐ │ │ Rank │ ┌ ─ ─ ─ ─ ┐ │ │User Query│─┼▶│(Language Model) ─┼─▶│ Vector Query 2 │─┼─▶│ Fuse │──▶ Re-Rank │ └──────────┘ │ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ └───────────────────┘ │ └──────┘ └ ─ ─ ─ ─ ┘ │ │ │ ┌───────────────────┐ │ │ │ ├─▶│ Text Query 1 │─┤ │ │ │ └───────────────────┘ │ │ │ └────────────────────────┘ │ └───────────────────────────────────────────────────────────────────────┘ ``` To improve search quality, multiple strategies can be used together. This is commonly referred to as hybrid search. turbopuffer supports vector search and BM25 full-text search. Combining them produces semantically relevant search results (vectors), as well as results matching specific words or strings (i.e. product SKUs, email addresses, weighing exact keywords highly). Keep search logic in `{search.py, search.ts}`. Use turbopuffer for initial retrieval to narrow millions of results to dozens for rank fusion and re-ranking. To improve search results further, we suggest: - Using a re-ranker (such as [Cohere](https://cohere.com/rerank), [ZeroEntropy](https://docs.zeroentropy.dev/models#rerankers), [MixedBread](https://www.mixedbread.com/docs/stores/search/rerank), or [Voyage](https://docs.voyageai.com/docs/reranker)) - Building a test suite of queries and ideal results, and evaluate NDCG ([blog post](https://softwaredoug.com/blog/2021/02/21/what-is-a-judgment-list)) - Building a query rewriting layer ([LlamaIndex resource](https://docs.llamaindex.ai/en/stable/examples/query_transformations/query_transform_cookbook/)) - Trying various chunking strategies ([LangChain resource](https://docs.langchain.com/oss/javascript/integrations/splitters) [chonkie resource](https://github.com/chonkie-inc/chonkie)) - Trying [contextual retrieval](https://www.anthropic.com/news/contextual-retrieval), or otherwise rewriting the chunks to be embedded - Adding additional multi-modal data to query, e.g. embeddings of the images ([Cohere image model](https://docs.cohere.com/v2/docs/embeddings#image-embeddings), [Voyage image model](https://docs.voyageai.com/docs/multimodal-embeddings)) Choose an embedding provider for the vector side of hybrid search. Pick from the dropdown in the code sample below, or use random vectors to start (don't use in production or for benchmarking). ```python # $ pip install turbopuffer sentence-transformers import os import uuid from typing import List import turbopuffer from sentence_transformers import SentenceTransformer from turbopuffer.types import ID, Row tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"hybrid-example-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) # Local embeddings with BGE -- no API key needed. # Model is downloaded on first run (~130 MB). bge = SentenceTransformer("BAAI/bge-small-en-v1.5") def embed(text: str) -> List[float]: return bge.encode(text).tolist() # Upsert documents with both FTS and vector search capabilities ns.write( upsert_rows=[ { "id": 1, "vector": embed("Muesli: A mix of raw oats, nuts and dried fruit served with cold milk"), "content": "Muesli: A quick mix of raw oats, nuts and dried fruit served with cold milk", }, { "id": 2, "vector": embed("Classic chia seed pudding is a cold breakfast that takes 5 minutes to prepare"), "content": "Classic chia seed pudding is a cold breakfast that takes 5 minutes to prepare", }, { "id": 3, "vector": embed("Overnight oats: Mix oats with milk, refrigerate overnight for a delicious chilled breakfast"), "content": "Overnight oats: Mix oats with milk, refrigerate overnight for a delicious chilled breakfast", }, { "id": 4, "vector": embed("Hot oatmeal is a quick and healthy breakfast"), "content": "Hot oatmeal is a quick and healthy breakfast", }, { "id": 5, "vector": embed("Breakfast sandwich: A little extra prep, but worth it on Sunday mornings!"), "content": "Breakfast sandwich: A little extra prep, but worth it on Sunday mornings!", } ], distance_metric="cosine_distance", schema={"content": {"type": "string", "full_text_search": True}}, ) query = "quick breakfast like oatmeal but cold" print("Ideal:", [1, 2, 3, 4, 5]) response = ns.multi_query( queries=[ { "rank_by": ("vector", "ANN", embed(query)), "limit": 10, "include_attributes": ["content"], }, { "rank_by": ("content", "BM25", query), "limit": 10, "include_attributes": ["content"], }, ] ) vector_result, fts_result = response.results[0].rows, response.results[1].rows print("Vector:", [item.id for item in vector_result]) print("FTS:", [item.id for item in fts_result]) def reciprocal_rank_fusion(result_lists, k = 60): scores = {} all_results = {} for results in result_lists: for rank, item in enumerate(results, start=1): scores[item.id] = scores.get(item.id, 0) + 1.0 / (k + rank) all_results[item.id] = item return [ setattr(all_results[doc_id], '$dist', score) or all_results[doc_id] for doc_id, score in sorted(scores.items(), key=lambda x: x[1], reverse=True) ] fused_results = reciprocal_rank_fusion([vector_result, fts_result]) print("Fused:", [item.id for item in fused_results]) def cohere_rerank_or_unranked(results, query, k = None): if not os.getenv("COHERE_API_KEY"): print("Warning: COHERE_API_KEY not set (https://dashboard.cohere.com/api-keys), returning unranked results") return results try: co = __import__('cohere').Client(os.getenv("COHERE_API_KEY")) reranked = co.rerank(query=query, documents=[r.content for r in results], top_n=k or len(results)).results for r in reranked: results[r.index]['$dist'] = r.relevance_score return [results[r.index] for r in reranked] except (ImportError, AttributeError): print("Warning: cohere package not installed (`pip install cohere`), returning unranked results") return results reranked_results = cohere_rerank_or_unranked(fused_results, query) print("Reranked:", [item.id for item in reranked_results]) ``` --- This page: [/docs/hybrid.md](https://turbopuffer.com/docs/hybrid.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Introduction **100B Vector Search** - p50: 46ms - p90: 61ms - p99: 185ms **Vector Query** (768 dimensions, f16, 10M docs, ~15GB. Strongly consistent.) - warm (10M docs): p50=14ms, p90=17ms, p99=27ms - cold (10M docs): p50=874ms, p90=1214ms, p99=1686ms **Full-Text Search** (BM25, 1M docs, ~300MB. Strongly consistent.) - warm (1M docs): p50=13ms, p90=18ms, p99=29ms - cold (1M docs): p50=316ms, p90=381ms, p99=559ms **Upsert** (Time for the batch to be durably acknowledged by object storage. Documents are immediately available to consistent reads after this.) - Upsert latency (512kb docs): p50=165ms, p90=248ms, p99=850ms ``` ╔═ turbopuffer ════════════════════════════╗ ╔════════════╗ ║ ║░ ║ ║░ ║ ┏━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━┓ ║░ ║ client ║░───API──▶║ ┃ Memory/ ┃────▶┃ Object ┃ ║░ ║ ║░ ║ ┃ SSD Cache ┃ ┃ Storage (S3) ┃ ║░ ╚════════════╝░ ║ ┗━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━┛ ║░ ░░░░░░░░░░░░░░ ║ ║░ ╚══════════════════════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` turbopuffer is a fast, object-storage native search engine. We service documents, , and . Using only object storage for state and NVMe SSD with memory cache for compute, turbopuffer scales low-latency queries to petabyte scale. This makes turbopuffer as fast as in-memory search engines when cached, but far cheaper to run. turbopuffer has many features you'd expect from a database optimized for search, including: * [Filters](/docs/query#param-filters), against an inverted index * [Text search](/docs/query#param-rank_by), ranking and boosting * [Vector search](/docs/query#param-rank_by), >90% recall, combined with any filters, dense & sparse * [Regex search](/docs/query#param-Regex), with fast trigram regex indexes * [Branching](/docs/branching), copy-on-write namespaces * [Encryption](/docs/encryption), your key or your customer's * Multi-tenant (default), single-tenant, or BYOC deployments turbopuffer's [tradeoffs](/docs/tradeoffs) for its excellent economics and scalability are higher write latency (p90=248ms for 512KB upserts) from writing directly to object storage, and occasional cold queries for uncached or [unpinned](/docs/pinning) data (p90=1214ms on 1M documents). These are excellent tradeoffs for search. Using object storage as the sole source of truth enables operations like [branching](/docs/branching) — a copy-on-write clone of any namespace, created in constant time regardless of size, with fully independent reads and writes afterward. turbopuffer is currently focused on first-stage retrieval to efficiently narrow millions of documents (trillions of tokens) down to tens or hundreds documents (thousands of tokens). While it may have fewer features than traditional search engines, this streamlined approach enables higher quality, more maintainable search applications that you can customize in your preferred programming language. See [Hybrid Search](/docs/hybrid-search) to get started. To get started with turbopuffer, see the [quickstart guide](/docs/quickstart). --- This page: [/docs/index.md](https://turbopuffer.com/docs/index.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Ingestion data={ingestionByLanguage} unit="MB/s" /> turbopuffer's ingestion layer is heavily optimized for throughput and can achieve over 150 MB/s. This guide presents several strategies for eliminating bottlenecks in your ingestion pipeline to achieve maximum throughput. ### Ingestion vs. indexing ``` ╔═ turbopuffer ════════════════════════════╗ ╔════════╗ ingestion ║ ┏━━━━━━━━━┓ indexing ┏━━━━━━━━━┓ ║ ║ client ║─────────────────╫─▶LB─▶┃ WAL ┃────────────▶┃ index ┃ ║ ╚════════╝ synchronous ║ ┗━━━━━━━━━┛ async ┗━━━━━━━━━┛ ║ ╚══════════════════════════════════════════╝ ``` Two distinct things happen when you write to turbopuffer: 1. **Ingestion** is a synchronous process driven by the client: a write request appends rows to the namespace's [write-ahead log (WAL)](/docs/architecture) on object storage and returns once the commit is durable. The write is immediately visible to queries. 2. **Indexing** is the asynchronous process that turbopuffer drives in the background: an indexer reads from the WAL and indexes new documents into data structures optimized for search queries. If indexing falls behind ingestion by more than 2 GB, writes will be rejected (HTTP 429) until indexing catches up unless you [disable backpressure](#2-disable-backpressure). We are always working to increase indexing throughput. [Contact us](/contact/support) if indexing is too slow for your workload. Whereas optimizing indexing throughput is generally in turbopuffer's control, optimizing ingestion throughput is generally in your control. The strategies in this guide will help you increase ingestion throughput. ### 1. Batch your writes To minimize the number of roundtrips across the network between your client and the backend, group your writes into batches. Each batch can be up to 512 MB. As a bonus, batching your writes also [lowers your cost by up to 50%](/pricing#faq-write-billing). ### 2. Disable backpressure If your application can tolerate eventually consistent queries, consider setting [`disable_backpressure`](/docs/write#param-disable_backpressure) to `True`. This will prevent the backend from rejecting writes when the 2 GB WAL [limit](/docs/limits) is reached. When the WAL limit is exceeded, eventual consistency queries are still accepted, but strongly consistent queries are rejected with HTTP 429. ```bash # Write with backpressure disabled curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/disable-backpressure-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ {"id": 1, "vector": [0.1, 0.1]} ], "distance_metric": "cosine_distance", "disable_backpressure": true }' # Query with eventual consistency curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/disable-backpressure-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": [ "vector", "ANN", [0.1, 0.1] ], "limit": 10, "consistency": {"level": "eventual"} }' ``` ```python ns = tpuf.namespace(f'disable-backpressure-example-py') # Write with backpressure disabled ns.write( upsert_rows=[ {'id': 1, 'vector': [0.1, 0.1]}, ], distance_metric='cosine_distance', disable_backpressure=True, ) # Query with eventual consistency ns.query( rank_by=('vector', 'ANN', [0.1, 0.1]), limit=10, consistency={'level': 'eventual'}, ) ``` ```typescript const ns = tpuf.namespace(`disable-backpressure-example-ts`); // Write with backpressure disabled await ns.write({ upsert_rows: [ { id: 1, vector: [0.1, 0.1] }, ], distance_metric: "cosine_distance", disable_backpressure: true, }); // Query with eventual consistency await ns.query({ rank_by: ["vector", "ANN", [0.1, 0.1]], limit: 10, consistency: { level: "eventual" }, }); ``` ```go ns := tpuf.Namespace("disable-backpressure-example-go") _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": []float32{0.1, 0.1}, }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, DisableBackpressure: turbopuffer.Bool(true), }, ) if err != nil { panic(err) } _, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAnn("vector", []float32{0.1, 0.1}), Limit: turbopuffer.LimitParam{ Total: 10, }, Consistency: turbopuffer.NamespaceQueryParamsConsistency{ Level: "eventual", }, }, ) if err != nil { panic(err) } } ``` ```java var ns = tpuf.namespace("disable-backpressure-example-java"); // Write with backpressure disabled ns.write( NamespaceWriteParams.builder() .addUpsertRow(Row.builder().put("id", 1).put("vector", List.of(0.1f, 0.1f)).build()) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .disableBackpressure(true) .build() ); // Query with eventual consistency ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.ann("vector", List.of(0.1f, 0.1f))) .limit(10) .consistency( NamespaceQueryParams.Consistency.builder() .level(NamespaceQueryParams.Consistency.Level.EVENTUAL) .build() ) .build() ); } } ``` ```cs var ns = tpuf.Namespace("disable-backpressure-example-csharp"); // Write with backpressure disabled await ns.Write( new NamespaceWriteParams { UpsertRows = [new Row().Set("id", 1).Set("vector", new[] { 0.1f, 0.1f })], DistanceMetric = DistanceMetric.CosineDistance, DisableBackpressure = true, } ); // Query with eventual consistency await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", new[] { 0.1f, 0.1f }), Limit = 10, Consistency = new NamespaceQueryParamsConsistency { Level = NamespaceQueryParamsConsistencyLevel.Eventual, }, } ); ``` ```ruby ns = tpuf.namespace("disable-backpressure-example-rb") # Write with backpressure disabled ns.write( upsert_rows: [ { id: 1, vector: [0.1, 0.1] }, ], distance_metric: "cosine_distance", disable_backpressure: true, ) # Query with eventual consistency ns.query( rank_by: ["vector", "ANN", [0.1, 0.1]], limit: 10, consistency: { level: "eventual" }, ) ``` If your application requires strongly consistent queries, this approach can still be used during the initial ingestion into turbopuffer before turbopuffer is serving queries. For example: 1. Backfill data in a namespace using `disable_backpressure=True` 2. Begin serving production traffic with strong consistency 3. Send future updates using `disable_backpressure=False` As a side effect, letting the unindexed WAL grow larger also lets the indexer work on bigger batches at once, which can increase indexing throughput. Note that you cannot use `disable_backpressure` with `patch_rows`, `patch_columns`, and conditional writes, as these rely on strongly consistent queries. ### 3. Concurrent, not sequential Don't let your process sit idle waiting for API requests to return. A single object storage roundtrip has a p50 of 165ms for a 500 kB batch, so waiting for one request to return before issuing the next is capped at single-digit writes per second. ``` Sequential ┃━━ req1 ━━┫┃━━ req2 ━━┫┃━━ req3 ━━┫┃━━ req4 ━━┫ ≈ 3 writes/s Concurrent ┃━━ req1 ━━┫ ┃━━ req2 ━━┫ ┃━━ req3 ━━┫ ┃━━ req4 ━━┫ ┃━━ req5 ━━┫ ┃━━ req6 ━━┫ ... ``` Limiting concurrency to 2 · NCPUs is a good rule of thumb, but don't be afraid to benchmark to find the optimal concurrency for your setup. ### 4. Use more CPUs Client-side serialization can be a bottleneck with large batches, especially in interpreted languages like Python and TypeScript. You can determine this by looking at the per-thread CPU usage of your ingestion pipeline. If you see one or more threads with high CPU usage, parallelize the work across more CPUs, using whatever primitive your runtime provides. This may require spawning additional processes or containers, or provisioning larger machines. Note that threading in some languages like Ruby/JS/Python won't help here, as threads in these languages aren't multi-core by default. ### 5. Use a bigger box If your ingestion process is CPU-bound, the simplest fix is often to run it on a larger machine. More cores let you run more concurrent writers (see [concurrent writes](#3-concurrent-not-sequential)), and faster cores speed up batch serialization. More memory also helps when building large batches in memory before sending them. For a one-time backfill, consider spinning up a large instance in the same cloud region as your turbopuffer [region](/docs/regions), running the ingest, and tearing it down when done. ### 6. Optimize your schema and index design Careful schema design can improve ingestion and indexing performance by ensuring you only ingest necessary data and avoid building search indexes for attributes you will not search. A few schema tips to speed up throughput: - Mark large attributes as `filterable: false` to avoid building an attribute index - Use the smallest vector precision that still provides acceptable recall - Whenever possible, use UUIDs as IDs for your documents Additionally, consider separating ANN and BM25 index namespaces. If indexing performance suffers on a namespace with both ANN and BM25 indexes, we recommend splitting these indexes into separate namespaces to improve throughput. We're actively working on improving performance for combined ANN and BM25 namespaces, and this temporary workaround will be unnecessary soon. See our [Performance guide](/docs/performance) for more optimization strategies. ### Examples Below are code snippets that optimize ingestion throughput by implementing these strategies with our Python and Go client SDKs. The comments in each snippet describe language-specific details. ```python # $ pip install turbopuffer import asyncio import math import multiprocessing import os import random import time from typing import Any, List, Tuple import turbopuffer MAX_DOCUMENTS = 16384 # 16,384 BATCH_SIZE = 4096 # 4,096 PROCESS_COUNT: int = os.cpu_count() or 1 # set this according to machine specs using os.cpu_count() or similar VECTOR_DIM = 1024 DOCS_PER_PROCESS = MAX_DOCUMENTS // PROCESS_COUNT async def write_rows(batch_num: int, ns: Any, data: List[Any]) -> None: rows = data[batch_num*BATCH_SIZE:(batch_num+1)*BATCH_SIZE] await ns.write( upsert_rows=rows, distance_metric='cosine_distance', # use lower precision vectors when possible schema={ 'vector': {'type': '[1024]f16', 'ann': True}, }, # set disable backpressure to avoid hitting limits when 2GB WAL limit # is reached disable_backpressure=True, ) async def process_items(ns: Any, data: List[Any]) -> None: num_batches = math.ceil(len(data) / BATCH_SIZE) await asyncio.gather(*[ write_rows(batch_num, ns, data) for batch_num in range(num_batches) ]) async def async_worker(data: List[Any]) -> None: # to avoid blocking the CPU while waiting for backend to respond # use the async API to write data print(f"[Process {os.getpid()}] initialised") async with turbopuffer.AsyncTurbopuffer( api_key=os.getenv('TURBOPUFFER_API_KEY'), region="gcp-us-central1", # pick the region closest to where you run your ingestion process ) as tpuf: start_time = time.perf_counter() ns = tpuf.namespace(f'async-vector-multiprocessing-ingest-1024') await process_items(ns, data) elapsed = time.perf_counter() - start_time print(f"[Process {os.getpid()}] Finished in {elapsed:.2f} seconds") def worker(args: Tuple[int, int]) -> None: id_start, id_end = args print(f"[Process {os.getpid()}] generating {id_end - id_start} vectors") # to avoid the overhead of copying data between parent and child processes # generate and/or read vectors in the child process data = [{'id': block_id, 'vector': [random.gauss(0, 1) for _ in range(VECTOR_DIM)]} for block_id in range(id_start, id_end)] try: asyncio.run(async_worker(data)) except Exception as e: raise RuntimeError(f"[Process {os.getpid()}] failed: {type(e).__name__}: {e}") from None if __name__ == '__main__': # total number of documents might yield a remainder when divided by process_count # to make sure last process captures remaining documents chunks = [(i * DOCS_PER_PROCESS, MAX_DOCUMENTS if i == PROCESS_COUNT - 1 else (i + 1) * DOCS_PER_PROCESS) for i in range(PROCESS_COUNT)] with multiprocessing.Pool(PROCESS_COUNT) as pool: pool.map(worker, chunks) ``` ```go package main import ( "context" "fmt" "math/rand" "os" "runtime" "sync" "time" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" "github.com/turbopuffer/turbopuffer-go/v2/packages/param" ) const ( maxDocuments = 16_384 // Batch your writes: group rows into batches up to 512 MB batchSize = 4_096 vectorDim = 1024 ) func loadData() []turbopuffer.RowParam { // Randomly generate some data. // Replace with code to load your actual data. rows := make([]turbopuffer.RowParam, maxDocuments) for i := range rows { vec := make([]float32, vectorDim) for j := range vec { vec[j] = rand.Float32() } rows[i] = turbopuffer.RowParam{ "id": i, "vector": vec, } } return rows } func writeBatch(ctx context.Context, ns turbopuffer.Namespace, batch []turbopuffer.RowParam) error { _, err := ns.Write(ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: batch, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, // Use lower precision vectors when possible Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "vector": {Type: "[1024]f16", Ann: param.Override[turbopuffer.AttributeSchemaConfigAnnParam](true)}, }, // Disable backpressure: disable 2 GiB unindexed WAL cap for bulk ingestion DisableBackpressure: turbopuffer.Bool(true), }) return err } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), option.WithRegion("gcp-us-central1"), ) ns := tpuf.Namespace("async-vector-ingest-test-go") allVectors := loadData() start := time.Now() // Write batches in parallel: up to NCPUs * 2 batches. var wg sync.WaitGroup sem := make(chan struct{}, 2*runtime.NumCPU()) for i := 0; i < len(allVectors); i += batchSize { batch := allVectors[i:min(i+batchSize, len(allVectors))] sem <- struct{}{} // acquire slot wg.Add(1) go func(batch []turbopuffer.RowParam) { defer wg.Done() defer func() { <-sem }() // release slot if err := writeBatch(ctx, ns, batch); err != nil { panic(err) } }(batch) } wg.Wait() fmt.Printf("Total time: %.2f seconds\n", time.Since(start).Seconds()) } ``` --- This page: [/docs/ingestion.md](https://turbopuffer.com/docs/ingestion.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Limits There isn't a limit or performance metric we can't improve by an order of magnitude when prioritized. If you expect to brush up against a limit or you are limited by present performance, [contact us](/contact). | Metric | Observed in production | Production limits (current) | | --- | --- | --- | | Max documents (global) | 4T+ @ 15PB+ | Unlimited[^limits-note-1] | | Max documents (queried simultaneously) | [100B+ @ 10TB](/blog/ann-v3) | Unlimited[^limits-note-2] | | Max documents (per namespace) | 500M+ @ 2TB | 500M @ 2TB[^limits-note-3] | | Max number of namespaces | 250M+ | Unlimited[^limits-note-4] | | Max number of [pinned namespaces](/docs/pinning) | 256 | Contact us for custom | | Max vector columns per namespace[^limits-note-5] | | 2 | | Max dimensions for dense vectors | | 10,752 | | Max total dimensions for sparse vectors | 30,522 | Unlimited | | Max dimensions per sparse vector | | 1,024 | | Max inactive time in cache | hours[^limits-note-6] | Contact us for custom | | Max write throughput (global) | 10M+ writes/s @ 32GB/s | Unlimited[^limits-note-7] | | Max write throughput (per namespace) | 32k+ writes/s @ 64MB/s[^limits-note-8] | 10k writes/s @ 32 MB/s | | Max namespace copy throughput[^limits-note-9] | 72 MB/s | Contact us if bottlenecked | | Number of branches[^limits-note-10] | 10M+ | Unlimited | | Max upsert batch request size | | 512 MB | | Max rows affected by [patch by filter](/docs/write#patch-by-filter) | | 50k[^limits-note-11] | | Max rows affected by [delete by filter](/docs/write#delete-by-filter) | | 5M[^limits-note-12] | | Max ingested, unindexed data[^limits-note-13] | | 2 GB | | Max queries (global) | 25k+ queries/s | Unlimited[^limits-note-14] | | Max queries (per namespace)[^limits-note-15] | 1k+ queries/s | 1k+ queries/s | | Max queries in a [multi-query request](/docs/query#param-queries) | | 16 | | Max concurrent queries per namespace[^limits-note-16] | | 16 (100s of queries/s) | | Max read replicas[^limits-note-17] | 3 | Unlimited | | Vector search recall@10[^limits-note-18] | 90-100% | 90-100% | | Max attribute value size[^limits-note-19] | | 8 MiB | | Max filterable value size[^limits-note-20] | | 4 KiB | | Max document size | | 64 MiB | | Max id size | | 64 bytes | | Max attribute name length[^limits-note-21] | | 128 bytes | | Max attribute names per namespace | | 1,024 | | Max namespace name length[^limits-note-22] | | 128 bytes | | Max full-text query length | 8,192 | 1,024 | | Max [limit.total](/docs/query#param-limit) | 10k | 10k | | Max aggregation groups per query | 10k | 10k | [^limits-note-1]: Per namespace limits still apply, but due to the extreme scalability of object storage, we don't see any problems storing trillions of documents across billions of namespaces. [^limits-note-2]: Requires manually sharding into multiple namespaces (e.g. id % N) to comply with per-namespace limits. Each namespace is limited to 2TB. [^limits-note-3]: The byte size limit is enforced - writes to namespaces exceeding this size will be rejected. The document count limit is a soft limit; performance depends on query complexity. [^limits-note-4]: Each namespace is simply a prefix on object storage, which means it scales to virtually unlimited as object storage providers don't specify a limit. [Architecture](/docs/architecture) for more details [^limits-note-5]: The number of vector columns is fixed at namespace creation time and cannot be changed. Eventually up to 128. [^limits-note-6]: This is simply the current production average cache SSD expiry time. We are currently over-provisioned in cache capacity. In the future, this may change slightly as we optimize the economies of scale. However, consider that our incentive is aligned with yours to keep the cache intelligent, as using object storage for hot reads is expensive. [^limits-note-7]: Writes are processed through a cluster of horizontally scaleable turbopuffer Rust binaries. They write directly to object storage, which scales to virtually unlimited writes. Per namespace limits still apply. Read more on the [architecture page](/docs/architecture) [^limits-note-8]: Limited by indexing performance, see "Max ingested, unindexed data". We are constantly working on improving indexing performance. [^limits-note-9]: Throughput for [copy_from_namespace](/docs/write#param-copy_from_namespace). Cross-region copies may be 20-35% slower depending on distance. See [Cross-Region Backups](/docs/backups) for details. [^limits-note-10]: There are no limits on branching. This means there is no limit on how many children branches a namespace can have (A->B, A->C, A->D,...) nor on the length of chains of branches (A->B, B->C, C->D,...). See the [branching guide](/docs/branching) for details. [^limits-note-11]: Your write will contain a `rows_remaining` field indicating whether any rows were skipped. You can issue a duplicate patch_by_filter request to patch more rows. This limit is there to ensure that indexing and consistent queries can keep up with patches. [^limits-note-12]: Your write will contain a `rows_remaining` field indicating whether any rows were skipped. You can issue a duplicate delete_by_filter request to delete more rows. This limit is there to ensure that indexing and consistent queries can keep up with deletes. [^limits-note-13]: Ingested data is asynchronously indexed (see [architecture docs](/docs/architecture) for details). It is possible to ingest faster than we can index, causing a backlog. If the indexing backlog reaches this limit, upsert requests will return HTTP 429 until the backlog decreases. This ensures queries can be executed without excessive resource use and latency. We continuously improve indexing throughput. You can see the number of unindexed documents by sending a query and examining `.performance.exhaustive_search_count` in the response. [^limits-note-14]: Due to turbopuffer's simple, horizontally scalable architecture with a cluster of Rust binaries pointing to object storage, scaling reads is easy. Per namespace limits still apply. Read more about batching on the [architecture page](/docs/architecture) [^limits-note-15]: Adding read replicas can raise this limit to arbitrarily high values (contact us). Replicas will be added automatically in future versions of turbopuffer. [^limits-note-16]: If this is exceeded, the 17th query waits up to 800ms to start. If it can't claim the semaphore in that window, it will return an HTTP 429. This limit serves to mitigate the noisy neighbour effect, and can be raised by contacting us. Note that query latency interacts with this limit to determine effective max QPS. For 50ms queries, the default limit allows >300 QPS. Eventually consistent queries can be enabled to improve throughput even further. As we improve performance, effective QPS will increase. Aggregate / group-by queries scan the namespace and use more CPU and I/O than typical vector or BM25 queries, so each one consumes 4 slots from this limit instead of 1. Exact-distance (kNN) vector queries are also more CPU and I/O intensive than ANN and consume 2 slots. Adding read replicas can raise this limit to arbitrarily high values (contact us). Replicas will be added automatically in future versions of turbopuffer. [^limits-note-17]: Adding read replicas can raise concurrent queries and QPS to arbitrarily high values (contact us). Replicas will be automatically added in future versions of turbopuffer. For pinned namespaces, see [Pinned Replicas](/docs/pinning#replicas) for details. [^limits-note-18]: This means the top 10 returned from turbopuffer has on average 90-95% of the true top 10. In the future, this is tunable, but currently the configuration is hardcoded for this recall. Vector search is fundamentally about the tradeoff between recall and other attributes like cost and performance. You can read more on our blog about how we monitor recall. [^limits-note-19]: How large any individual attribute value can be. For arrays, this represents the total byte size of the array; there is no limit on the number of elements in an array. [^limits-note-20]: How large any filterable attribute value can be. For arrays, this represents the maximum size of any element; there is no limit on the number of elements in an array. [^limits-note-21]: Attribute names cannot start with $ [^limits-note-22]: Must match `[A-Za-z0-9-_.]{1,128}` --- This page: [/docs/limits.md](https://turbopuffer.com/docs/limits.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Metadata ## View Metadata GET /v1/namespaces/:namespace/metadata Returns metadata about a namespace. **Metadata** (Metadata lookup for a namespace.) - Metadata Latency: p50=11ms, p90=15ms, p99=29ms ## Response **schema** object See the [schema documentation](/docs/write#schema). --- **approx_logical_bytes** integer The approximate number of logical bytes in the namespace. This is a coarse approximation and may change over time as turbopuffer's data representation evolves. When using [`disable_backpressure`](/docs/write#param-disable_backpressure), this metric will not be updated until all data has been indexed. --- **approx_row_count** integer The approximate number of rows in the namespace. When using [`disable_backpressure`](/docs/write#param-disable_backpressure), this metric will not be updated until all data has been indexed. --- **created_at** string The timestamp when the namespace was created, in ISO 8601 format. Example: `"2024-03-15T10:30:45Z"` --- **last_write_at** string The timestamp when the namespace's data was last modified, in ISO 8601 format. Example: `"2024-03-19T09:12:14Z"` --- **updated_at** string The timestamp when the namespace when the namespace's data or schema was last modified, in ISO 8601 format. Example: `"2024-04-16T09:27:32Z"` --- **encryption** object Describes how the namespace is encrypted. Possible values include default server-side encryption and [CMEK](/docs/encryption). Example (default): ```json { "mode": "default" } ``` Example (GCP CMEK): ```json { "mode": "customer-managed", "key_name": "projects/myproject/locations/us-central1/keyRings/EXAMPLE/cryptoKeys/KEYNAME" } ``` Example (AWS CMEK): ```json { "mode": "customer-managed", "key_name": "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" } ``` --- **index** object The state of the [index](/docs/architecture) for the namespace. Contains the following fields: - `status` (string): `updating` or `up-to-date` - `unindexed_bytes` (integer): The number of bytes in the namespace that are in the [write-ahead log](/docs/architecture) but have not yet been indexed. Note that unindexed data is still searched by queries (see [consistency](/docs/query#param-consistency) for details). Only present when `status` is `updating`. --- **pinning** object [Namespace pinning](/docs/pinning) provisions reserved compute for a namespace to provide predictable cost and performance for large namespaces with sustained query volume, with always-warm cache. Only present when the namespace is pinned. Contains the following fields: - `replicas` (integer): The number of read replicas configured for the namespace. Each replica increases read throughput. - `status` (object): Operational status for the pinned namespace. When available, includes the number of `ready_replicas` that are warm and able to serve traffic, along with the average `utilization` of all ready replicas. When `utilization` exceeds 90%, consider increasing replica count. Example: ```json { "replicas": 2, "status": { "ready_replicas": 1, "utilization": 0.73 } } ``` --- **branching** object The state of [branching](/docs/branching) for the namespace. Only present for branched namespaces. Contains the following fields: - `parent` (string): The namespace this was branched from. --- **branching** object The state of [branching](/docs/branching) for the namespace. Only present for branched namespaces. Contains the following fields: - `parent` (string): The namespace this was branched from. ## Example ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v1/namespaces/metadata-curl/metadata \ -X GET --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" # Response payload # { # "schema": { # "id": { # "type": "uint" # }, # "vector": { # "type": "[2]f32", # "ann": { # "distance_metric": "cosine_distance" # } # }, # "my-number": { # "type": "uint", # "filterable": true, # "full_text_search": null # } # }, # "approx_size_bytes": 4398046511104, # "approx_num_rows": 720, # "created_at": "2024-03-15T10:30:45Z", # "updated_at": "2024-04-16T09:27:32Z", # "encryption": { # "sse": true # }, # "index": { # "status": "updating", # "unindexed_bytes": 128 # } # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f"metadata-inspect-example-py") metadata = ns.metadata() print(metadata) # returns a turbopuffer.NamespaceMetadata object ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`metadata-inspect-example-ts`); const metadata = await ns.metadata(); console.log(metadata); // Prints a Turbopuffer.NamespaceMetadata object ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("metadata-inspect-example-go") metadata, err := ns.Metadata(ctx, turbopuffer.NamespaceMetadataParams{}) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(metadata)) // returns a turbopuffer.NamespaceMetadata struct } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class MetadataInspect { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("metadata-inspect-example-java"); Object metadata = ns.metadata(); System.out.println(metadata); // returns a NamespaceMetadata object } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("metadata-inspect-example-csharp"); var metadata = await ns.Metadata(new NamespaceMetadataParams()); Console.WriteLine(metadata); // returns a NamespaceMetadata object ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("metadata-inspect-example-rb") puts ns.metadata # outputs a Turbopuffer::NamespaceMetadata object ``` ## Billing This request is billed as a query that returns zero rows. --- ## Change Metadata PATCH /v1/namespaces/:namespace/metadata Updates metadata configuration for a namespace. Updates the configuration for a namespace. Currently used to configure [namespace pinning](/docs/pinning). ### Request **pinning** object Configuration for [namespace pinning](/docs/pinning), which provisions reserved compute for a namespace to provide predictable cost and performance for large namespaces with sustained query volume, with always-warm cache. Set to `null` to remove pinning from a namespace. Contains the following fields: - `replicas` (integer, optional): The number of read replicas to provision. Defaults to `1`. Each replica runs on its own reserved node, increases read throughput, and multiplies pinning cost. Example (enable pinning): ```json { "pinning": { "replicas": 2 } } ``` Example (use defaults): ```json { "pinning": true } ``` Example (disable pinning): ```json { "pinning": null } ``` ### Response Returns the updated namespace metadata. See [View Metadata](#view-metadata) response fields for details. ### Billing This request is billed as a query that returns zero rows. --- This page: [/docs/metadata.md](https://turbopuffer.com/docs/metadata.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # List namespaces GET /v1/namespaces **List Namespaces** (Listing namespaces with a prefix filter.) - List Namespaces: p50=32ms, p90=41ms, p99=66ms Paginate through your namespaces. Paginate through the list of namespaces, optionally with a given prefix. You can retrieve more information about a specific namespace with the [metadata endpoint](/docs/metadata). This endpoint is available to API keys with list or admin permissions. ## Request **cursor** string optional retrieve the next page of results (pass `next_cursor` from the response payload) --- **prefix** string optional retrieve only namespaces that match the prefix, e.g. `foo` would return `foo` and `foo-bar`. --- **page_size** string default: 100 limit the number of results per page (max of 1000) ## Response **namespaces** array An array of namespace objects. Each namespace object contains: * `id` (string): the namespace identifier Example: ```json [ { "id": "my-namespace" }, { "id": "test-namespace" }, { "id": "production-data" } ] ``` **next_cursor** string A cursor for pagination. Pass this value as the `cursor` parameter in the next request to retrieve the next page of results. Only present when there are more namespaces to retrieve. ## Examples ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v1/namespaces?page_size=50 \ --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" # Response payload # { # "namespaces": [ # { # "id": "03b0fdf8-b1ae-11ee-a548-b121c8275f7a-client_test", # }, # { # "id": "03b0fdf8-b1ae-11ee-a548-b121c8275f7a-hello_world", # }, # ] # "next_cursor": "MDk0ZGY4NjYtYjM1Yi0xMWVlLWI5YzYtMWRiM2IyMzRkNWEzLWhlbGxvX3dvcmxk" # } # # Unlike with the SDKs, you'll need to paginate through the results manually # using `next_cursor`. ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) # List all namespaces namespaces = tpuf.namespaces() for namespace in namespaces: print('Namespace', namespace.id) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); for await (const namespace of tpuf.namespaces()) { console.log("Namespace", namespace.id); } ``` ```go package main import ( "context" "fmt" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespaces := tpuf.NamespacesAutoPaging(ctx, turbopuffer.NamespacesParams{}) for namespaces.Next() { fmt.Println("Namespace:", namespaces.Current().ID) } if err := namespaces.Err(); err != nil { panic(err.Error()) } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; public class Namespaces { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespaces = tpuf.namespaces(); for (var namespace : namespaces.autoPager()) { System.out.println("Namespace: " + namespace.id()); } } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var namespaces = await tpuf.Namespaces(); await foreach (var ns in namespaces.Paginate()) { Console.WriteLine($"Namespace: {ns.ID}"); } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) # List all namespaces tpuf.namespaces.auto_paging_each do |namespace| puts "Namespace #{namespace.id}" end ``` --- This page: [/docs/namespaces.md](https://turbopuffer.com/docs/namespaces.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # API Overview ## Authentication All API calls require authenticating with your API key. You can create and expire tokens in the [dashboard](/dashboard). The HTTP API expects the API key to be formatted as a standard Bearer token and passed in the Authorization header: ```http Authorization: Bearer ``` ## Encoding The API uses JSON encoding for both request and response payloads. ## Compression The API supports standard HTTP compression headers. However, for most workloads, disabling compression offers the best performance. turbopuffer clients are typically CPU constrained, not network bandwidth constrained. The official client libraries disable request and response compression by default. ## Error responses If an error occurs for your request, all endpoints will return a JSON payload in the format: ```json { "status": "error", "error": "an error message" } ``` You may encounter an `HTTP 429` if you query or write too quickly. See [limits](/docs/limits) for more information. ## Asynchronous requests Some long-running operations can run asynchronously rather than holding the connection open until they finish. The official client libraries handle this automatically and transparently — the call will block until the operation finishes. Make sure to [configure the request timeout](#configuring-timeouts) long enough for the operation to complete. In case of a timeout, the operation will still continue server-side. The rest of this section is only relevant if you call the HTTP API directly. Currently supported operations: - [copy_from_namespace](/docs/write#param-copy_from_namespace) - [recall evaluation](/docs/recall) Send the `Prefer: respond-async` header to allow the server to start the operation in the background. The server returns `202 Accepted` with a `Location` header pointing to the operation. Poll that location to check on progress: The response is `{"status": "running"}` until the operation finishes, then carries the result. Note that the `Prefer: respond-async` header is just a hint. The server may return a sync response, for efficiency or load reasons. ### Configuring timeouts ```bash curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/asyncreq-dst-curl \ -X POST --fail-with-body --include \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Prefer: respond-async' \ -H 'Content-Type: application/json' \ -d '{"copy_from_namespace": "asyncreq-src-curl-'"$NONCE"'"}' # Response: # HTTP/1.1 202 Accepted # Preference-Applied: respond-async # Location: /v1/namespaces/asyncreq-dst-curl/operations/tpuf-abc123 # # {"token": "tpuf-abc123"} curl https://gcp-us-central1.turbopuffer.com/v1/namespaces/asyncreq-dst-curl/operations/tpuf-abc123 \ -X GET --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" # Response (while running): # {"status": "running"} # Response (finished successfully): # { # "status": "finished", # "result": { # "success": {"status": "OK", "message": "namespace cloned successfully"} # } # } # Response (operation failed): # { # "status": "finished", # "result": { # "error": { # "status_code": 400, # "detail": {"status": "error", "error": "destination namespace already exists"} # } # } # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer(region='gcp-us-central1') ns = tpuf.namespace(f'asyncreq-dst-py') # Allow up to 30 minutes for the call to complete. ns.copy_from( source_namespace=f'asyncreq-src-py', timeout=30 * 60, ) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({region: "gcp-us-central1"}); const ns = tpuf.namespace(`asyncreq-dst-ts`); // Allow up to 30 minutes for the call to complete. await ns.copyFrom( { source_namespace: `asyncreq-src-ts` }, { timeout: 30 * 60 * 1000 }, ); ``` ```go package main import ( "context" "os" "time" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient(option.WithRegion("gcp-us-central1")) ns := tpuf.Namespace("asyncreq-dst-go") // Allow up to 30 minutes for the call to complete. copyCtx, cancel := context.WithTimeout(ctx, 30*time.Minute) defer cancel() _, err := (&ns).CopyFrom( copyCtx, turbopuffer.NamespaceCopyFromParams{ SourceNamespace: "asyncreq-src-go", }, ) if err != nil { panic(err) } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.time.Duration; import java.util.*; public class AsyncRequest { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder().fromEnv().region("gcp-us-central1").build(); var ns = tpuf.namespace("asyncreq-dst-java"); // Allow up to 30 minutes for the call to complete. ns.copyFrom( NamespaceCopyFromParams.builder() .sourceNamespace("asyncreq-src-java") .build(), RequestOptions.builder().timeout(Duration.ofMinutes(30)).build() ); } } ``` ## Specification The API has a public OpenAPI specification available at: https://github.com/turbopuffer/turbopuffer-openapi --- This page: [/docs/overview.md](https://turbopuffer.com/docs/overview.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Optimizing Performance **Upsert** (Time for the batch to be durably acknowledged by object storage. Documents are immediately available to consistent reads after this.) - Upsert latency (512kb docs): p50=165ms, p90=248ms, p99=850ms **Vector Query** (768 dimensions, f16, 10M docs, ~15GB. Strongly consistent.) - warm (10M docs): p50=14ms, p90=17ms, p99=27ms - cold (10M docs): p50=874ms, p90=1214ms, p99=1686ms **Full-Text Search** (BM25, 1M docs, ~300MB. Strongly consistent.) - warm (1M docs): p50=13ms, p90=18ms, p99=29ms - cold (1M docs): p50=316ms, p90=381ms, p99=559ms turbopuffer is designed to be performant by default, but there are ways to optimize performance further. These suggestions aren't requirements for good performance—rather, they highlight opportunities for improvement when you have the flexibility to choose. For example, while a single namespace with 100M documents works fine, splitting it into 10 namespaces of 10M documents each may yield better query performance if there's a natural way to group the documents. These suggestions apply generally. For specific strategies to optimize ingestion throughput, read our [Ingestion guide](/docs/ingestion). ## Choose the Closest Region Choose the [region][region] closest to your backend. We can't beat the speed of light. If there isn't a region close to us and the latency is paramount, [contact us.](/contact) ## HTTP Connection Reuse Use the same `Turbopuffer` client instance for as many requests as possible. This uses a connection pool behind the scenes to avoid the overhead of a TCP and TLS handshake on every request. ## Use U64 or UUID IDs The smaller the IDs, the faster the puffin'. A UUID encoded as a string is 36 bytes, whereas the [UUID-native type is 16 bytes][schema]. A u64 is even smaller at 8 bytes. ## Inverted Index Attribute values that are filterable are indexed into an inverted index. Inverted indexes means large intersects can be much faster than on a traditional B-Tree index. ## Disable Filtering for Unfiltered Attributes For attributes you never intend to filter on, marking [attributes as `filterable: false`][schema] will improve indexing performance and grant you a 50% discount. For large attribute values, e.g. storing a raw text chunk or image, this can improve performance and cost significantly. ## Use Small Namespaces The rule of thumb is to make the namespaces as small as they can be without having to routinely query more than one at a time. If documents have significantly different schemas, it's also worth splitting them. Don't try to be too clever. Smaller namespaces will be faster to query and index. ## Prewarm Namespaces If your application is latency-sensitive, consider [warming the cache][warm] for the namespace before the user interacts with it (e.g. when they open the search or chat dialog). ## Use Smaller Vectors Smaller vectors will be faster to search, e.g. 512 dimensions will be faster than 1536 dimensions. [f16](/docs/write#param-type) will be faster than f32, and [i8](/docs/write#param-type) faster still. The tradeoff with smaller vectors is typically lower search precision. Consider the cost/performance vs precision tradeoff with your own evals. For models with quantization-aware training ([voyage-4 series][voyage-4], [voyage-context-3][voyage-context-3], [embed-v4][embed-v4], [Qwen3-VL-Embedding-8B][qwen3-vl]), `int8` output matches `f32` precision ([benchmarks][voyage-benchmarks]). Create the namespace with an [`[N]i8`](/docs/write#param-type) vector type and pass the `int8` values directly as JSON integers (in the range `-128` to `127`) . ## Use Branching to Duplicate Namespaces If you're creating copies of namespaces for testing, backups, or code repositories, [branching](/docs/branching) creates a copy-on-write clone in constant time regardless of namespace size. ## Batch Writes If you're writing a lot of documents, consider batching them into fewer writes. This will improve performance and [leverages batch discounts up to 50%][pricing]. Each individual write batch request can be a maximum of 512MB. ## Concurrent Writes If you're writing a lot of documents, consider using multiple processes to write batches in parallel. Especially for single-threaded runtimes like Node.js or Python, this can be a significant performance boost as upserting is generally bottlenecked by serialization and compression. ## Control include_attributes The more data we have to return, the slower it will be. Make sure to only specify the attributes you need. ## Use Eventual Consistency If you need higher query throughput and can tolerate stale results, consider using [eventual consistency](/docs/query#param-consistency) for your queries. ## Pin High QPS Namespaces For sustained high query volumes over a few, large namespaces, consider [namespace pinning](/docs/pinning) to provision reserved compute nodes for more predictable cost and performance with always-warm cache. ## Avoid Large Attributes with Frequent Patches When using [patch][patch] or [patch_by_filter][patch_by_filter], turbopuffer currently reads all attributes of the documents being patched, even those not being modified. If you have large attributes (>10KB), consider storing them in a separate namespace linked by id. For example, if you have chunks with vectors and a large metadata blob that's shared across chunks, store the metadata in a separate namespace keyed by a shared id (e.g. `file_id`). At query time, do a vector search on chunks, then look up the metadata using the unique ids from your results. This way, patches to chunk-specific attributes never touch the large metadata. ## Keep First-Stage Ranking Simple `rank_by` expressions can quickly become quite sophisticated. For best performance, we recommend keeping the first-stage ranking function simple, with only a few attributes being used to compute BM25 scores and/or attribute scores, retrieving in the order of 100 to 1,000 hits, and then applying more sophisticated ranking in the second stage. * **Understand how Glob and Regex filters are optimized.** Under the hood, they use a trigram-based index to quickly narrow down the set of possibly matching candidates. As a general rule of thumb, the more specific the pattern, the better the performance. Anchored patterns (`turbo*` or `*puffer`) are much more specific than unanchored patterns (`*tpuf*`), and thus will perform better. Avoid unspecific patterns like `[a-z]*`, which require a full-table scan. * **Separate ANN and BM25 index namespaces**. If indexing performance suffers on a namespace with both ANN and BM25 indexes, we recommend splitting these indexes into separate namespaces to improve throughput. We're actively working on improving performance for combined ANN and BM25 namespaces, and this temporary workaround will be unnecessary soon. [patch]: /docs/write#param-patch_rows [patch_by_filter]: /docs/write#param-patch_by_filter [warm]: /docs/warm-cache [schema]: /docs/write#schema [region]: /docs/regions [pricing]: /#pricing [voyage-benchmarks]: https://docs.google.com/spreadsheets/d/1qLBWWvN7-4W53BveJgkQiDSoK_j2RYLh5DafDdEOPnc/edit?gid=1834510862#gid=1834510862&range=A11:B14 [voyage-4]: https://blog.voyageai.com/2026/01/15/voyage-4/ [voyage-context-3]: https://blog.voyageai.com/2025/07/23/voyage-context-3/ [embed-v4]: https://cohere.com/blog/embed-4 [qwen3-vl]: https://qwen.ai/blog?id=qwen3-vl-embedding --- This page: [/docs/performance.md](https://turbopuffer.com/docs/performance.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Permissions When a namespace contains documents belonging to multiple users or groups, queries should only return documents the user has access to. Permissions in turbopuffer currently have to be implemented at the user-level with filters, as turbopuffer doesn't provide built-in mechanisms for row/document-level RBAC. ## Recommended approach Store the `user_id` or `group_ids` that have read access directly on each document. At query time, fetch the user's id and groups from your auth layer and pass them as a filter. Generally this approach is more performant than passing document ids in a filter. An array can be up to 8Mib in size so any group and user id identifiers stored on each document have to fit into this [limit](/docs/limits). We store [filterable attributes in an inverted index structure](/docs/query#filtering) that allows us to efficiently filter 10 000s of user ids without performance degradation; the sidebar widget shows representative p90 latency as the number of permission ids in the query grows. To reduce storage costs associated with storing user and group permissions on each document, encode them as uuids. Note that the uuid type needs to be explicitly specified in the schema, otherwise the type will be inferred as a slower and more expensive string type. ```bash # write a few sample documents that are permissioned by group and user_ids # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/permissions-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "upsert_rows": [ { "id": 1, "vector": [1, 1], "content": "changes in the leadership team", "groups": [], "user_ids": [123, 453, 125, 189] }, { "id": 2, "vector": [2, 1], "content": "simon & nikhil - 1:1 notes", "groups": [], "user_ids": [123, 125] }, { "id": 3, "vector": [6, 1], "content": "notes on planned Kubernetes migration", "groups": ["eng"], "user_ids": [96] } ], "schema": { "content": { "type": "string", "full_text_search": true } }, "distance_metric": "cosine_distance" }' # now we can query the data passing in the appropriate permissions # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/permissions-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "rank_by": ["content", "BM25", "notes"], "filters": ["Or", [ ["groups", "Contains", "design"], ["user_ids", "Contains", 96] ]], "limit": 10, "include_attributes": ["content"] }' # {"rows": [{"id": 3, "$dist": 0.9686553, "content": "notes on planned Kubernetes migration"}]} ``` ```python import os import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions api_key=os.getenv('TURBOPUFFER_API_KEY'), ) ns = tpuf.namespace(f'permissions-example-py') # write a few sample documents that are permissioned by group and user_ids ns.write( upsert_rows=[ { 'id': 1, 'vector': [1, 1], 'content': 'changes in the leadership team', 'groups': [], 'user_ids' : [123, 453, 125, 189] }, { 'id': 2, 'vector': [2, 1], 'content': 'simon & nikhil - 1:1 notes', 'groups': [], 'user_ids' : [123, 125] }, { 'id': 3, 'vector': [6, 1], 'content': 'notes on planned Kubernetes migration', 'groups': ['eng'], 'user_ids' : [96] } ], schema={ 'content': { 'type': 'string', 'full_text_search': True } }, distance_metric='cosine_distance' ) # now we can query the data passing in the appropriate permissions result = ns.query( rank_by=('content', 'BM25', 'notes'), filters=('Or', ( ('groups', 'Contains', 'design'), ('user_ids', 'Contains', 96))), limit=10, include_attributes=['content'] ) print(result.rows) # [Row(id=3, vector=None, $dist=0.9686553, content='notes on planned Kubernetes migration')] ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`permissions-example-ts`); // write a few sample documents that are permissioned by group and user_ids await ns.write({ upsert_rows: [ { id: 1, vector: [1, 1], content: "changes in the leadership team", groups: [], user_ids: [123, 453, 125, 189], }, { id: 2, vector: [2, 1], content: "simon & nikhil - 1:1 notes", groups: [], user_ids: [123, 125], }, { id: 3, vector: [6, 1], content: "notes on planned Kubernetes migration", groups: ["eng"], user_ids: [96], }, ], schema: { content: { type: "string", full_text_search: true, }, }, distance_metric: "cosine_distance", }); // now we can query the data passing in the appropriate permissions const result = await ns.query({ rank_by: ["content", "BM25", "notes"], filters: [ "Or", [ ["groups", "Contains", "design"], ["user_ids", "Contains", 96], ], ], limit: 10, include_attributes: ["content"], }); console.log(result.rows); // [{ id: 3, dist: 0.9686553, attributes: { content: 'notes on planned Kubernetes migration' } }] ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() client := turbopuffer.NewClient(option.WithRegion("gcp-us-central1")) // choose best region: https://turbopuffer.com/docs/regions ns := client.Namespace("permissions-example-go") // write a few sample documents that are permissioned by group and user_ids _, err := ns.Write(ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": []float32{1, 1}, "content": "changes in the leadership team", "groups": []string{}, "user_ids": []int{123, 453, 125, 189}, }, { "id": 2, "vector": []float32{2, 1}, "content": "simon & nikhil - 1:1 notes", "groups": []string{}, "user_ids": []int{123, 125}, }, { "id": 3, "vector": []float32{6, 1}, "content": "notes on planned Kubernetes migration", "groups": []string{"eng"}, "user_ids": []int{96}, }, }, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "content": { Type: turbopuffer.AttributeType("string"), FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, }) if err != nil { panic(err) } // now we can query the data passing in the appropriate permissions result, err := ns.Query(ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByTextBM25("content", "notes"), Limit: turbopuffer.LimitParam{ Total: 10, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{StringArray: []string{"content"}}, Filters: turbopuffer.NewFilterOr([]turbopuffer.Filter{ turbopuffer.NewFilterContains("groups", "design"), turbopuffer.NewFilterContains("user_ids", 96), }), }) if err != nil { panic(err) } fmt.Println(result.Rows) // [map[id:3 $dist:0.9686553 content:notes on planned Kubernetes migration]] } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.TurbopufferOkHttpClient; import com.turbopuffer.models.namespaces.AttributeSchemaConfig; import com.turbopuffer.models.namespaces.DistanceMetric; import com.turbopuffer.models.namespaces.Filter; import com.turbopuffer.models.namespaces.NamespaceQueryParams; import com.turbopuffer.models.namespaces.NamespaceWriteParams; import com.turbopuffer.models.namespaces.RankByText; import com.turbopuffer.models.namespaces.Row; import com.turbopuffer.models.namespaces.Schema; import java.util.Arrays; import java.util.List; public class Permissions { public static void main(String[] args) { var client = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = client.namespace("permissions-example-java"); // write a few sample documents that are permissioned by group and user_ids ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("vector", Arrays.asList(1.0, 1.0)) .put("content", "changes in the leadership team") .put("groups", List.of()) .put("user_ids", Arrays.asList(123, 453, 125, 189)) .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("vector", Arrays.asList(2.0, 1.0)) .put("content", "simon & nikhil - 1:1 notes") .put("groups", List.of()) .put("user_ids", Arrays.asList(123, 125)) .build() ) .addUpsertRow( Row.builder() .put("id", 3) .put("vector", Arrays.asList(6.0, 1.0)) .put("content", "notes on planned Kubernetes migration") .put("groups", List.of("eng")) .put("user_ids", List.of(96)) .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .schema( Schema.builder() .put( "content", AttributeSchemaConfig.builder().type("string").fullTextSearch(true).build() ) .build() ) .build() ); // now we can query the data passing in the appropriate permissions var result = ns.query( NamespaceQueryParams.builder() .rankBy(RankByText.bm25("content", "notes")) .filters(Filter.or(Filter.contains("groups", "design"), Filter.contains("user_ids", 96))) .limit(10) .includeAttributes(List.of("content")) .build() ); System.out.println(result.rows()); // [Row{id=3, $dist=0.9686553, content=notes on planned Kubernetes migration}] } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("permissions-example-csharp"); // write a few sample documents that are permissioned by group and user_ids await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("vector", new[] { 1.0f, 1.0f }) .Set("content", "changes in the leadership team") .Set("groups", Array.Empty()) .Set("user_ids", new[] { 123, 453, 125, 189 }), new Row() .Set("id", 2) .Set("vector", new[] { 2.0f, 1.0f }) .Set("content", "simon & nikhil - 1:1 notes") .Set("groups", Array.Empty()) .Set("user_ids", new[] { 123, 125 }), new Row() .Set("id", 3) .Set("vector", new[] { 6.0f, 1.0f }) .Set("content", "notes on planned Kubernetes migration") .Set("groups", new[] { "eng" }) .Set("user_ids", new[] { 96 }), ], DistanceMetric = DistanceMetric.CosineDistance, Schema = new Dictionary { ["content"] = new AttributeSchemaConfig { Type = "string", FullTextSearch = true, }, }, } ); // now we can query the data passing in the appropriate permissions var result = await ns.Query( new NamespaceQueryParams { RankBy = RankByText.BM25("content", "notes"), Filters = Filter.Or( Filter.Contains("groups", "design"), Filter.Contains("user_ids", 96) ), Limit = 10, IncludeAttributes = new List { "content" }, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } // {"$dist": 0.9686553, "id": 3, "content": "notes on planned Kubernetes migration"} ``` ```ruby require "turbopuffer" client = Turbopuffer::Client.new(region: "gcp-us-central1") # choose best region: https://turbopuffer.com/docs/regions ns = client.namespace("permissions-example-rb") # write a few sample documents that are permissioned by group and user_ids ns.write( upsert_rows: [ { id: 1, vector: [1, 1], content: "changes in the leadership team", groups: [], user_ids: [123, 453, 125, 189], }, { id: 2, vector: [2, 1], content: "simon & nikhil - 1:1 notes", groups: [], user_ids: [123, 125], }, { id: 3, vector: [6, 1], content: "notes on planned Kubernetes migration", groups: ["eng"], user_ids: [96], }, ], schema: { content: { type: "string", full_text_search: true, }, }, distance_metric: Turbopuffer::DistanceMetric::COSINE_DISTANCE, ) # now we can query the data passing in the appropriate permissions result = ns.query( rank_by: ["content", "BM25", "notes"], filters: ["Or", [ ["groups", "Contains", "design"], ["user_ids", "Contains", 96], ]], limit: 10, include_attributes: ["content"], ) puts result.rows # [{id: 3, $dist: 0.9686553, content: "notes on planned Kubernetes migration"}] ``` --- This page: [/docs/permissions.md](https://turbopuffer.com/docs/permissions.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Namespace Pinning **100B Vector Search** - p50: 46ms - p90: 61ms - p99: 185ms **Pin if:** the namespace is large, traffic is sustained, cold queries would hurt, or you want GB-hour instead of per-query pricing. **Rule of thumb:** start evaluating pinning around `>16 GB` and `>10 QPS`. ``` ╔══turbopuffer region═════╗ ╔═══Object Storage═════════════════╗ ║ ┌────────────────┐ ║░ ║ ┏━━Indexing Queue━━━━━━━━━━━━━━┓ ║░ ║ ┌─▶│ ./tpuf query │ ║░ ║ ┃■■■■■■■■■ ┃ ║░ ┌──╩─┐ │ └────────────────┘ ║░ ║ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║░ ╔══════════╗ │ │ │ ┌────────────────┐ ║░ ║ ┏━/{org_id}/{namespace}━━━━━━━━┓ ║░ ║ Client ║───▶│ LB │─┼─▶│ ./tpuf query │─╬─────▶║ ┃ ┏━/wal━━━━━━━━━━━━━━━━━━━━━┓ ┃ ║░ ╚══════════╝░ │ │ │ └────────────────┘ ║░ ║ ┃ ┃■■■■■■■■■■■■■■■◈◈◈◈ ┃ ┃ ║░ ░░░░░░░░░░░░ └──╦─┘ │ ┌────────────────┐ ║░ ║ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ║░ ║ │─▶│ ./tpuf query │ ║░ ║ ┃ ┏━/index━━━━━━━━━━━━━━━━━━━┓ ┃ ║░ ║ │ │ [pin:org1/nsA] │ ║░ ║ ┃ ┃■■■■■■■■■■■■■■■ ┃ ┃ ║░ ║ │ └────────────────┘ ║░ ║ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ ║░ ║ │ ┌────────────────┐ ║░ ║ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ║░ ║ └─▶│ ./tpuf query │ ║░ ╚══════════════════════════════════╝░ ║ │ [pin:org2/nsY] │ ║░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ║ └────────────────┘ ║░ ╚═════════════════════════╝░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` By default, namespaces run on turbopuffer's shared, multi-tenant query infrastructure. That is the right choice for most workloads. **Namespace pinning** reserves compute and NVMe SSD cache for a specific namespace. Once pinned, that namespace's queries run on those reserved resources, so its data stays hot, cost and performance stay predictable, and sustained high query volume is often much cheaper. ## Multi-tenant vs. pinned * **Multi-tenant** (default): Shared compute and cache. This is the simplest and most cost-effective option for most namespaces. Smart caching adapts to traffic patterns to minimize query latency. For spiky traffic, you can [warm the cache](/docs/warm-cache) to reduce cold starts. * **Pinned**: Reserved compute and NVMe SSD cache for one namespace. This keeps the hot path warm and gives large, busy namespaces more predictable throughput, latency, and cost. Pinning also changes billing: instead of per-query (**TB Queried**) pricing, pinned namespaces are billed by **GB-hours** based on namespace size and how long the namespace stays pinned. That means the effective cost per query goes down as query volume goes up, with a break-even point typically around 10 queries per second. ## When to use pinning Pinning is a good fit when: * **You run sustained high query volume** on a namespace, where **GB-hours** are cheaper than paying per query. * **You want predictable query latency** on a namespace, where occasional cold queries would hurt your product. * **You want predictable cost** on a namespace, where **GB-hours** are easier to forecast than per-query **TB Queried**. For many small or naturally sharded namespaces, the default multi-tenant path is still the best choice. As a rule of thumb, pinning is worth evaluating when a large (>16 GB) namespace sustains above 10 queries per second and you want more predictable cost and performance. ## How it works 1. You turn pinning on for an existing namespace via the [metadata API](/docs/metadata#change-metadata). 2. turbopuffer loads the namespace into the SSD cache of reserved [query nodes](/docs/concepts#query-and-indexing-nodes). 3. All queries for that namespace route to those query nodes. 4. The namespace stays hot on those reserved SSDs for as long as it stays pinned. Pinning usually takes less than 30 minutes. During that time, queries continue to work on the existing path with no downtime. Pinning does not change durability: data remains stored durably in object storage whether pinned or not. ### Replicas Replicas increase read throughput. Each replica runs on its own reserved [query node](/docs/concepts#query-and-indexing-nodes), and reads are load-balanced across them. Throughput scales linearly with replica count. A single replica can handle between 100 and 1000 QPS, depending on query shape, filters, and namespace size. Filtered vector and full-text search queries fall in the middle of this range. To decide when to add replicas, monitor `pinning.status.utilization` on `GET /v1/namespaces/:namespace/metadata` (see [Metadata](/docs/metadata#view-metadata)). If utilization stays high or if queries return `HTTP 429 (Too Many Requests)` errors, add more replicas to increase read capacity. Replicas do not currently autoscale for a pinned namespace. Support for this is planned. Replicas also improve fault tolerance. If a replica hits a hardware failure, turbopuffer will fail over and rewarm a new replica. With multiple replicas, this is less likely to result in cold query latency reaching your application. If you are okay with cold latency for a few minutes during a cloud hardware failure, you do not need multiple replicas. ## Pricing Pinned namespaces are billed by **GB-hours**: `namespace size (GB) × replicas × hours pinned` Queries served by a pinned namespace are not subject to **TB Queried** usage-based pricing. At sustained query volume, this often makes individual queries much cheaper than per-query pricing. Exact break-even depends on your workload. Billing has a floor of **64 GB** and **10 minutes**. A pinned namespace smaller than 64 GB is billed as 64 GB, and a namespace pinned for less than 10 minutes is billed for 10 minutes. Use the calculator below to compare multi-tenant and pinned pricing based on namespace size and QPS. ## Configuration Configure pinning with `PATCH /v1/namespaces/:namespace/metadata` (see [Metadata](/docs/metadata#change-metadata)). Set `pinning` to `true` to pin with default settings and 1 replica. Set `pinning` to `null` to unpin. Inspect current settings with `GET /v1/namespaces/:namespace/metadata`. **Example: Enable pinning with 2 replicas** ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v1/namespaces/pinning-enable-example-curl/metadata \ -X PATCH --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "pinning": { "replicas": 2 } }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'pinning-enable-example-py') ns.update_metadata( pinning={ 'replicas': 2, } ) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`pinning-enable-example-ts`); await ns.updateMetadata({ pinning: { replicas: 2, }, }); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" "github.com/turbopuffer/turbopuffer-go/v2/packages/param" ) func main() { ctx := context.Background() client := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := client.Namespace("pinning-enable-example-go") _, err = ns.UpdateMetadata(ctx, turbopuffer.NamespaceUpdateMetadataParams{ NamespaceMetadataPatch: turbopuffer.NamespaceMetadataPatchParam{ Pinning: turbopuffer.PinningConfigParam{ Replicas: turbopuffer.Int(2), }, }, }) if err != nil { panic(err) } fmt.Println("Pinning enabled") } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class EnablePinning { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("pinning-enable-example-java"); ns.updateMetadata( NamespaceMetadataPatch.builder().pinning(PinningConfig.builder().replicas(2L).build()).build() ); } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("pinning-enable-example-csharp"); await ns.UpdateMetadata( new NamespaceUpdateMetadataParams { Pinning = new PinningConfig { Replicas = 2 } } ); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("pinning-enable-example-rb") ns.update_metadata(pinning: { replicas: 2 }) ``` **Example: Disable pinning** ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v1/namespaces/pinning-disable-example-curl/metadata \ -X PATCH --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "pinning": null }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'pinning-disable-example-py') ns.update_metadata(pinning=None) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`pinning-disable-example-ts`); await ns.updateMetadata({ pinning: null, }); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" "github.com/turbopuffer/turbopuffer-go/v2/packages/param" ) func main() { ctx := context.Background() client := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := client.Namespace("pinning-disable-example-go") _, err = ns.UpdateMetadata(ctx, turbopuffer.NamespaceUpdateMetadataParams{ NamespaceMetadataPatch: turbopuffer.NamespaceMetadataPatchParam{ Pinning: param.NullStruct[turbopuffer.PinningConfigParam](), }, }) if err != nil { panic(err) } fmt.Println("Pinning disabled") } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class DisablePinning { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("pinning-disable-example-java"); ns.updateMetadata(NamespaceMetadataPatch.builder().pinning(false).build()); } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("pinning-disable-example-csharp"); await ns.UpdateMetadata(new NamespaceUpdateMetadataParams { Pinning = false }); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("pinning-disable-example-rb") ns.update_metadata(pinning: nil) ``` --- This page: [/docs/pinning.md](https://turbopuffer.com/docs/pinning.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Pricing Changelog **Last updated:** April 3, 2026 This page tracks pricing changes over time. If you need help estimating impact for your workload, [contact us](/contact/sales). For more details on how your turbopuffer bill is calculated, see our [pricing page](/pricing). ## 2026 ### April 2026 Introduced [namespace pinning](/docs/pinning), which bills pinned namespaces in GB-hours instead of per-query `TB Queried` pricing. Pinning cost scales with namespace size, replica count, and time pinned, with minimums of 64 GB and 10 minutes. ### March 2026 Pricing for namespaces with [multiple vector columns](/docs/write#multiple-vector-columns): - Filterable attributes are billed once per vector column for both writes and storage, reflecting the cost of maintaining indexes across multiple ANN indexes - Non-filterable attributes are billed once regardless of the number of vector columns ### February 2026 Query pricing for the largest namespaces reduced by up to 94%: - Base queried data rate decreased from $5/PB to $1/PB - 80% marginal discount when queried data size is between 32 GB and 128 GB - 96% marginal discount when queried data size is greater than 128 GB - Minimum billable data per query increased from 256 MB to 1.28 GB ## 2025 ### July 2025 Query pricing for large namespaces reduced by up to 80%: - 80% marginal discount on bytes queried over 32 GB, per query ## 2024 ### September 2024 Introduced `copy_from_namespace`, allowing data to be copied between namespaces at a 50% discount on write costs. --- This page: [/docs/pricing-log.md](https://turbopuffer.com/docs/pricing-log.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Private Networking ``` ┌─────your VPC───────────────┐ ┌─────tpuf VPC───────────────┐ │ │░ │ │░ │ ┌─────────┐ ┌─────────┐ │░ │ ┌─────────┐ ┌─────────┐ │░ │ │ client │ │ client │ ◀──┼──────────────────┼──▶ │ storage │ │ compute │ │░ │ └─────────┘ └─────────┘ │░ PrivateLink/ │ └─────────┘ └─────────┘ │░ │ │░ PSC │ │░ └────────────────────────────┘░ └────────────────────────────┘░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ``` turbopuffer supports private network connections between your VPC and our [multi-tenant regions](/docs/regions). * AWS regions use [AWS PrivateLink](https://aws.amazon.com/privatelink/) * GCP regions use [GCP Private Service Connect](https://cloud.google.com/vpc/docs/private-service-connect) Private network connection across cloud providers (e.g. AWS => GCP) are not supported. [Contact us](/contact/support) if you need this. ## Enforcement By default, even after establishing a private network connection to a region, API requests for your organization will still be permitted via the public endpoint for the region. Upon request, turbopuffer can enforce that all API requests for an organization are made via your private endpoints. ## Pricing * Private networking is only available on the [enterprise plan](/pricing). * There are no usage-based fees for private network endpoints. ## Setup 1. Provide [turbopuffer support](/contact/support) with: - Your AWS account id or your GCP project id - The [region](/docs/regions) you want to establish a private connection to 2. Wait for turbopuffer to authorize connections from your cloud account 3. Establish a private network connection to the service name provided by turbopuffer support - **AWS:** [Create an interface VPC endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html) - **GCP:** [Create a Private Service Connect endpoint](https://cloud.google.com/vpc/docs/create-manage-private-service-connect-interfaces) 4. Set the `base_url` in your client to the private endpoint for your region (see table below) | Region | Private Endpoint | | --- | --- | | aws-ap-southeast-2 | https://privatelink.aws-ap-southeast-2.turbopuffer.com | | aws-ca-central-1 | https://privatelink.aws-ca-central-1.turbopuffer.com | | aws-eu-central-1 | https://privatelink.aws-eu-central-1.turbopuffer.com | | aws-eu-west-1 | https://privatelink.aws-eu-west-1.turbopuffer.com | | aws-eu-west-2 | https://privatelink.aws-eu-west-2.turbopuffer.com | | aws-us-east-1 | https://privatelink.aws-us-east-1.turbopuffer.com | | aws-us-east-2 | https://privatelink.aws-us-east-2.turbopuffer.com | | aws-us-west-2 | https://privatelink.aws-us-west-2.turbopuffer.com | | aws-ap-south-1 | https://privatelink.aws-ap-south-1.turbopuffer.com | | aws-sa-east-1 | https://privatelink.aws-sa-east-1.turbopuffer.com | | gcp-us-central1 | https://.psc.gcp-us-central1.turbopuffer.com | | gcp-us-east1 | https://.psc.gcp-us-east1.turbopuffer.com | | gcp-us-west1 | https://.psc.gcp-us-west1.turbopuffer.com | | gcp-us-east4 | https://.psc.gcp-us-east4.turbopuffer.com | | gcp-northamerica-northeast2 | https://.psc.gcp-northamerica-northeast2.turbopuffer.com | | gcp-europe-west3 | https://.psc.gcp-europe-west3.turbopuffer.com | | gcp-europe-west1 | https://.psc.gcp-europe-west1.turbopuffer.com | | gcp-asia-southeast1 | https://.psc.gcp-asia-southeast1.turbopuffer.com | | gcp-asia-northeast3 | https://.psc.gcp-asia-northeast3.turbopuffer.com | --- This page: [/docs/private-networking.md](https://turbopuffer.com/docs/private-networking.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Query documents POST /v2/namespaces/:namespace/query **Vector Query** (768 dimensions, f16, 10M docs, ~15GB. Strongly consistent.) - warm (10M docs): p50=14ms, p90=17ms, p99=27ms - cold (10M docs): p50=874ms, p90=1214ms, p99=1686ms **Full-Text Search** (BM25, 1M docs, ~300MB. Strongly consistent.) - warm (1M docs): p50=13ms, p90=18ms, p99=29ms - cold (1M docs): p50=316ms, p90=381ms, p99=559ms Query, filter, full-text search and vector search documents. A query retrieves documents in a single [namespace](/docs/write), returning the ordered or highest-ranked documents that match the query's filters. turbopuffer supports the following types of queries: * [Vector search](#vector-search): find the documents closest to a query vector using approximate nearest neighbor (ANN) * [kNN (exact search)](#knn-exact-search): find the exact nearest neighbors over a filtered subset * [Full-text search](#full-text-search): find documents with the highest [BM25 score](https://en.wikipedia.org/wiki/Okapi_BM25), a classic text search algorithm that considers query term frequency and document length * [Sparse vector search](#sparse-vector-search): find the documents closest to a query sparse vector * [Ordering by attributes](#ordering-by-attributes): find all documents matching filters in order of an attribute * [Lookups](#lookups): find all documents matching filters when order isn't important * [Aggregations](#aggregations): aggregate attribute values across all documents matching filters * [Grouped aggregations](#group-by): aggregate while grouping by one or more attributes * [Multi-queries](#multi-queries): send multiple queries to the same namespace used for hybrid searches. turbopuffer is fast by default. See [Performance](/docs/performance) for how you can influence performance. ## Request **rank_by** array required unless aggregate_by is set How to rank the documents in the namespace. Supported ranking functions: * [ANN](#vector-search) ("approximate nearest neighbor") * [kNN](#knn-exact-search) ("exact nearest neighbor", requires filters) * [BM25](#full-text-search) (combine with [Sum](#fts-operators), [Max](#fts-operators)) * [SparseKNN](#sparse-vector-search) (sparse vector search) * [Order by attribute](#ordering-by-attributes) Documents with a score of zero are excluded from results. For [hybrid search](/docs/hybrid-search), you can use [multi-queries](#multi-queries) (e.g. BM25 + vector). Example (ANN): ```json [ "vector", "ANN", [0.1, 0.2, 0.3, ..., 76.8] ] ``` Example (kNN): ```json // Requires filters [ "vector", "kNN", [0.1, 0.2, 0.3, ..., 76.8] ] ``` Example (BM25): ```json [ "text", "BM25", "fox jumping" ] ``` Example (SparseKNN): ```json [ "sparse_vector", "SparseKNN", { "dim0": 0.1, "dimt1": 0.2, ... } ] ``` Example (order by attribute): ```json [ "timestamp", "desc" ] ``` Example (weighted BM25): ```json [ "Sum", [ ["Product", 2, ["title", "BM25", "fox jumping"]], ["content", "BM25", "fox jumping"] ] ] ``` --- **top_k** number Alias for [`limit.total`](#param-limit). Maximum: 10,000 --- **filters** array optional Exact filters for attributes to refine search results for. Think of it as a SQL WHERE clause. See [Filtering Parameters](#filtering-parameters) below for details. When combined with a vector, the query planner will automatically combine the attribute index and the approximate nearest neighbor index for best performance and recall. See our post on [Native Filtering](/blog/native-filtering) for details. For the best performance, separate documents into namespaces instead of filtering where possible. See also [Performance](/docs/performance). Example: ```json [ "And", [ ["id", "Gte", 1000], [ "permissions", "ContainsAny", [ "3d7a7296-3d6a-4796-8fb0-f90406b1f621", "92ef7c95-a212-43a4-ae4e-0ebc96a65764" ] ] ] ] ``` --- **include_attributes** array[string] | boolean default: id List of attribute names to return in the response. Can be set to `true` to return all attributes. Return only the ones you need for best performance. --- **exclude_attributes** array[string] List of attribute names to exclude from the response. All other attributes will be included in the response. Exclude any attributes you don't need for best performance. Unlike `include_attributes`, attributes that do not exist in the schema are silently ignored. Cannot be specified with [include_attributes](#param-include_attributes). Example: ```json [ "vector", "big_attribute" ] ``` --- **limit** number | object required Limits the number of documents returned. Can be a number to apply a total limit, or an object with the following fields: * `total` (number, required): limits the total number of documents returned Maximum: 10,000 * `per` (object, optional): limits the number of documents with the same value for a set of attributes (the "limit key") that can appear in the results. * `attributes` (string array): the attributes to include in the limit key * `limit` (number): the maximum number of documents to return for each value of the limit key `per` is only supported for [order by attribute](#ordering-by-attributes) queries. Support for BM25 and ANN queries is on our roadmap. Example (simple total): ```json { "limit": 10 } ``` Example (limit per category and size): ```json { "limit": { "per": { "attributes": ["category", "size"], "limit": 10 }, "total": 10 } } ``` See [Diversification](#diversification) below for details. --- **aggregate_by** object required unless rank_by is set [Aggregations](#aggregations) to compute over all documents in the namespace that match the [filters](#param-filters). Cannot be specified with [rank_by](#param-rank_by) or [include_attributes](#param-include_attributes). Each entry in the object maps a label for the aggregation to an aggregate function. Supported aggregate functions: * `["Count"]`: counts the number of documents. * `["Sum", "attribute_name"]`: sums the values of the specified scalar numeric attribute (supports `int`, `uint`, `float`) Example: ```json { "aggregate_by": { "my_count": ["Count"] } } ``` --- **group_by** array Only valid when [`aggregate_by`](#param-aggregate_by) is set. Groups documents by the specified attributes or expressions (the "group key") before computing aggregates. Aggregates are computed separately for each group. Up to [`limit.total`](#param-limit) groups are returned, ordered by group key. Example: ```json { "aggregate_by": { "count_by_color_and_size": ["Count"] }, "group_by": ["color", "size"] } ``` --- **queries** array Send an array of query objects to be executed simultaneously and atomically. Up to 16 queries can be sent per request. Each subquery will count against the [concurrent query limit](/docs/limits) for the namespace. If you need a higher limit, please [contact us](/contact). The provided array should consist of query objects, including every field except for `vector_encoding` or `consistency`, which should be set on the root object. The `queries` field is mutually exclusive with other query object fields. A request can contain either a multi-query or an ordinary query. --- **rerank_by** array optional Combine the rows returned by a [multi-query](#multi-queries) into a single ranked list. Supported reranking functions: * [RRF](#reciprocal-rank-fusion) (reciprocal rank fusion) A single list of up to [`limit.total`](#param-limit) results is returned. Example (RRF): ```json ["RRF"] ``` Example (RRF with config): ```json ["RRF", { "rank_constant": 10 }] ``` --- **vector_encoding** string default: float The encoding to use for the vectors in the response. The supported encodings are `float` and `base64`. If `float`, vectors are returned as arrays of numbers. If `base64`, vectors are returned as base64-encoded strings representing the vectors serialized in little-endian float32 binary format. This parameter has no effect if no vector attributes are included in the response (see the [include_attributes](#param-include_attributes) parameter). --- **consistency** object default: {'level': 'strong'} Controls the consistency level for the query. This determines whether the cache is updated and how much recently written data is included in query results. **Strong consistency (default):** `{"level": "strong"}` Searches all unindexed writes and updates the cache. This ensures the query includes all data written before the query started, providing the strongest consistency guarantees. **Eventual consistency:** `{"level": "eventual"}` Searches at most 128MiB of unindexed writes. Use when you need higher query throughput and can tolerate stale results. Most updates are visible immediately since queries usually hit the writing node. [Over 99.8% of queries return consistent data](/docs/query#param-consistency), however staleness of about 100ms can be observed during (rare) scaling/failover operations, and staleness of up to about one hour can be observed after significant writes. Specifically, once a namespace has more than 128MiB of outstanding writes, further writes are not visible until they are indexed and loaded into cache. For small namespaces indexing and cache warming takes tens of seconds; for large namespaces this process can take tens of minutes. ## Response **rows** array An array of the [`limit.total`](#param-limit) documents that matched the query, ordered by the ranking function. Only present if [rank_by](#param-rank_by) is specified. Each document is an object containing the [requested attributes](#param-include_attributes). The `id` attribute is always included. The special attribute `$dist` is set to the ranking function's score for the document (distance from the query vector for `ANN`; BM25 score for `BM25`; omitted when ordering by an attribute). Example: ```json [ { "$dist": 1.7, "id": 8, "extra_attr": "puffer" }, { "$dist": 3.1, "id": 20, "extra_attr": "fish" } ] ``` **results** array An array of response objects containing the results for each sub-query of a [multi-query](#multi-queries) request, the result objects are returned in the same order as the queries. Example: ```json [ { "rows": [ { "$dist": 0.0, "id": 0 } ] }, { "aggregations": { "my_count_of_ids": 42 } } ] ``` **aggregations** object An object mapping the label for each [requested aggregation](#param-aggregate_by) to the computed value. Only present if [aggregate_by](#param-aggregate_by) is specified but [group_by](#param-group_by) is **not** specified. Example: ```json { "my_count_of_ids": 42 } ``` **aggregation_groups** array An array of objects, one for each aggregation group, containing the [group key](#param-group_by) and the computed value of each [requested aggregation](#param-aggregate_by). Only present if both [aggregate_by](#param-aggregate_by) and [group_by](#param-group_by) are specified. Example: ```json // Sorted by group key. No more than limit.total groups returned. [ { "color": "blue", "size": "small", "my_grouped_count": 2 }, { "color": "blue", "size": "medium", "my_grouped_count": 7 }, { "color": "red", "size": "small", "my_grouped_count": 4 } ] ``` **billing** object The billable resources consumed by the query. The object contains the following fields: * `billable_logical_bytes_queried` (uint): the number of logical bytes processed by the query * `billable_logical_bytes_returned` (uint): the number of logical bytes returned by the query **performance** object The performance metrics for the query. The object currently contains the following fields, but these fields may change name, type, or meaning in the future: * `cache_hit_ratio` (float): the ratio of cache hits to total cache lookups * `cache_temperature` (string): a qualitative description of the cache hit ratio (`hot`, `warm`, or `cold`) * `server_total_ms` (uint): request time measured on the server, including time spent waiting for other queries to complete if the namespace was at its [concurrency limit](/docs/limits) * `query_execution_ms` (uint): request time measured on the server, excluding time spent waiting due to the namespace concurrency limit * `exhaustive_search_count` (uint): the number of unindexed documents processed by the query * `approx_namespace_size` (uint): the approximate number of documents in the namespace * `last_included_write_at` (string): the timestamp of the last write operation that the query observed [Contact the turbopuffer team](/contact) if you need help interpreting these metrics. ## Examples ### Vector Search The query vector must have the same dimensionality as the vector column being queried. ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-vector-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": [ "vector", "ANN", [0.1, 0.1] ], "limit": 10 }' # Response payload # { # "rows": [ # { "$dist": 0.0, "id": 1 }, # { "$dist": 2.0, "id": 2 }, # ... # ] # } ``` ```python # $ pip install turbopuffer import turbopuffer import os tpuf = turbopuffer.Turbopuffer( # API tokens are created in the dashboard: https://turbopuffer.com/dashboard api_key=os.getenv("TURBOPUFFER_API_KEY"), region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-vector-example-py') # If an error occurs, this call raises a turbopuffer.APIError if a retry was not successful. result = ns.query( rank_by=("vector", "ANN", [0.1, 0.1]), limit=10 ) print(result.rows) # Prints a list of row-oriented documents: # [ # Row(id=1, vector=None, $dist=0.0), # Row(id=2, vector=None, $dist=2.0) # ] ``` ```typescript // $ npm install @turbopuffer/turbopuffer import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ // API tokens are created in the dashboard: https://turbopuffer.com/dashboard apiKey: process.env.TURBOPUFFER_API_KEY, region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-vector-example-ts`); const result = await ns.query({ rank_by: [ "vector", "ANN", [0.1, 0.1], ], limit: 10, }); console.log(result.rows); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( // API tokens are created in the dashboard: https://turbopuffer.com/dashboard option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-vector-example-go") // If an error occurs, this call raises an error if a retry was not successful. result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAnn("vector", []float32{0.1, 0.1}), Limit: turbopuffer.LimitParam{ Total: 10, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) // Returns a row-oriented result: // [ // {"id": 1, "vector": null, "$dist": 0.0}, // {"id": 2, "vector": null, "$dist": 2.0} // ] } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryVector { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() // API tokens are created in the dashboard: https://turbopuffer.com/dashboard .apiKey(System.getenv("TURBOPUFFER_API_KEY")) .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-vector-example-java"); var result = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.ann("vector", List.of(0.1f, 0.1f))) .limit(10) .build() ); System.out.println(result.rows().get()); // Prints a list of row-oriented documents: // [ // {id=1, vector=None, $dist=0.0}, // {id=2, vector=None, $dist=2.0} // ] } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // API tokens are created in the dashboard: https://turbopuffer.com/dashboard // Loaded from TURBOPUFFER_API_KEY env var by default. Override if necessary: // ApiKey = "...", // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-vector-example-csharp"); var result = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", new[] { 0.1f, 0.1f }), Limit = 10, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } // Prints a list of row-oriented documents: // {"$dist": 0.0, "id": 1} // {"$dist": 2.0, "id": 2} ``` ```ruby # $ gem install turbopuffer require "turbopuffer" tpuf = Turbopuffer::Client.new( # API tokens are created in the dashboard: https://turbopuffer.com/dashboard api_key: ENV["TURBOPUFFER_API_KEY"], region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-vector-example-rb") # If an error occurs, this call raises a Turbopuffer::Errors::APIError if a retry was not successful. result = ns.query( rank_by: [ "vector", "ANN", [0.1, 0.1], ], limit: 10, ) puts result.rows # Prints a list of row-oriented documents: # {id: 1, dist: 0.0} # {id: 2, dist: 2.0} ``` ### Filters When you need to filter documents, you can combine filters with vector search or use them alone. Here's an example of finding recent public documents: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-filters-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "filters": [ "And", [ ["timestamp", "Gte", "2024-03-01T00:00:00.000Z"], ["public", "Eq", true] ] ], "rank_by": [ "vector", "ANN", [0.1, 0.2, 0.3] ], "limit": 10, "include_attributes": ["title", "timestamp"] }' ``` ```python from datetime import datetime import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-filters-example-py') result = ns.query( filters=('And', ( ('timestamp', 'Gte', datetime(2024, 3, 1, 0, 0, 0)), # Documents after March 1, 2024 ('public', 'Eq', True) )), rank_by=("vector", "ANN", [0.1, 0.2, 0.3]), # Optional: include vector to combine with filters limit=10, include_attributes=['title', 'timestamp'] ) print(result.rows) # Prints a list of row-oriented documents: # [ # Row(id=1, vector=None, $dist=0.15, title='Getting Started Guide', timestamp='2024-03-02T00:00:000000000Z'), # Row(id=2, vector=None, $dist=0.28, title='Advanced Features', timestamp='2024-03-03T00:00:000000000Z'), # ] ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-filters-example-ts`); const result = await ns.query({ filters: [ "And", [ ["timestamp", "Gte", new Date("2024-03-01")], // Documents after March 1, 2024 ["public", "Eq", true], ], ], rank_by: [ "vector", "ANN", [0.1, 0.2, 0.3], ], // Optional: include vector to combine with filters limit: 10, include_attributes: ["title", "timestamp"], }); console.log(result.rows); // Returns a row-oriented result: // [ // {id: 1, $dist: 0.15, title: "Getting Started Guide", timestamp: "2024-03-02T00:00:000000000Z"}, // {id: 2, $dist: 0.28, title: "Advanced Features", timestamp: "2024-03-03T00:00:000000000Z"} // ] ``` ```go package main import ( "context" "fmt" "os" "time" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-filters-example-go") result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Filters: turbopuffer.NewFilterAnd([]turbopuffer.Filter{ // Documents after March 1, 2024 turbopuffer.NewFilterGte("timestamp", time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC)), turbopuffer.NewFilterEq("public", true), }), // Optional: include vector to combine with filters RankBy: turbopuffer.NewRankByAnn("vector", []float32{0.1, 0.2, 0.3}), Limit: turbopuffer.LimitParam{ Total: 10, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"title", "timestamp"}, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) // Returns a row-oriented result: // [ // {"id": 1, "$dist": 0.15, "title": "Getting Started Guide", "timestamp": "2024-03-02T00:00:000000000Z"}, // {"id": 2, "$dist": 0.28, "title": "Advanced Features", "timestamp": "2024-03-03T00:00:000000000Z"} // ] } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.time.*; import java.util.*; public class QueryFilters { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-filters-example-java"); var result = ns.query( NamespaceQueryParams.builder() .filters( Filter.and( Filter.gte("timestamp", ZonedDateTime.of(2024, 3, 1, 0, 0, 0, 0, ZoneOffset.UTC)), // Documents after March 1, 2024 Filter.eq("public", true) ) ) .rankBy(RankBy.ann("vector", List.of(0.1f, 0.2f, 0.3f))) // Optional: include vector to combine with filters .limit(10) .includeAttributes("title", "timestamp") .build() ); System.out.println(result.rows().get()); // Prints a list of row-oriented documents: // [ // {id=1, vector=None, $dist=0.15, title='Getting Started Guide', timestamp='2024-03-02T00:00:00'}, // {id=2, vector=None, $dist=0.28, title='Advanced Features', timestamp='2024-03-03T00:00:00'} // ] } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-filters-example-csharp"); var result = await ns.Query( new NamespaceQueryParams { Filters = Filter.And( Filter.Gte("timestamp", new DateTime(2024, 3, 1, 0, 0, 0, DateTimeKind.Utc)), // Documents after March 1, 2024 Filter.Eq("public", true) ), RankBy = RankBy.Ann("vector", new[] { 0.1f, 0.2f, 0.3f }), // Optional: include vector to combine with filters Limit = 10, IncludeAttributes = new List { "title", "timestamp" }, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } // Prints a list of row-oriented documents: // {"$dist": 0.15, "id": 1, "timestamp": "2024-03-02T00:00:00.000000000Z", "title": "Getting Started Guide"} // {"$dist": 0.28, "id": 2, "timestamp": "2024-03-03T00:00:00.000000000Z", "title": "Advanced Features"} ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-filters-example-rb") result = ns.query( filters: [ "And", [ ["timestamp", "Gte", DateTime.new(2024, 3, 1, 0, 0, 0)], # Documents after March 1, 2024 ["public", "Eq", true], ], ], rank_by: [ "vector", "ANN", [0.1, 0.2, 0.3], ], # Optional: include vector to combine with filters limit: 10, include_attributes: ["title", "timestamp"], ) puts result.rows # Prints a list of row-oriented documents: # {id: 1, dist: 0.15, title: "Getting Started Guide", timestamp: '2024-03-02T00:00:000000000Z'} # {id: 2, dist: 0.28, title: "Advanced Features", timestamp: '2024-03-03T00:00:000000000Z'} ``` ### Sparse vector search You can use `SparseKNN` to rank by the distance between a sparse vector attribute and a sparse vector query. For example: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-sparse-vector-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": [ "sparse_vector", "SparseKNN", {"dim0": 0.2, "dim3": 0.1} ], "limit": 10 }' ``` ```python # $ pip install turbopuffer import turbopuffer import os tpuf = turbopuffer.Turbopuffer( # API tokens are created in the dashboard: https://turbopuffer.com/dashboard api_key=os.getenv("TURBOPUFFER_API_KEY"), # Pick the right region: https://turbopuffer.com/docs/regions region="gcp-us-central1", ) ns = tpuf.namespace(f'query-sparse-vector-example-py') # If an error occurs, this call raises a turbopuffer.APIError if a retry was not successful. result = ns.query( rank_by=("sparse_vector", "SparseKNN", {"dim0": 0.2, "dim3": 0.1}), limit=10, ) print(result.rows) ``` ```typescript // $ npm install @turbopuffer/turbopuffer import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ // API tokens are created in the dashboard: https://turbopuffer.com/dashboard apiKey: process.env.TURBOPUFFER_API_KEY, // Pick the right region: https://turbopuffer.com/docs/regions region: "gcp-us-central1", }); const ns = tpuf.namespace(`query-sparse-vector-example-ts`); const result = await ns.query({ rank_by: ["sparse_vector", "SparseKNN", { dim0: 0.2, dim3: 0.1 }], limit: 10, }); console.log(result.rows); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( // API tokens are created in the dashboard: https://turbopuffer.com/dashboard option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // Pick the right region: https://turbopuffer.com/docs/regions option.WithRegion("gcp-us-central1"), ) ns := tpuf.Namespace("query-sparse-vector-example-go") // If an error occurs, this call raises an error if a retry was not successful. result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankBySparseKnn("sparse_vector", map[string]float64{"dim0": 0.2, "dim3": 0.1}), Limit: turbopuffer.LimitParam{ Total: 10, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QuerySparseVector { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() // API tokens are created in the dashboard: https://turbopuffer.com/dashboard .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // Pick the right region: https://turbopuffer.com/docs/regions .region("gcp-us-central1") .build(); var ns = tpuf.namespace("query-sparse-vector-example-java"); var result = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.sparseKnn("sparse_vector", Map.of("dim0", 0.2, "dim3", 0.1))) .limit(10) .build() ); System.out.println(result.rows().get()); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // API tokens are created in the dashboard: https://turbopuffer.com/dashboard // Loaded from TURBOPUFFER_API_KEY env var by default. Override if necessary: // ApiKey = "...", // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-sparse-vector-example-csharp"); var result = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.SparseKnn( "sparse_vector", new Dictionary { ["dim0"] = 0.2, ["dim3"] = 0.1 } ), Limit = 10, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } ``` ```ruby # $ gem install turbopuffer require "turbopuffer" tpuf = Turbopuffer::Client.new( # API tokens are created in the dashboard: https://turbopuffer.com/dashboard api_key: ENV["TURBOPUFFER_API_KEY"], # Pick the right region: https://turbopuffer.com/docs/regions region: "gcp-us-central1", ) ns = tpuf.namespace("query-sparse-vector-example-rb") # If an error occurs, this call raises a Turbopuffer::Errors::APIError if a retry was not successful. result = ns.query( rank_by: ["sparse_vector", "SparseKNN", { dim0: 0.2, dim3: 0.1 }], limit: 10, ) puts result.rows ``` `SparseKNN` is compatible with FTS operators: `Sum`, `Max`, `Saturate`, etc. ### Ordering by Attributes You can specify a `rank_by` parameter to order results by a specific attribute (i.e. SQL `ORDER BY`). For example, to order by timestamp in descending order: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-ordering-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "filters": [ "timestamp", "Lt", "2024-03-01T00:00:00.000Z" ], "rank_by": [ "timestamp", "desc" ], "limit": 1000, "include_attributes": ["title", "timestamp"] }' ``` ```python from datetime import datetime import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-ordering-example-py') result = ns.query( filters=('timestamp', 'Lt', datetime(2024, 3, 1, 0, 0, 0)), # Documents before March 1, 2024 rank_by=('timestamp', 'desc'), # Order by timestamp in descending order limit=1000, include_attributes=['title', 'timestamp'] ) print(result.rows) # Prints a list of row-oriented documents: # [ # Row(id=6, vector=None, title='Roadmap', timestamp='2024-02-27T00:00:000000000Z'), # Row(id=4, vector=None, title='Performance Guide', timestamp='2024-02-24T00:00:000000000Z'), # ] ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-ordering-example-ts`); const result = await ns.query({ filters: [ "timestamp", "Lt", new Date("2024-03-01").toISOString(), ], // Documents before March 1, 2024 rank_by: [ "timestamp", "desc", ], // Order by timestamp in descending order limit: 1000, include_attributes: ["title", "timestamp"], }); console.log(result.rows); // Returns a row-oriented result: // [ // {id: 6, vector: null, title: "Roadmap", timestamp: "2024-02-27T00:00:000000000Z"}, // {id: 4, vector: null, title: "Performance Guide", timestamp: "2024-02-24T00:00:000000000Z"} // ] ``` ```go package main import ( "context" "fmt" "os" "time" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-ordering-example-go") result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Filters: turbopuffer.NewFilterLt("timestamp", time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC)), // Documents before March 1, 2024 RankBy: turbopuffer.NewRankByAttribute("timestamp", turbopuffer.RankByAttributeOrderDesc), // Order by timestamp in descending order Limit: turbopuffer.LimitParam{ Total: 1000, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"title", "timestamp"}, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) // Returns a row-oriented result: // [ // {"id": 6, "vector": null, "title": "Roadmap", "timestamp": "2024-02-27T00:00:000000000Z"}, // {"id": 4, "vector": null, "title": "Performance Guide", "timestamp": "2024-02-24T00:00:000000000Z"} // ] } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.time.*; import java.util.*; public class QueryOrdering { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-ordering-example-java"); var result = ns.query( NamespaceQueryParams.builder() .filters(Filter.lt("timestamp", ZonedDateTime.of(2024, 3, 1, 0, 0, 0, 0, ZoneOffset.UTC))) // Documents before March 1, 2024 .rankBy(RankBy.attribute("timestamp", RankByAttributeOrder.DESC)) // Order by timestamp in descending order .limit(1000) .includeAttributes("title", "timestamp") .build() ); System.out.println(result.rows().get()); // Prints a list of row-oriented documents: // [ // {id=6, vector=None, title='Roadmap', timestamp='2024-02-27T00:00:00'}, // {id=4, vector=None, title='Performance Guide', timestamp='2024-02-24T00:00:00'} // ] } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-ordering-example-csharp"); var result = await ns.Query( new NamespaceQueryParams { Filters = Filter.Lt("timestamp", new DateTime(2024, 3, 1, 0, 0, 0, DateTimeKind.Utc)), // Documents before March 1, 2024 RankBy = RankBy.Attribute("timestamp", RankByAttributeOrder.DESC), // Order by timestamp in descending order Limit = 1000, IncludeAttributes = new List { "title", "timestamp" }, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } // Prints a list of row-oriented documents: // {"id": 6, "timestamp": "2024-02-27T00:00:00.000000000Z", "title": "Roadmap"} // {"id": 4, "timestamp": "2024-02-24T00:00:00.000000000Z", "title": "Performance Guide"} ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-ordering-example-rb") result = ns.query( filters: [ "timestamp", "Lt", DateTime.new(2024, 3, 1, 0, 0, 0), ], # Documents before March 1, 2024 rank_by: [ "timestamp", "desc", ], # Order by timestamp in descending order limit: 1000, include_attributes: ["title", "timestamp"], ) puts result.rows # Prints a list of row-oriented documents: # {id: 6, dist: 0.15, title: "Roadmap", timestamp: '2024-03-02T00:00:000000000Z'} # {id: 4, dist: 0.28, title: "Performance Guide", timestamp: '2024-03-03T00:00:000000000Z'} ``` Ordering by multiple attributes isn't yet implemented. Similar to SQL, the ordering of results is not guaranteed when multiple documents have the same attribute value for the `rank_by` parameter. Array attributes aren't supported. Documents with a missing or `null` value for the `rank_by` attribute are sorted first in ascending order and last in descending order. ### Lookups To find all documents matching filters when order isn't important to you, rank by the `id` attribute, which is guaranteed to be present in every namespace: ```json "filters": [...], "rank_by": [ "id", "asc" ], "limit": ... ``` If you expect more than `limit.total` results, see [Pagination](#pagination). ### Aggregations Aggregations are still being optimized - we do not recommend using them for latency sensitive workloads on namespaces that exceed 1M documents in size. You can aggregate attribute values across all documents in the namespace that match the query's filters using the [aggregate_by parameter](#param-aggregate_by). For example, to count the number of documents in a namespace: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-count-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "aggregate_by": { "my_cool_count": ["Count"] }, "filters": [ "cool_score", "Gt", 7 ] }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-count-example-py') result = ns.query( aggregate_by={'my_cool_count': ('Count',)}, filters=('cool_score', 'Gt', 7), ) print(result.aggregations['my_cool_count']) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-count-example-ts`); const result = await ns.query({ aggregate_by: { my_cool_count: ["Count"] }, filters: [ "cool_score", "Gt", 7, ], }); console.log(result.aggregations!.my_cool_count); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-count-example-go") result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ AggregateBy: map[string]turbopuffer.AggregateBy{ "my_cool_count": turbopuffer.NewAggregateByCount(), }, Filters: turbopuffer.NewFilterGt("cool_score", 7), }, ) if err != nil { panic(err) } fmt.Println(result.Aggregations["my_cool_count"]) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryCount { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-count-example-java"); var queryResult = ns.query( NamespaceQueryParams.builder() .aggregateBy(Map.of("my_cool_count", AggregateBy.count("id"))) .filters(Filter.gt("cool_score", 7)) .build() ); var aggregations = queryResult.aggregations().get(); System.out.println(aggregations.get("my_cool_count")); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-count-example-csharp"); var queryResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["my_cool_count"] = AggregateBy.Count() }, Filters = Filter.Gt("cool_score", 7), } ); var aggregations = queryResult.GetAggregations(); Console.WriteLine(aggregations["my_cool_count"]); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-count-example-rb") result = ns.query( aggregate_by: { my_cool_count: ["Count"] }, filters: [ "cool_score", "Gt", 7, ], ) puts result.aggregations[:my_cool_count] ``` You can use `Sum` to sum numeric attribute values across all documents that match a particular filter: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-sum-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "aggregate_by": { "my_cool_sum": ["Sum", "cool_score"] }, "filters": [ "id", "Gte", 2 ] }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-sum-example-py') result = ns.query( aggregate_by={'my_cool_sum': ('Sum', 'cool_score')}, filters=('id', 'Gte', 2), ) print(result.aggregations['my_cool_sum']) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-sum-example-ts`); const result = await ns.query({ aggregate_by: { my_cool_sum: ["Sum", "cool_score"] }, filters: [ "id", "Gte", 2, ], }); console.log(result.aggregations!.my_cool_sum); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-sum-example-go") result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ AggregateBy: map[string]turbopuffer.AggregateBy{ "my_cool_sum": turbopuffer.NewAggregateBySum("cool_score"), }, Filters: turbopuffer.NewFilterGte("id", 2), }, ) if err != nil { panic(err) } fmt.Println(result.Aggregations["my_cool_sum"]) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QuerySum { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-sum-example-java"); var queryResult = ns.query( NamespaceQueryParams.builder() .aggregateBy(Map.of("my_cool_sum", AggregateBy.sum("cool_score"))) .filters(Filter.gte("id", 2)) .build() ); var aggregations = queryResult.aggregations().get(); System.out.println(aggregations.get("my_cool_sum")); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-sum-example-csharp"); var queryResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["my_cool_sum"] = AggregateBy.Sum("cool_score") }, Filters = Filter.Gte("id", 2), } ); var aggregations = queryResult.GetAggregations(); Console.WriteLine(aggregations["my_cool_sum"]); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-sum-example-rb") result = ns.query( aggregate_by: { my_cool_sum: ["Sum", "cool_score"] }, filters: [ "id", "Gte", 1, ], ) puts result.aggregations[:my_cool_sum] ``` ### Group by Aggregations are still being optimized - we do not recommend using them for latency sensitive workloads on namespaces that exceed 1M documents in size. When [aggregating](#param-aggregate_by), you can use the [group_by](#param-group_by) parameter to group results by one or more attributes. Aggregates are computed separately for each group. For example, to count documents grouped by the `color` and `size` attributes: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-group-by-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "aggregate_by": { "count_by_color_and_size": ["Count"] }, "group_by": ["color", "size"] }' # [ # {"color": "blue", "count_by_color_and_size": 1, "size": "XL"}, # {"color": "red", "count_by_color_and_size": 2, "size": "L"} # ] ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-group-by-example-py') result = ns.query( aggregate_by={"count_by_color_and_size": ("Count",)}, group_by=["color", "size"], ) print(result.aggregation_groups) # [ # Row(color='blue', count_by_color_and_size=1, size='XL'), # Row(color='red', count_by_color_and_size=2, size='L') # ] ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-group-by-example-ts`); const result = await ns.query({ aggregate_by: { count_by_color_and_size: ["Count"] }, group_by: ["color", "size"], }); console.log(result.aggregation_groups); // [ // { color: "blue", count_by_color_and_size: 1, size: "XL" }, // { color: "red", count_by_color_and_size: 2, size: "L" }, // ] ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-group-by-example-go") result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ AggregateBy: map[string]turbopuffer.AggregateBy{ "count_by_color_and_size": turbopuffer.NewAggregateByCount(), }, GroupBy: []turbopuffer.GroupBy{ turbopuffer.NewGroupByAttr("color"), turbopuffer.NewGroupByAttr("size"), }, }, ) if err != nil { panic(err) } fmt.Println(turbopuffer.PrettyPrint(result.AggregationGroups)) // [ // { color: "blue", count_by_color_and_size: 1, size: "XL" }, // { color: "red", count_by_color_and_size: 2, size: "L" }, // ] } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryGroupBy { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-group-by-example-java"); var queryResult = ns.query( NamespaceQueryParams.builder() .aggregateBy(Map.of("count_by_color_and_size", AggregateBy.count())) .groupBy(List.of(GroupBy.attr("color"), GroupBy.attr("size"))) .build() ); var aggregationGroups = queryResult.aggregationGroups().get(); System.out.println(aggregationGroups); // [ // {color=blue, count_by_color_and_size=1, size=XL}, // {color=red, count_by_color_and_size=2, size=L} // ] } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-group-by-example-csharp"); var queryResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["count_by_color_and_size"] = AggregateBy.Count(), }, GroupBy = [GroupBy.Attr("color"), GroupBy.Attr("size")], } ); foreach (var group in queryResult.GetAggregationGroups()) { Console.WriteLine(group); } // Prints a list of aggregation groups: // { "size": "XL", "count_by_color_and_size": 1, "color": "blue" } // { "size": "L", "count_by_color_and_size": 2, "color": "red" } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-group-by-example-rb") result = ns.query( aggregate_by: { count_by_color_and_size: ["Count"] }, group_by: ["color", "size"], ) puts result.aggregation_groups # {color: "blue", count_by_color_and_size: 1, size: "XL"} # {color: "red", count_by_color_and_size: 2, size: "L"} ``` You can use the `ForEachUnique` operator to explode array attributes when grouping. This creates a separate group for each unique element of the array. For example, if documents have a `tags` array attribute with values like `["electronics", "mobile"]`, using the `ForEachUnique` operator will create separate groups for `electronics` and `mobile`: ```bash curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-group-by-for-each-unique-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "aggregate_by": { "count_by_tag": ["Count"] }, "group_by": [ {"tag": ["ForEachUnique", "tags"]} ] }' # [ # {"tag": "electronics", "count_by_tag": 2}, # {"tag": "mobile", "count_by_tag": 1} # ] ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region="gcp-us-central1", # pick the right region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-group-by-for-each-unique-example-py') result = ns.query( aggregate_by={"count_by_tag": ("Count",)}, group_by=[{"tag": ("ForEachUnique", "tags")}], ) print(result.aggregation_groups) # [ # Row(tag='electronics', count_by_tag=2), # Row(tag='mobile', count_by_tag=1), # ] ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // pick the right region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-group-by-for-each-unique-example-ts`); const result = await ns.query({ aggregate_by: { count_by_tag: ["Count"] }, group_by: [{ tag: ["ForEachUnique", "tags"] }], }); console.log(result.aggregation_groups); // [ // { tag: "electronics", count_by_tag: 2 }, // { tag: "mobile", count_by_tag: 1 }, // ] ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( // Pick the right region: https://turbopuffer.com/docs/regions option.WithRegion("gcp-us-central1"), ) ns := tpuf.Namespace("query-group-by-for-each-unique-example-go") result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ AggregateBy: map[string]turbopuffer.AggregateBy{ "count_by_tag": turbopuffer.NewAggregateByCount(), }, GroupBy: []turbopuffer.GroupBy{ turbopuffer.NewGroupByExpr("tag", turbopuffer.NewGroupByFunctionForEachUnique("tags")), }, }, ) if err != nil { panic(err) } fmt.Println(turbopuffer.PrettyPrint(result.AggregationGroups)) // [ // { tag: "electronics", count_by_tag: 2 }, // { tag: "mobile", count_by_tag: 1 }, // ] } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryGroupByForEachUnique { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() // Pick the right region: https://turbopuffer.com/docs/regions .region("gcp-us-central1") .build(); var ns = tpuf.namespace( "query-group-by-for-each-unique-example-java" ); var queryResult = ns.query( NamespaceQueryParams.builder() .aggregateBy(Map.of("count_by_tag", AggregateBy.count())) .groupBy(List.of(GroupBy.expr("tag", GroupByFunction.forEachUnique("tags")))) .build() ); var aggregationGroups = queryResult.aggregationGroups().get(); System.out.println(aggregationGroups); // [ // {count_by_tag=2, tag=electronics}, // {count_by_tag=1, tag=mobile} // ] } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-group-by-for-each-unique-example-csharp"); var queryResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["count_by_tag"] = AggregateBy.Count(), }, GroupBy = [GroupBy.Expr("tag", GroupByFunction.ForEachUnique("tags"))], } ); foreach (var group in queryResult.GetAggregationGroups()) { Console.WriteLine(group); } // Prints a list of aggregation groups: // { "count_by_tag": 2, "tag": "electronics" } // { "count_by_tag": 1, "tag": "mobile" } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # pick the right region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-group-by-for-each-unique-example-rb") result = ns.query( aggregate_by: { count_by_tag: ["Count"] }, group_by: [{ tag: ["ForEachUnique", "tags"] }], ) puts result.aggregation_groups # {tag: "electronics", count_by_tag: 2} # {tag: "mobile", count_by_tag: 1} ``` You can also combine `ForEachUnique` with regular grouping attributes: ```jsonc { "aggregate_by": { "count_by_tag_and_status": ["Count"] }, "group_by": [ { "tag": ["ForEachUnique", "tags"] }, "status" ] } ``` This query returns aggregation groups for each combination of `tag` and `status`. ### Multi-queries You can provide multiple query objects to be executed simultaneously on a namespace. Individual subqueries can be one of any other primitive query type, simplifying complex retrieval workflows. Multi-queries offer better performance than issuing independent queries to the same namespace. All reads in a multi-query are executed against the same consistent snapshot of the database (snapshot isolation). Up to 16 queries can be sent per request. Each subquery will count against the [concurrent query limit](/docs/limits) for the namespace. If you need a higher limit, please [contact us](/contact). For example, a standard hybrid query combining full-text and vector searches executed together through a multi-query: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-multi-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "queries": [ { "rank_by": [ "vector", "ANN", [1.0, 0.0] ], "limit": 1 }, { "rank_by": [ "attr1", "BM25", "quick fox" ], "limit": 1 } ] }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-multi-example-py') response = ns.multi_query( queries=[ { "rank_by": ("vector", "ANN", [1.0, 0.0]), "limit": 1 }, { "rank_by": ("attr1", "BM25", "quick fox"), "limit": 1, }, ] ) print(response.results) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-multi-example-ts`); const result = await ns.multiQuery({ queries: [ { rank_by: [ "vector", "ANN", [1.0, 0.0], ], limit: 1, }, { rank_by: [ "attr1", "BM25", "quick fox", ], limit: 1, } ] }); console.log(result.results); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-multi-example-go") result, err := ns.MultiQuery( ctx, turbopuffer.NamespaceMultiQueryParams{ Queries: []turbopuffer.QueryParam{ { RankBy: turbopuffer.NewRankByAnn("vector", []float32{1.0, 0.0}), Limit: turbopuffer.LimitParam{ Total: 1, }, }, { RankBy: turbopuffer.NewRankByTextBM25("attr1", "quick fox"), Limit: turbopuffer.LimitParam{ Total: 1, }, }, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Results)) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryMulti { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-multi-example-java"); var response = ns.multiQuery( NamespaceMultiQueryParams.builder() .addQuery( Query.builder().rankBy(RankBy.ann("vector", List.of(1.0f, 0.0f))).limit(1).build() ) .addQuery(Query.builder().rankBy(RankByText.bm25("attr1", "quick fox")).limit(1).build()) .build() ); System.out.println(response.results()); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-multi-example-csharp"); var response = await ns.MultiQuery( new NamespaceMultiQueryParams { Queries = [ new Query { RankBy = RankBy.Ann("vector", new[] { 1.0f, 0.0f }), Limit = 1 }, new Query { RankBy = RankByText.BM25("attr1", "quick fox"), Limit = 1 }, ], } ); foreach (var result in response.Results) { foreach (var row in result.GetRows()) { Console.WriteLine(row); } } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-multi-example-rb") response = ns.multi_query( queries: [ { rank_by: [ "vector", "ANN", [1.0, 0.0], ], limit: 1, }, { rank_by: [ "attr1", "BM25", "quick fox", ], limit: 1, }, ], ) puts response.results ``` Individual sub-queries can vary their parameters independently including different `filters`, `limit`, `rank_by` or `aggregate_by`. The `consistency` parameter must be set at the root level of the request, not on individual sub-queries. All sub-queries in a multi-query share the same consistency level. ### Reciprocal rank fusion Reciprocal rank fusion (RRF) lets you combine the results of a [multi-query](#multi-queries) into a single ranked list. It operates on ranks rather than scores, so it can fuse results across different search types, such as BM25, dense vector, and sparse vector search. ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-rrf-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "queries": [ {"rank_by": ["vector", "ANN", [1.0, 0.0]], "limit": 10}, {"rank_by": ["attr1", "BM25", "quick fox"], "limit": 10} ], "rerank_by": ["RRF"] }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-rrf-example-py') response = ns.multi_query( queries=[ {"rank_by": ("vector", "ANN", [1.0, 0.0]), "limit": 10}, {"rank_by": ("attr1", "BM25", "quick fox"), "limit": 10}, ], rerank_by=("RRF",), ) print(response.results) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-rrf-example-ts`); const result = await ns.multiQuery({ queries: [ {rank_by: ["vector", "ANN", [1.0, 0.0]], limit: 10}, {rank_by: ["attr1", "BM25", "quick fox"], limit: 10}, ], rerank_by: ["RRF"], }); console.log(result.results); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-rrf-example-go") result, err := ns.MultiQuery( ctx, turbopuffer.NamespaceMultiQueryParams{ Queries: []turbopuffer.QueryParam{ { RankBy: turbopuffer.NewRankByAnn("vector", []float32{1.0, 0.0}), Limit: turbopuffer.LimitParam{Total: 10}, }, { RankBy: turbopuffer.NewRankByTextBM25("attr1", "quick fox"), Limit: turbopuffer.LimitParam{Total: 10}, }, }, RerankBy: turbopuffer.NewRerankByRrf(), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Results)) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryRrf { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-rrf-example-java"); var response = ns.multiQuery( NamespaceMultiQueryParams.builder() .addQuery( Query.builder().rankBy(RankBy.ann("vector", List.of(1.0f, 0.0f))).limit(10).build() ) .addQuery(Query.builder().rankBy(RankByText.bm25("attr1", "quick fox")).limit(10).build()) .rerankBy(RerankBy.rrf()) .build() ); System.out.println(response.results()); } } ``` ```cs using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-rrf-example-csharp"); var response = await ns.MultiQuery( new NamespaceMultiQueryParams { Queries = [ new Query { RankBy = RankBy.Ann("vector", new[] { 1.0f, 0.0f }), Limit = 10 }, new Query { RankBy = RankByText.BM25("attr1", "quick fox"), Limit = 10 }, ], RerankBy = RerankBy.Rrf(), } ); foreach (var result in response.Results) { foreach (var row in result.GetRows()) { Console.WriteLine(row); } } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-rrf-example-rb") response = ns.multi_query( queries: [ { rank_by: ["vector", "ANN", [1.0, 0.0]], limit: 10 }, { rank_by: ["attr1", "BM25", "quick fox"], limit: 10 }, ], rerank_by: ["RRF"], ) puts response.results ``` The [results](#responsefield-results) contain a single list combining all the subquery results, sorted by descending RRF score. The RRF score for each row is reported in the `$dist` field. It is calculated as the sum of `1 / (rank_constant + rank)` across all subquery results. `rank_constant` is an integer greater than zero, with a default of 60. Use the `["RRF", { "rank_constant": }]` syntax to select a different value. RRF reranking requires at least two subqueries and is not supported for aggregations. ## Full-Text Search The FTS attribute must be configured with `full_text_search` set in the schema when writing documents. See [Schema documentation](/docs/write#schema) and the [Full-Text Search guide](/docs/fts) for more details. For an example of hybrid search (combining both vector and BM25 results), see [Hybrid Search](/docs/hybrid-search). ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-fts-basic-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": [ "content", "BM25", "quick fox" ], "limit": 10, "include_attributes": ["title", "content"] }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-fts-basic-example-py') result = ns.query( rank_by=('content', 'BM25', 'quick fox'), limit=10, include_attributes=['title', 'content'] ) print(result.rows) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-fts-basic-example-ts`); const result = await ns.query({ rank_by: [ "content", "BM25", "quick fox", ], limit: 10, include_attributes: ["title", "content"], }); console.log(result.rows); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-fts-basic-example-go") result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByTextBM25("content", "quick fox"), Limit: turbopuffer.LimitParam{ Total: 10, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"title", "content"}, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryFtsBasic { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-fts-basic-example-java"); var result = ns.query( NamespaceQueryParams.builder() .rankBy(RankByText.bm25("content", "quick fox")) .limit(10) .includeAttributes("title", "content") .build() ); System.out.println(result.rows().get()); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-fts-basic-example-csharp"); var result = await ns.Query( new NamespaceQueryParams { RankBy = RankByText.BM25("content", "quick fox"), Limit = 10, IncludeAttributes = new List { "title", "content" }, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-fts-basic-example-rb") result = ns.query( rank_by: [ "content", "BM25", "quick fox", ], limit: 10, include_attributes: ["title", "content"], ) puts result.rows ``` You can combine BM25 full-text search with filters to limit results to a specific subset of documents. ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/fts-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": [ "content", "BM25", "quick fox" ], "filters": [ "And", [ ["timestamp", "Gte", "2024-03-01T00:00:00.000Z"], ["public", "Eq", true] ] ], "limit": 10, "include_attributes": ["title", "content", "timestamp"] }' ``` ```python from datetime import datetime import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-fts-example-py') result = ns.query( rank_by=('content', 'BM25', 'quick fox'), filters=('And', ( ('timestamp', 'Gte', datetime(2024, 3, 1, 0, 0, 0)), # Documents after March 1, 2024 ('public', 'Eq', True), )), limit=10, include_attributes=['title', 'content', 'timestamp'] ) print(result.rows) # Prints a list of row-oriented documents: # [ # Row(id=1, vector=None, $dist=0.85, title='Animal Stories', content='The quick brown fox...', timestamp='2024-03-02T00:00:000000000Z'), # Row(id=2, vector=None, $dist=1.28, title='Forest Tales', content='A quick red fox...', timestamp='2024-03-03T00:00:000000000Z'), # ] ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-fts-example-ts`); const result = await ns.query({ rank_by: [ "content", "BM25", "quick fox", ], filters: [ "And", [ ["timestamp", "Gte", new Date("2024-03-01").toISOString()], // Documents after March 1, 2024 ["public", "Eq", true], ], ], limit: 10, include_attributes: ["title", "content", "timestamp"], }); console.log(result.rows); // Returns a row-oriented result: // [ // {id: 1, vector: null, title: "Animal Stories", content: "The quick brown fox...", timestamp: "2024-03-02T00:00:000000000Z", $dist: 0.85}, // {id: 2, vector: null, title: "Forest Tales", content: "A quick red fox...", timestamp: "2024-03-03T00:00:000000000Z", $dist: 1.28} // ] ``` ```go package main import ( "context" "fmt" "os" "time" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-fts-example-go") result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByTextBM25("content", "quick fox"), Filters: turbopuffer.NewFilterAnd([]turbopuffer.Filter{ turbopuffer.NewFilterGte("timestamp", time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC)), // Documents after March 1, 2024 turbopuffer.NewFilterEq("public", true), }), Limit: turbopuffer.LimitParam{ Total: 10, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"title", "content", "timestamp"}, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) // Returns a row-oriented result: // [ // {"id": 1, "vector": null, "title": "Animal Stories", "content": "The quick brown fox...", "timestamp": "2024-03-02T00:00:000000000Z", "$dist": 0.85}, // {"id": 2, "vector": null, "title": "Forest Tales", "content": "A quick red fox...", "timestamp": "2024-03-03T00:00:000000000Z", "$dist": 1.28} // ] } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.time.*; import java.util.*; public class QueryFts { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-fts-example-java"); var result = ns.query( NamespaceQueryParams.builder() .rankBy(RankByText.bm25("content", "quick fox")) .filters( Filter.and( Filter.gte("timestamp", ZonedDateTime.of(2024, 3, 1, 0, 0, 0, 0, ZoneOffset.UTC)), // Documents after March 1, 2024 Filter.eq("public", true) ) ) .limit(10) .includeAttributes("title", "content", "timestamp") .build() ); System.out.println(result.rows().get()); // Prints a list of row-oriented documents: // [ // {id=1, vector=None, $dist=0.85, title='Animal Stories', content='The quick brown fox...', timestamp='2024-03-02T00:00:00'}, // {id=2, vector=None, $dist=1.28, title='Forest Tales', content='A quick red fox...', timestamp='2024-03-03T00:00:00'} // ] } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-fts-example-csharp"); var result = await ns.Query( new NamespaceQueryParams { RankBy = RankByText.BM25("content", "quick fox"), Filters = Filter.And( Filter.Gte("timestamp", new DateTime(2024, 3, 1, 0, 0, 0, DateTimeKind.Utc)), // Documents after March 1, 2024 Filter.Eq("public", true) ), Limit = 10, IncludeAttributes = new List { "title", "content", "timestamp" }, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } // Prints a list of row-oriented documents: // {"$dist": 0.85, "id": 1, "content": "The quick brown fox...", "timestamp": "2024-03-02T00:00:00.000000000Z", "title": "Animal Stories"} // {"$dist": 1.28, "id": 2, "content": "A quick red fox...", "timestamp": "2024-03-03T00:00:00.000000000Z", "title": "Forest Tales"} ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-fts-example-rb") result = ns.query( rank_by: [ "content", "BM25", "quick fox", ], filters: [ "And", [ ["timestamp", "Gte", DateTime.new(2024, 3, 1, 0, 0, 0)], # Documents after March 1, 2024 ["public", "Eq", true], ], ], limit: 10, include_attributes: ["title", "content", "timestamp"], ) puts result.rows # Prints a list of row-oriented documents: # {id: 1, dist: 0.85, title: "Animal Stories", content: "The quick brown fox...", timestamp: '2024-03-02T00:00:000000000Z'} # {id: 2, dist: 1.28, title: "Forest Tales", content: "A quick red fox...", timestamp: '2024-03-03T00:00:000000000Z'} ``` ### FTS operators FTS operators combine the results of multiple clauses into a single score. Specifically, the following operators are supported: - `Sum`: Sum the scores of the clauses. - `Max`: Use the maximum score of clauses as the score. Operators can be nested. For example: ```json "rank_by": [ "Sum", [ [ "Max", [ ["title", "BM25", "whale facts"], ["description", "BM25", "whale facts"] ] ], ["content", "BM25", "huge whale"] ] ] ``` ### Field weights/boosts You can specify a weight / boost per-field by using the `Product` operator inside a `rank_by`. For example, to apply a 2x score multiplier on the `title` clause: ```json "rank_by": [ "Sum", [ ["Product", 2, ["title", "BM25", "quick fox"]], ["content", "BM25", "quick fox"] ] ] ``` Note that the weight must be non-negative. ### Rank by filter [Filters](#filtering) can be used inside `rank_by` expressions to conditionally boost documents matching certain criteria. Documents that pass the filter get a score of 1, and are otherwise scored 0. ```json "rank_by": [ "Sum", [ ["title", "BM25", "quick fox"], ["species", "Eq", "whale"] ] ] ``` Use `Product` to change how large the boost is: ```json "rank_by": [ "Product", 2.0, ["species", "Eq", "whale"] ] ``` ### Rank by attribute You can use attribute values from your documents to influence ranking by using the `Attribute` operator. For instance, to rank by the sum of the BM25 score and the value of an attribute named `clicks`: ```json "rank_by": [ "Sum", [ ["title", "BM25", "quick fox"], ["Max", 0, ["Attribute", "clicks"]] ] ] ``` Scores have to be non-negative for best performance. turbopuffer will reject `Attribute` operators applied on signed types such as int and float without the Max operator. The above ranking function will generally not give good relevance since BM25 scores and the `clicks` attribute have hardly comparable distributions. It is possible to improve it by using the `Saturate` operator, which maps non-negative values into `[0, 1)` through the following function: `Saturate(x) = x^exponent / (x^exponent + midpoint^exponent)` where - `midpoint` is a required parameter that controls the input value that gives 0.5. The function grows slower and slower after this point. - `exponent` (default: 1) is a second-order tuning parameter that helps further tune how quickly the function grows. `Saturate` implicitly takes the max of attribute values with zero, so wrapping the `Attribute` operator under a `Max` is not needed. ![saturate_plot.svg](/images/saturate_plot.svg) Think of `Saturate` as the counterpart of BM25, but for numeric attributes rather than text, something that turns numeric values into scores that can be combined with other scores - such as BM25 scores - through a weighted linear combination. See how the below example uses `Product` to set a weight of 1.7 on the contribution of the `clicks` attribute: ```json "rank_by": [ "Sum", [ ["title", "BM25", "quick fox"], [ "Product", 1.7, [ "Saturate", ["Attribute", "clicks"], { "midpoint": 100 } ] ] ] ] ``` A difference though is that BM25 takes advantage of the fact that text data usually follows the same pattern to come with good defaults for the weight and `midpoint` of the score contribution of each term, while you are responsible for providing this information for attributes. We recommend vibe-tuning the `midpoint` and weights to begin with against simple queries you test yourself, and setting up more robust evals later. We intend to write more about how to set up search evals soon, for now, ask your favourite LLM. - As long as your weight stays in [1, 3], you should be relatively safe as your attribute will have score contributions of the same order as a term that occurs in 40% of documents or more. So if your BM25 query includes rare terms, BM25 will still drive the ordering of hits. - Good values for `midpoint` are usually close to the lower end of the value range. For instance, BM25 configures a `midpoint` of 1.2 by default (the `k1` parameter) for the contribution of the term frequency (the number of occurrences of the term in the document) to the score. - Leave `exponent` to its default value of 1 until you have evals to tune it. Sometimes, attributes correlate inversely with relevance, e.g. a `number_of_negative_reviews` attribute where higher values make a document less relevant. In such cases, one should swap the `Saturate` operator with the `Decay` operator, which takes the same configuration parameters and is implemented as `Decay(x) = midpoint^exponent / (x^exponent + midpoint^exponent)`. ![decay_plot.svg](/images/decay_plot.svg) ```json "rank_by": [ "Sum", [ ["title", "BM25", "quick fox"], [ "Decay", ["Attribute", "number_of_negative_reviews"], { "midpoint": 2 } ] ] ] ``` ### Rank by distance The `Dist` operator can be used to boost by the distance between an origin point and an attribute value. The returned distance is typically passed to the `Decay` operator, then summed with BM25 scores. For instance, to boost by recency (time distance): ```json "rank_by": [ "Sum", [ ["title", "BM25", "quick fox"], [ "Decay", ["Dist", ["Attribute", "published_at"], "2026-02-03T12:13:14"], { "midpoint": "6h" } ] ] ] ``` When used on a `datetime` field, the `midpoint` can be provided either as a number of milliseconds, or as a duration string. Supported units include `s` (seconds), `m` (minutes), `h` (hours), `d` (days) and `w` (weeks). ### Phrase matching `ContainsTokenSequence` matches documents that contain all the tokens present in the filter input string, in the exact order and adjacent to each other. ```json "filters": [ "text", "ContainsTokenSequence", "walrus is lazy" ] ``` Currently, turbopuffer implements `ContainsTokenSequence` using a partial postfilter which may lead to reduced recall on ANN queries, and potentially higher latency on filter-only and FTS queries; we expect to improve this in the future. `ContainsAllTokens` matches documents that contain all the tokens present in the filter input string, regardless of order or adjacency. For example, this filter would match a document like "walrus is lazy", provided said document didn't contain both "polar" and "bear": ```json "filters": [ "And", [ ["text", "ContainsAllTokens", "lazy walrus"], ["Not", ["text", "ContainsAllTokens", "polar bear"]] ] ] ``` `ContainsAllTokens` is generally faster than `ContainsTokenSequence`. ### Prefix queries Type-ahead style prefix queries are supported through the `ContainsAllTokens` filter, `ContainsAnyToken` filter, and the `BM25` ranking operator using the `last_as_prefix` parameter: ```jsonc // As a filter (matching all tokens) "filters": [ "text", "ContainsAllTokens", "lazy wal", { "last_as_prefix": true } ] // As a filter (matching any token) "filters": [ "text", "ContainsAnyToken", "lazy wal", { "last_as_prefix": true } ] // Within a BM25 query "rank_by": [ "text", "BM25", "lazy wal", { "last_as_prefix": true } ] ``` When `last_as_prefix` is true, the last token in the input string is treated as a literal prefix. In this case, the prefix "wal" matches documents that contain "wal", "walrus", "walnut", etc. `BM25` prefix matches are assigned a score of `1.0`. ## Filtering Filters allow you to narrow down results by applying exact conditions to attributes. Conditions are arrays with an attribute name, operation, and value, for example: - `["attr_name", "Eq", 42]` - `["page_id", "In", ["page1", "page2"]]` - `["user_migrated_at", "NotEq", null]` Values must have the same type as the attribute's value, or an array of that type for operators like `ContainsAny`. Filters are evaluated against an inverted index, which makes even large intersects fast. turbopuffer's [filtering is recall-aware for vector queries](/blog/native-filtering). Conditions can be combined using `{And,Or}` operations: ```json // basic And condition "filters": [ "And", [ ["attr_name", "Eq", 42], ["page_id", "In", ["page1", "page2"]] ] ] // conditions can be nested "filters": [ "And", [ ["page_id", "In", ["page1", "page2"]], [ "Or", [ ["public", "Eq", 1], ["permission_id", "In", ["3iQK2VC4", "wzw8zpnQ"]] ] ] ] ] ``` Filters can also be applied to the `id` field, which refers to the document id. ### Filtering Parameters **And** array[filter] Matches if all of the filters match. **Or** array[filter] Matches if at least one of the filters matches. **Not** filter Matches if the filter does not match. --- **Eq** id or value Exact match for `id` or `attributes` values. If value is `null`, matches documents missing the attribute. **NotEq** value Inverse of `Eq`, for `attributes` values. If value is `null`, matches documents with the attribute. --- **In** array[value] Matches any `attributes` values contained in the provided list. **NotIn** array[value] Inverse of `In`, matches any `attributes` values not contained in the provided list. --- **Contains** value Checks whether the selected array attribute contains the provided value **NotContains** value Inverse of Contains **ContainsAny** array[value] Checks whether the selected array attribute contains any of the values provided (intersection filter) **NotContainsAny** array[value] Inverse of ContainsAny --- **Lt** value For ints, this is a numeric less-than on `attributes` values. For strings, lexicographic less-than. For datetimes, numeric less-than on millisecond representation. Note that this matches `null` attribute values unless the value passed to `Lt` is `null` itself. **Lte** value For ints, this is a numeric less-than-or-equal on `attributes` values. For strings, lexicographic less-than-or-equal. For datetimes, numeric less-than-or-equal on millisecond representation. Note that this matches `null` attribute values. **Gt** value For ints, this is a numeric greater-than on `attributes` values. For strings, lexicographic greater-than. For datetimes, numeric greater-than on millisecond representation. **Gte** value For ints, this is a numeric greater-than-or-equal on `attributes` values. For strings, lexicographic greater-than-or-equal. For datetimes, numeric greater-than-or-equal on millisecond representation. --- **AnyLt** value Checks whether any element of an array attribute is less than the provided value, using the same rules as [`Lt`](#param-Lt). **AnyLte** value Checks whether any element of an array attribute is less than or equal to the provided value, using the same rules as [`Lte`](#param-Lte). **AnyGt** value Checks whether any element of an array attribute is greater than the provided value, using the same rules as [`Gt`](#param-Gt). **AnyGte** value Checks whether any element of an array attribute is greater than or equal to the provided value, using the same rules as [`Gte`](#param-Gte). --- **Glob** globset Unix-style glob match against `string` or `[]string` attribute values. The full syntax is described in the [globset](https://docs.rs/globset/latest/globset/#syntax) documentation. Requires the [glob](/docs/write#param-glob) (or for backwards compatibility, [filterable](/docs/write#param-filterable)) schema attribute to be enabled before use. **NotGlob** globset Inverse of `Glob`, Unix-style glob filters against `string` or `[]string` attribute values. The full syntax is described in the [globset](https://docs.rs/globset/latest/globset/#syntax) documentation. Requires the [glob](/docs/write#param-glob) (or for backwards compatibility, [filterable](/docs/write#param-filterable)) schema attribute to be enabled before use. **IGlob** globset Case insensitive version of `Glob`. **NotIGlob** globset Case insensitive version of `NotGlob`. --- **Regex** string Regular expression match against `string` attribute values. Requires the [regex schema attribute](/docs/write#param-regex) to be enabled before use. ```json // matches "swordfish", "pufferfish", "clownfish", … "filters": [ "text", "Regex", "\\w+fish" ] ``` **Warning:** Doesn't support certain advanced features (e.g. look-around, backreferences). Currently requires exhaustive evaluation; not recommended for large namespaces or ANN queries unless used in conjunction with other selective filters. [Contact us](/contact) if you run into performance problems. --- **Fuzzy** string Fuzzy substring match against `string` or `[]string` attribute values. Requires the [fuzzy schema attribute](/docs/write#param-fuzzy) to be enabled before use. See [Full-Text Search](/docs/fts#fuzzy-matching) for more details. - `max_edit_distance` (required) sets how many edits to tolerate by the query length in characters (uses [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance)). `distance` can be `0`, `1`, or `2`, and `min_query_chars` must be at least 3 · (`distance` + 1). Queries shorter than the first `min_query_chars` threshold return no matches. - `case_sensitive` (optional; defaults to `true`) controls case sensitivity. When `true`, the edit distance between `M` and `m` is 1. ```jsonc { // Will match substrings within edit distance 1, // since query has >= 6 chars. // If query was < 3 chars, would match nothing "filters": ["company_name", "Fuzzy", "turbopufer", { "max_edit_distance": [ {"min_query_chars": 3, "distance": 0}, {"min_query_chars": 6, "distance": 1}, ], "case_sensitive": false }] } ``` --- **ContainsAllTokens** string Matches documents that contain all the tokens present in the filter input string. If you need tokens to be adjacent and in order, use `ContainsTokenSequence` instead. See [phrase matching](#phrase-matching) for usage examples. Requires that the attribute is configured for [full-text search](/docs/fts). Supports [prefix queries](#prefix-queries) by providing an options object as the fourth parameter with `"last_as_prefix": true`. Prefixes match using byte representations, e.g. "🧑" is a prefix of "🧑‍💻". --- **ContainsTokenSequence** string Matches documents that contain all the tokens present in the input string, in the exact order and adjacent to each other. See [phrase matching](#phrase-matching) for usage examples. Requires that the attribute is configured for [full-text search](/docs/fts). --- **ContainsAnyToken** string Matches documents that contain any of the tokens present in the filter input string. Requires that the attribute is configured for [full-text search](/docs/fts). Supports [prefix queries](#prefix-queries) in the same way as `ContainsAllTokens`. ### Complex Example Using nested `And` and `Or` filters: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-complex-filter-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": [ "vector", "ANN", [0.1, 0.1] ], "limit": 10, "exclude_attributes": ["vector", "filename"], "filters": [ "And", [ ["id", "In", [1, 2, 3]], ["key1", "Eq", "one"], ["filename", "NotGlob", "/vendor/**"], [ "Or", [ ["filename", "Glob", "**.tsx"], ["filename", "Glob", "**.js"] ] ] ] ] }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-complex-filter-example-py') # If an error occurs, this call raises a turbopuffer.APIError if a retry was not successful. result = ns.query( rank_by=("vector", "ANN", [0.1, 0.1]), limit=10, exclude_attributes=["vector", "filename"], filters=('And', ( ('id', 'In', [1, 2, 3]), ('key1', 'Eq', 'one'), ('filename', 'NotGlob', '/vendor/**'), ('Or', [ ('filename', 'Glob', '**.tsx'), ('filename', 'Glob', '**.js'), ]), )) ) print(result.rows) # Returns a row-oriented VectorResult ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-complex-filter-example-ts`); const result = await ns.query({ rank_by: [ "vector", "ANN", [0.1, 0.1], ], limit: 10, exclude_attributes: ["vector", "filename"], filters: [ "And", [ ["id", "In", [1, 2, 3]], ["key1", "Eq", "one"], ["filename", "NotGlob", "/vendor/**"], [ "Or", [ ["filename", "Glob", "**.tsx"], ["filename", "Glob", "**.js"], ], ], ], ], }); console.log(result.rows); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-complex-filter-example-go") // If an error occurs, this call raises an error if a retry was not successful. result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAnn("vector", []float32{0.1, 0.1}), Limit: turbopuffer.LimitParam{ Total: 10, }, ExcludeAttributes: []string{"vector", "filename"}, Filters: turbopuffer.NewFilterAnd([]turbopuffer.Filter{ turbopuffer.NewFilterIn("id", []int{1, 2, 3}), turbopuffer.NewFilterEq("key1", "one"), turbopuffer.NewFilterNotGlob("filename", "/vendor/**"), turbopuffer.NewFilterOr([]turbopuffer.Filter{ turbopuffer.NewFilterGlob("filename", "**.tsx"), turbopuffer.NewFilterGlob("filename", "**.js"), }), }), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) // Returns a row-oriented result } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryComplexFilter { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-complex-filter-example-java"); // If an error occurs, this call raises a TurbopufferServiceException if a // retry was not successful. var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.ann("vector", List.of(0.1f, 0.1f))) .limit(10) .excludeAttributes(List.of("vector", "filename")) .filters( Filter.and( Filter.in("id", List.of(1, 2, 3)), Filter.eq("key1", "one"), Filter.notGlob("filename", "/vendor/**"), Filter.or(Filter.glob("filename", "**.tsx"), Filter.glob("filename", "**.js")) ) ) .build() ); System.out.println(queryResult.rows().get()); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-complex-filter-example-csharp"); // If an error occurs, this call raises a TurbopufferApiException if a // retry was not successful. var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", new[] { 0.1f, 0.1f }), Limit = 10, ExcludeAttributes = new List { "vector", "filename" }, Filters = Filter.And( Filter.In("id", new[] { 1, 2, 3 }), Filter.Eq("key1", "one"), Filter.NotGlob("filename", "/vendor/**"), Filter.Or(Filter.Glob("filename", "**.tsx"), Filter.Glob("filename", "**.js")) ), } ); foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } // Returns a row-oriented result ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-complex-filter-example-rb") # If an error occurs, this call raises a Turbopuffer::Errors::APIError if a retry was not successful. result = ns.query( rank_by: [ "vector", "ANN", [0.1, 0.1], ], limit: 10, exclude_attributes: ["vector", "filename"], filters: [ "And", [ ["id", "In", [1, 2, 3]], ["key1", "Eq", "one"], ["filename", "NotGlob", "/vendor/**"], [ "Or", [ ["filename", "Glob", "**.tsx"], ["filename", "Glob", "**.js"], ], ], ], ], ) puts result.rows # Returns a row-oriented result ``` ## Diversification The [limit.per](#param-limit) parameter is a simple mechanism for increasing the diversity of results. For example, to ensure that no category appears more than five times in the results: ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-limit-per-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": [ "id", "asc" ], "filters": [ "product_name", "ContainsAllTokens", "red cotton" ], "limit": { "per": {"attributes": ["category"], "limit": 5}, "total": 50 }, "include_attributes": ["category"] }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-limit-per-example-py') result = ns.query( rank_by=('id', 'asc'), filters=('product_name', 'ContainsAllTokens', 'red cotton'), # No more than 5 docs per category limit={ 'per': {'attributes': ['category'], 'limit': 5}, 'total': 50, }, include_attributes=['category'], ) print(result.rows) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-limit-per-example-ts`); const result = await ns.query({ rank_by: [ "id", "asc", ], filters: [ "product_name", "ContainsAllTokens", "red cotton", ], // No more than 5 docs per category limit: { per: { attributes: ["category"], limit: 5 }, total: 50, }, include_attributes: ["category"], }); console.log(result.rows); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-limit-per-example-go") result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAttribute("id", turbopuffer.RankByAttributeOrderAsc), Filters: turbopuffer.NewFilterContainsAllTokens("product_name", "red cotton"), // No more than 5 docs per category Limit: turbopuffer.LimitParam{ Total: 50, Per: turbopuffer.LimitPerParam{ Attributes: []string{"category"}, Limit: 5, }, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{ StringArray: []string{"category"}, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryLimitPer { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-limit-per-example-java"); var result = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.attribute("id", RankByAttributeOrder.ASC)) .filters(Filter.containsAllTokens("product_name", "red cotton")) // No more than 5 docs per category .limit( Limit.builder() .total(50L) .per(Limit.Per.builder().attributes(List.of("category")).limit(5L).build()) .build() ) .includeAttributes("category") .build() ); System.out.println(result.rows().get()); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-limit-per-example-csharp"); var result = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Attribute("id", RankByAttributeOrder.ASC), Filters = Filter.ContainsAllTokens("product_name", "red cotton"), // No more than 5 docs per category Limit = new NamespaceLimit { Total = 50, Per = new Per { Attributes = new List { "category" }, Limit = 5 }, }, IncludeAttributes = new List { "category" }, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-limit-per-example-rb") result = ns.query( rank_by: [ "id", "asc", ], filters: [ "product_name", "ContainsAllTokens", "red cotton", ], # No more than 5 docs per category limit: { per: { attributes: ["category"], limit: 5 }, total: 50, }, include_attributes: ["category"], ) puts result.rows ``` ## Pagination For full-text and vector search, you have two main options. If you're letting your users paginate through hits with an infinite scrolling experience, the best option is to create a filter that excludes ids that have already been rendered. This preserves a smooth user experience in the case when a write operation changes the top hits of the query after the first page is retrieved. ```json { "limit": 20, "rank_by": [...], "filters": [ "id", "NotIn", [...] ] } ``` If you're letting users jump to arbitrary page numbers, pass a larger `limit` value and ignore hits which belong to previous pages. This is what other searches engines do internally when you pass an offset, which we have not exposed yet. In case your users use pagination heavily, you may want to request a large number of hits in the first place, cache them, and implement pagination on the client side. When [Ordering by Attributes](#ordering-by-attributes), you can page through results by advancing a filter on the order attribute. For example, to paginate by id, advance a greater-than filter on id: ```python from datetime import datetime import turbopuffer from turbopuffer.types import Filter from typing import List tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-pagination-example-py') last_id = None while True: filters: List[Filter] = [('timestamp', 'Gte', datetime(2024, 1, 1, 0, 0, 0))] if last_id is not None: filters.append(('id', 'Gt', last_id)) result = ns.query( rank_by=('id', 'asc'), limit=1000, filters=('And', filters), ) print(result) if len(result.rows) < 1000: break last_id = result.rows[-1].id ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; import { Filter } from "@turbopuffer/turbopuffer/resources"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-pagination-example-ts`); let lastId: string | number | null = null; while (true) { const filters: Filter[] = [ ["timestamp", "Gte", new Date("2024-01-01").toISOString()], ]; if (lastId !== null) filters.push(["id", "Gt", lastId]); const result = await ns.query({ rank_by: [ "id", "asc", ], limit: 1000, filters: [ "And", filters, ], }); console.log(result.rows); if (result.rows!.length < 1000) break; lastId = result.rows![result.rows!.length - 1].id; } ``` ```go package main import ( "context" "fmt" "os" "time" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-pagination-example-go") var lastID any for { filters := []turbopuffer.Filter{ turbopuffer.NewFilterGte("timestamp", time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), } if lastID != nil { filter := turbopuffer.NewFilterGt("id", lastID) filters = append(filters, filter) } result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAttribute("id", turbopuffer.RankByAttributeOrderAsc), Limit: turbopuffer.LimitParam{ Total: 1000, }, Filters: turbopuffer.NewFilterAnd(filters), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) if len(result.Rows) < 1000 { break } lastID = result.Rows[len(result.Rows)-1]["id"] } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.time.*; import java.util.*; import java.util.stream.*; public class QueryPagination { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-pagination-example-java"); JsonValue lastId = null; while (true) { Filter filters = Filter.gte( "timestamp", ZonedDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC) ); if (lastId != null) { filters = Filter.and(filters, Filter.gt("id", lastId)); } var result = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.attribute("id", RankByAttributeOrder.ASC)) .limit(1000) .filters(filters) .build() ); var rows = result.rows().get(); // Do something with the page of results. System.out.println(rows); if (rows.size() < 1000) { break; } lastId = rows.get(rows.size() - 1).get("id"); } } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using System.Text.Json; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-pagination-example-csharp"); JsonElement? lastId = null; while (true) { Filter filters = Filter.Gte("timestamp", new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)); if (lastId != null) { filters = Filter.And(filters, Filter.Gt("id", lastId)); } var result = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Attribute("id", RankByAttributeOrder.ASC), Limit = 1000, Filters = filters, } ); var rows = result.GetRows(); // Do something with the page of results. foreach (var row in rows) { Console.WriteLine(row); } if (rows.Count < 1000) { break; } lastId = rows[^1]["id"]; } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-pagination-example-rb") last_id = nil loop do filters = [["timestamp", "Gte", DateTime.new(2024, 1, 1, 0, 0, 0)]] if last_id filters << ["id", "Gt", last_id] end result = ns.query( rank_by: [ "id", "asc", ], limit: 1000, filters: [ "And", filters, ], ) puts result.rows break if result.rows.length < 1000 last_id = result.rows.last.id end ``` ## kNN (Exact Search) Use `kNN` instead of `ANN` when you need exact nearest neighbor results rather than approximate results. kNN performs an exhaustive search, computing the exact distance from the query vector to every document matching the filters. Because kNN computes distances to all matching documents, latency scales linearly with the number of documents matching the filters and can be significantly higher than ANN queries. **Requirements:** - kNN queries **require filters** to be specified to bound the search space - Without filters, use `ANN` which leverages the vector index for fast approximate search **When to use kNN vs ANN:** - Use `ANN` (default) for large-scale search where slight approximation is acceptable and speed matters - Use `kNN` when you need guaranteed exact results over a filtered subset of your data ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/query-knn-example-curl/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": [ "vector", "kNN", [0.1, 0.1] ], "filters": [ "category", "Eq", "A" ], "limit": 10 }' # Response payload # { # "rows": [ # { "$dist": 0.2, "id": 1 }, # { "$dist": 0.7, "id": 2 } # ] # } ``` ```python # $ pip install turbopuffer import turbopuffer import os tpuf = turbopuffer.Turbopuffer( # API tokens are created in the dashboard: https://turbopuffer.com/dashboard api_key=os.getenv("TURBOPUFFER_API_KEY"), region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'query-knn-example-py') # kNN performs exact nearest neighbor search over filtered results result = ns.query( rank_by=("vector", "kNN", [0.1, 0.1]), filters=("category", "Eq", "A"), limit=10, ) print(result.rows) # Prints a list of row-oriented documents: # [ # Row(id=1, vector=None, $dist=0.2), # Row(id=2, vector=None, $dist=0.7) # ] ``` ```typescript // $ npm install @turbopuffer/turbopuffer import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ // API tokens are created in the dashboard: https://turbopuffer.com/dashboard apiKey: process.env.TURBOPUFFER_API_KEY, region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`query-knn-example-ts`); // kNN performs exact nearest neighbor search over filtered results const result = await ns.query({ rank_by: [ "vector", "kNN", [0.1, 0.1], ], filters: [ "category", "Eq", "A", ], limit: 10, }); console.log(result.rows); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( // API tokens are created in the dashboard: https://turbopuffer.com/dashboard option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("query-knn-example-go") // kNN performs exact nearest neighbor search over filtered results result, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByKnn("vector", []float32{0.1, 0.1}), Filters: turbopuffer.NewFilterEq("category", "A"), Limit: turbopuffer.LimitParam{ Total: 10, }, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result.Rows)) // Returns a row-oriented result: // [ // {"id": 1, "$dist": 0.2}, // {"id": 2, "$dist": 0.7} // ] } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QueryKnn { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() // API tokens are created in the dashboard: https://turbopuffer.com/dashboard .apiKey(System.getenv("TURBOPUFFER_API_KEY")) .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("query-knn-example-java"); // kNN performs exact nearest neighbor search over filtered results var result = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.knn("vector", List.of(0.1f, 0.1f))) .filters(Filter.eq("category", "A")) .limit(10) .build() ); System.out.println(result.rows().get()); // Prints a list of row-oriented documents: // [ // {id=1, $dist=0.2}, // {id=2, $dist=0.7} // ] } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // API tokens are created in the dashboard: https://turbopuffer.com/dashboard // Loaded from TURBOPUFFER_API_KEY env var by default. Override if necessary: // ApiKey = "...", // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("query-knn-example-csharp"); // kNN performs exact nearest neighbor search over filtered results var result = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Knn("vector", new[] { 0.1f, 0.1f }), Filters = Filter.Eq("category", "A"), Limit = 10, } ); foreach (var row in result.GetRows()) { Console.WriteLine(row); } // Prints a list of row-oriented documents: // {"$dist": 0.2, "id": 1} // {"$dist": 0.7, "id": 2} ``` ```ruby # $ gem install turbopuffer require "turbopuffer" tpuf = Turbopuffer::Client.new( # API tokens are created in the dashboard: https://turbopuffer.com/dashboard api_key: ENV["TURBOPUFFER_API_KEY"], region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("query-knn-example-rb") # kNN performs exact nearest neighbor search over filtered results result = ns.query( rank_by: [ "vector", "kNN", [0.1, 0.1], ], filters: [ "category", "Eq", "A", ], limit: 10, ) puts result.rows # Prints a list of row-oriented documents: # {id: 1, dist: 0.2} # {id: 2, dist: 0.7} ``` --- This page: [/docs/query.md](https://turbopuffer.com/docs/query.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Quickstart Guide **100B Vector Search** - p50: 46ms - p90: 61ms - p99: 185ms **Vector Query** (768 dimensions, f16, 10M docs, ~15GB. Strongly consistent.) - warm (10M docs): p50=14ms, p90=17ms, p99=27ms - cold (10M docs): p50=874ms, p90=1214ms, p99=1686ms **Upsert** (Time for the batch to be durably acknowledged by object storage. Documents are immediately available to consistent reads after this.) - Upsert latency (512kb docs): p50=165ms, p90=248ms, p99=850ms Walk a tiny namespace through the core loop: connect, write rows with vectors and attributes, query them, simple aggregations, then layer on conditional writes and branching. If you are an agent, you may wish to [read the full documentation in Markdown](/llms-full.txt). ## Connect 1. Install an SDK: 2. Create an [API key](https://turbopuffer.com/dashboard) from the Dashboard. The snippets default to [`gcp-us-central1`](/docs/regions); change it to your preferred region if needed. 3. Choose an embedding provider. Pick from the dropdown in the code sample below, or use random vectors to start (don't use in production or for benchmarking). ```bash # Later steps use curl to call the turbopuffer HTTP API (usually preinstalled). # Set your API key and a fresh namespace for this run. api_key="${TURBOPUFFER_API_KEY:-your-api-key}" # created here: https://turbopuffer.com/dashboard TPUF_URL="https://gcp-us-central1.turbopuffer.com" # choose best region: https://turbopuffer.com/docs/regions TPUF_NAMESPACE="${TPUF_NAMESPACE:-quickstart-$(date +%s)-$$}" TPUF_BRANCH_NAMESPACE="${TPUF_BRANCH_NAMESPACE:-$TPUF_NAMESPACE-branch}" ``` ```python import os import uuid import random from typing import List import turbopuffer tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) def embed(_text: str) -> List[float]: return [random.random(), random.random()] ``` ```python # $ pip install turbopuffer sentence-transformers import os import uuid from typing import List import turbopuffer from sentence_transformers import SentenceTransformer tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) # Local embeddings with BGE — no API key needed. # Model is downloaded on first run (~130 MB). bge = SentenceTransformer("BAAI/bge-small-en-v1.5") def embed(text: str) -> List[float]: return bge.encode(text).tolist() ``` ```python # $ pip install turbopuffer cohere import os import uuid from typing import List import turbopuffer from cohere import ClientV2 tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) cohere = ClientV2(api_key=os.environ["COHERE_API_KEY"]) def embed(text: str) -> List[float]: return cohere.embed( model="embed-v4.0", input_type="search_document", texts=[text], embedding_types=["float"], ).embeddings.float[0] ``` ```python # $ pip install turbopuffer google-genai import os import uuid from typing import List import turbopuffer from google.genai import Client from google.genai.types import EmbedContentConfig tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) gemini = Client(api_key=os.environ["GEMINI_API_KEY"]) def embed(text: str) -> List[float]: return gemini.models.embed_content( model="gemini-embedding-001", contents=text, config=EmbedContentConfig(task_type="RETRIEVAL_DOCUMENT"), ).embeddings[0].values ``` ```python # $ pip install turbopuffer openai import os import uuid from typing import List import turbopuffer from openai import OpenAI tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) fireworks = OpenAI( api_key=os.environ["FIREWORKS_API_KEY"], base_url="https://api.fireworks.ai/inference/v1", ) def embed(text: str) -> List[float]: return fireworks.embeddings.create( model="fireworks/qwen3-embedding-8b", input=text, ).data[0].embedding ``` ```python # $ pip install turbopuffer voyageai import os import uuid from typing import List import turbopuffer import voyageai tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) voyage = voyageai.Client(api_key=os.environ["VOYAGE_API_KEY"]) def embed(text: str) -> List[float]: return voyage.embed( [text], model="voyage-4-lite" ).embeddings[0] ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); function embed(_text: string): number[] { return [Math.random(), Math.random()]; } ``` ```typescript // $ npm install @turbopuffer/turbopuffer cohere-ai import { Turbopuffer } from "@turbopuffer/turbopuffer"; import { CohereClient } from "cohere-ai"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); const cohere = new CohereClient({ token: process.env.COHERE_API_KEY }); async function embed(text: string): Promise { return (await cohere.v2.embed({ model: "embed-v4.0", inputType: "search_document", texts: [text], embeddingTypes: ["float"], })).embeddings.float![0]; } ``` ```typescript // $ npm install @turbopuffer/turbopuffer @google/genai import { Turbopuffer } from "@turbopuffer/turbopuffer"; import { GoogleGenAI } from "@google/genai"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); const gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); async function embed(text: string): Promise { return ( await gemini.models.embedContent({ model: "gemini-embedding-001", contents: text, config: { taskType: "RETRIEVAL_DOCUMENT" }, }) ).embeddings![0].values!; } ``` ```typescript // $ npm install @turbopuffer/turbopuffer openai import { Turbopuffer } from "@turbopuffer/turbopuffer"; import OpenAI from "openai"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); const fireworks = new OpenAI({ apiKey: process.env.FIREWORKS_API_KEY, baseURL: "https://api.fireworks.ai/inference/v1", }); async function embed(text: string): Promise { return ( await fireworks.embeddings.create({ model: "fireworks/qwen3-embedding-8b", input: text, encoding_format: "float", }) ).data[0].embedding; } ``` ```typescript // $ npm install @turbopuffer/turbopuffer voyageai import { Turbopuffer } from "@turbopuffer/turbopuffer"; import { VoyageAIClient } from "voyageai"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); const voyage = new VoyageAIClient({ apiKey: process.env.VOYAGE_API_KEY, }); async function embed(text: string): Promise { return (await voyage.embed({ input: text, model: "voyage-4-lite", })).data[0].embedding; } ``` ```go package main import ( "context" "fmt" "math/rand" "os" "time" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func embed(_ context.Context, _ string) []float32 { return []float32{rand.Float32(), rand.Float32()} } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // created here: https://turbopuffer.com/dashboard option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespace := os.Getenv("TURBOPUFFER_NAMESPACE") if namespace == "" { namespace = fmt.Sprintf("quickstart-%d", time.Now().UnixNano()) } ns := tpuf.Namespace(namespace) ``` ```go // $ go get github.com/turbopuffer/turbopuffer-go github.com/cohere-ai/cohere-go/v2 package main import ( "context" "fmt" "os" "time" cohere "github.com/cohere-ai/cohere-go/v2" cohereclient "github.com/cohere-ai/cohere-go/v2/client" "github.com/turbopuffer/turbopuffer-go" "github.com/turbopuffer/turbopuffer-go/option" ) func embed(ctx context.Context, text string) []float32 { resp, err := cohereclient.NewClient( cohereclient.WithToken(os.Getenv("COHERE_API_KEY")), ).V2.Embed(ctx, &cohere.V2EmbedRequest{ Model: "embed-v4.0", InputType: cohere.EmbedInputTypeSearchDocument, Texts: []string{text}, EmbeddingTypes: []cohere.EmbeddingType{cohere.EmbeddingTypeFloat}, }) if err != nil { panic(err) } return toFloat32Slice(resp.GetEmbeddings().GetFloat()[0]) } func toFloat32Slice(values []float64) []float32 { out := make([]float32, len(values)) for i, v := range values { out[i] = float32(v) } return out } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // created here: https://turbopuffer.com/dashboard option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespace := os.Getenv("TURBOPUFFER_NAMESPACE") if namespace == "" { namespace = fmt.Sprintf("quickstart-%d", time.Now().UnixNano()) } ns := tpuf.Namespace(namespace) ``` ```go // $ go get github.com/turbopuffer/turbopuffer-go google.golang.org/genai package main import ( "context" "fmt" "os" "time" "github.com/turbopuffer/turbopuffer-go" "github.com/turbopuffer/turbopuffer-go/option" "google.golang.org/genai" ) func embed(ctx context.Context, text string) []float32 { client, err := genai.NewClient(ctx, &genai.ClientConfig{ APIKey: os.Getenv("GEMINI_API_KEY"), Backend: genai.BackendGeminiAPI, }) if err != nil { panic(err) } defer client.Close() resp, err := client.Models.EmbedContent( ctx, "gemini-embedding-001", []*genai.Content{genai.NewContentFromText(text, genai.RoleUser)}, &genai.EmbedContentRequest{TaskType: genai.TaskTypeRetrievalDocument}, ) if err != nil { panic(err) } return resp.Embeddings[0].Values } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // created here: https://turbopuffer.com/dashboard option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespace := os.Getenv("TURBOPUFFER_NAMESPACE") if namespace == "" { namespace = fmt.Sprintf("quickstart-%d", time.Now().UnixNano()) } ns := tpuf.Namespace(namespace) ``` ```go // $ go get github.com/turbopuffer/turbopuffer-go github.com/openai/openai-go package main import ( "context" "fmt" "os" "time" "github.com/openai/openai-go" openaioption "github.com/openai/openai-go/option" "github.com/turbopuffer/turbopuffer-go" tpufoption "github.com/turbopuffer/turbopuffer-go/option" ) func embed(ctx context.Context, text string) []float32 { resp, err := openai.NewClient( openaioption.WithAPIKey(os.Getenv("FIREWORKS_API_KEY")), openaioption.WithBaseURL("https://api.fireworks.ai/inference/v1"), ).Embeddings.New(ctx, openai.EmbeddingNewParams{ Input: openai.EmbeddingNewParamsInputUnion{OfString: openai.String(text)}, Model: openai.EmbeddingModel("fireworks/qwen3-embedding-8b"), }) if err != nil { panic(err) } return toFloat32Slice(resp.Data[0].Embedding) } func toFloat32Slice(values []float64) []float32 { out := make([]float32, len(values)) for i, v := range values { out[i] = float32(v) } return out } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( tpufoption.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // created here: https://turbopuffer.com/dashboard tpufoption.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespace := os.Getenv("TURBOPUFFER_NAMESPACE") if namespace == "" { namespace = fmt.Sprintf("quickstart-%d", time.Now().UnixNano()) } ns := tpuf.Namespace(namespace) ``` ```go // $ go get github.com/turbopuffer/turbopuffer-go github.com/openai/openai-go package main import ( "context" "fmt" "os" "time" "github.com/openai/openai-go" openaioption "github.com/openai/openai-go/option" "github.com/turbopuffer/turbopuffer-go" tpufoption "github.com/turbopuffer/turbopuffer-go/option" ) func embed(ctx context.Context, text string) []float32 { resp, err := openai.NewClient( openaioption.WithAPIKey(os.Getenv("VOYAGE_API_KEY")), openaioption.WithBaseURL("https://api.voyageai.com/v1"), ).Embeddings.New(ctx, openai.EmbeddingNewParams{ Input: openai.EmbeddingNewParamsInputUnion{OfString: openai.String(text)}, Model: openai.EmbeddingModel("voyage-4-lite"), }) if err != nil { panic(err) } return toFloat32Slice(resp.Data[0].Embedding) } func toFloat32Slice(values []float64) []float32 { out := make([]float32, len(values)) for i, v := range values { out[i] = float32(v) } return out } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( tpufoption.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // created here: https://turbopuffer.com/dashboard tpufoption.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespace := os.Getenv("TURBOPUFFER_NAMESPACE") if namespace == "" { namespace = fmt.Sprintf("quickstart-%d", time.Now().UnixNano()) } ns := tpuf.Namespace(namespace) ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class DefaultConnect { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // created here: https://turbopuffer.com/dashboard .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespace = Optional.ofNullable(System.getenv("TURBOPUFFER_NAMESPACE")).orElse( "quickstart-" + UUID.randomUUID().toString().substring(0, 8) ); var ns = tpuf.namespace(namespace); } public static List embed(String text) { Random rand = new Random(); return List.of(rand.nextFloat(), rand.nextFloat()); } } ``` ```java // Gradle: implementation("com.turbopuffer:turbopuffer-java:+"), implementation("com.cohere:cohere-java:+") package com.turbopuffer.docs; import com.cohere.api.Cohere; import com.cohere.api.resources.v2.requests.V2EmbedRequest; import com.cohere.api.types.EmbedInputType; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class CohereConnect { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // created here: https://turbopuffer.com/dashboard .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespace = Optional.ofNullable(System.getenv("TURBOPUFFER_NAMESPACE")).orElse( "quickstart-" + UUID.randomUUID().toString().substring(0, 8) ); var ns = tpuf.namespace(namespace); } public static List embed(String text) { var response = Cohere.builder() .token(System.getenv("COHERE_API_KEY")) .clientName("turbopuffer-quickstart") .build() .v2() .embed( V2EmbedRequest.builder() .model("embed-v4.0") .inputType(EmbedInputType.SEARCH_DOCUMENT) .texts(List.of(text)) .build() ); return response .getEmbeddings() .getFloat() .orElseThrow() .get(0) .stream() .map(Double::floatValue) .toList(); } } ``` ```java // Gradle: implementation("com.turbopuffer:turbopuffer-java:+"), implementation("com.google.genai:google-genai:+") package com.turbopuffer.docs; import com.google.genai.Client; import com.google.genai.types.EmbedContentConfig; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class GeminiConnect { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // created here: https://turbopuffer.com/dashboard .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespace = Optional.ofNullable(System.getenv("TURBOPUFFER_NAMESPACE")).orElse( "quickstart-" + UUID.randomUUID().toString().substring(0, 8) ); var ns = tpuf.namespace(namespace); } public static List embed(String text) { try (Client client = Client.builder().apiKey(System.getenv("GEMINI_API_KEY")).build()) { return client.models .embedContent( "gemini-embedding-001", text, EmbedContentConfig.builder().taskType("RETRIEVAL_DOCUMENT").build() ) .embeddings() .orElseThrow() .get(0) .values() .orElseThrow(); } } } ``` ```java // Gradle: implementation("com.turbopuffer:turbopuffer-java:+"), implementation("com.openai:openai-java:+") package com.turbopuffer.docs; import com.openai.client.okhttp.*; import com.openai.models.embeddings.*; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QwenConnect { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // created here: https://turbopuffer.com/dashboard .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespace = Optional.ofNullable(System.getenv("TURBOPUFFER_NAMESPACE")).orElse( "quickstart-" + UUID.randomUUID().toString().substring(0, 8) ); var ns = tpuf.namespace(namespace); } public static List embed(String text) { var client = OpenAIOkHttpClient.builder() .apiKey(System.getenv("FIREWORKS_API_KEY")) .baseUrl("https://api.fireworks.ai/inference/v1") .build(); return client .embeddings() .create( EmbeddingCreateParams.builder() .input(text) .model(EmbeddingModel.of("fireworks/qwen3-embedding-8b")) .build() ) .data() .get(0) .embedding(); } } ``` ```java // Gradle: implementation("com.turbopuffer:turbopuffer-java:+"), implementation("com.openai:openai-java:+") package com.turbopuffer.docs; import com.openai.client.okhttp.*; import com.openai.models.embeddings.*; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class VoyageConnect { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // created here: https://turbopuffer.com/dashboard .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespace = Optional.ofNullable(System.getenv("TURBOPUFFER_NAMESPACE")).orElse( "quickstart-" + UUID.randomUUID().toString().substring(0, 8) ); var ns = tpuf.namespace(namespace); } public static List embed(String text) { var client = OpenAIOkHttpClient.builder() .apiKey(System.getenv("VOYAGE_API_KEY")) .baseUrl("https://api.voyageai.com/v1") .build(); return client .embeddings() .create( EmbeddingCreateParams.builder() .input(text) .model(EmbeddingModel.of("voyage-4-lite")) .build() ) .data() .get(0) .embedding(); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using var tpuf = new TurbopufferClient { // API tokens are created in the dashboard: https://turbopuffer.com/dashboard Region = "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }; var namespaceName = Environment.GetEnvironmentVariable("TURBOPUFFER_NAMESPACE") ?? $"quickstart-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; var ns = tpuf.Namespace(namespaceName); static List Embed(string text) => new() { (float)Random.Shared.NextDouble(), (float)Random.Shared.NextDouble() }; ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using Turbopuffer; using var tpuf = new TurbopufferClient { Region = "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }; var namespaceName = Environment.GetEnvironmentVariable("TURBOPUFFER_NAMESPACE") ?? $"quickstart-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; var ns = tpuf.Namespace(namespaceName); // Create an embedding with Cohere. // Requires COHERE_API_KEY to be set (https://dashboard.cohere.com/api-keys) static List Embed(string text) { using var http = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.cohere.com/v2/embed") { Content = new StringContent( JsonSerializer.Serialize( new { model = "embed-v4.0", input_type = "search_document", texts = new[] { text }, embedding_types = new[] { "float" }, } ), Encoding.UTF8, "application/json" ), }; request.Headers.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("COHERE_API_KEY") ); var response = http.Send(request); response.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(response.Content.ReadAsStream()); return doc .RootElement.GetProperty("embeddings") .GetProperty("float")[0] .EnumerateArray() .Select(v => v.GetSingle()) .ToList(); } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Text.Json; using Turbopuffer; using var tpuf = new TurbopufferClient { Region = "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }; var namespaceName = Environment.GetEnvironmentVariable("TURBOPUFFER_NAMESPACE") ?? $"quickstart-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; var ns = tpuf.Namespace(namespaceName); // Create an embedding with Gemini. // Requires GEMINI_API_KEY to be set (https://aistudio.google.com/app/apikey) static List Embed(string text) { var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); using var http = new HttpClient(); var request = new HttpRequestMessage( HttpMethod.Post, "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=" + apiKey ) { Content = new StringContent( JsonSerializer.Serialize( new { model = "models/gemini-embedding-001", content = new { parts = new[] { new { text } } }, taskType = "RETRIEVAL_DOCUMENT", } ), Encoding.UTF8, "application/json" ), }; var response = http.Send(request); response.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(response.Content.ReadAsStream()); return doc .RootElement.GetProperty("embedding") .GetProperty("values") .EnumerateArray() .Select(v => v.GetSingle()) .ToList(); } ``` ```cs // dotnet add package Turbopuffer // dotnet add package OpenAI using System; using System.ClientModel; using System.Collections.Generic; using System.Linq; using OpenAI; using OpenAI.Embeddings; using Turbopuffer; using var tpuf = new TurbopufferClient { Region = "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }; var namespaceName = Environment.GetEnvironmentVariable("TURBOPUFFER_NAMESPACE") ?? $"quickstart-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; var ns = tpuf.Namespace(namespaceName); // Create an embedding with Qwen on Fireworks. // Requires FIREWORKS_API_KEY to be set (https://fireworks.ai/settings/users/api-keys) static List Embed(string text) { var client = new EmbeddingClient( "fireworks/qwen3-embedding-8b", new ApiKeyCredential(Environment.GetEnvironmentVariable("FIREWORKS_API_KEY")!), new OpenAIClientOptions { Endpoint = new Uri("https://api.fireworks.ai/inference/v1") } ); return [.. client.GenerateEmbedding(text).Value.ToFloats().Span]; } ``` ```cs // dotnet add package Turbopuffer // dotnet add package OpenAI using System; using System.ClientModel; using System.Collections.Generic; using System.Linq; using OpenAI; using OpenAI.Embeddings; using Turbopuffer; using var tpuf = new TurbopufferClient { Region = "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }; var namespaceName = Environment.GetEnvironmentVariable("TURBOPUFFER_NAMESPACE") ?? $"quickstart-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; var ns = tpuf.Namespace(namespaceName); // Create an embedding with Voyage. // Requires VOYAGE_API_KEY to be set: // https://dashboard.voyageai.com/organization/api-keys static List Embed(string text) { var client = new EmbeddingClient( "voyage-4-lite", new ApiKeyCredential(Environment.GetEnvironmentVariable("VOYAGE_API_KEY")!), new OpenAIClientOptions { Endpoint = new Uri("https://api.voyageai.com/v1") } ); return [.. client.GenerateEmbedding(text).Value.ToFloats().Span]; } ``` ```ruby require "turbopuffer" require "securerandom" tpuf = Turbopuffer::Client.new( api_key: ENV["TURBOPUFFER_API_KEY"], # created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = ENV["TURBOPUFFER_NAMESPACE"] || "quickstart-#{SecureRandom.hex(4)}" ns = tpuf.namespace(namespace) def embed(_text) [rand, rand] end ``` ```ruby # $ gem install turbopuffer cohere-ruby require "turbopuffer" require "securerandom" require "cohere" tpuf = Turbopuffer::Client.new( api_key: ENV["TURBOPUFFER_API_KEY"], # created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = ENV["TURBOPUFFER_NAMESPACE"] || "quickstart-#{SecureRandom.hex(4)}" ns = tpuf.namespace(namespace) def embed(text) Cohere::Client .new(api_key: ENV["COHERE_API_KEY"]) .embed( model: "embed-v4.0", texts: [text], input_type: "search_document", embedding_types: ["float"], ) .embeddings.float.first end ``` ```ruby # $ gem install turbopuffer openai require "turbopuffer" require "securerandom" require "openai" tpuf = Turbopuffer::Client.new( api_key: ENV["TURBOPUFFER_API_KEY"], # created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = ENV["TURBOPUFFER_NAMESPACE"] || "quickstart-#{SecureRandom.hex(4)}" ns = tpuf.namespace(namespace) def embed(text) OpenAI::Client .new( api_key: ENV["FIREWORKS_API_KEY"], base_url: "https://api.fireworks.ai/inference/v1", ) .embeddings .create(model: "fireworks/qwen3-embedding-8b", input: text) .data[0].embedding end ``` ```ruby # $ gem install turbopuffer openai require "turbopuffer" require "securerandom" require "openai" tpuf = Turbopuffer::Client.new( api_key: ENV["TURBOPUFFER_API_KEY"], # created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = ENV["TURBOPUFFER_NAMESPACE"] || "quickstart-#{SecureRandom.hex(4)}" ns = tpuf.namespace(namespace) def embed(text) OpenAI::Client .new( api_key: ENV["VOYAGE_API_KEY"], base_url: "https://api.voyageai.com/v1", ) .embeddings .create(model: "voyage-4-lite", input: text) .data[0].embedding end ``` ## Write [Upsert](/docs/write) documents with vectors, typed attributes, and [full-text search](/docs/fts) on `text` and `category` (with [regex](/docs/query#param-Regex) on `text`). ```bash curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ {"id": 1, "vector": [0.1, 0.2], "category": ["mammal"], "public": true, "text": "walrus narwhal"}, {"id": 2, "vector": [0.3, 0.4], "category": ["fish"], "public": false, "text": "pufferfish clownfish swordfish"} ], "schema": { "text": {"type": "string", "full_text_search": true, "regex": true}, "category": {"type": "[]string", "full_text_search": true} }, "distance_metric": "cosine_distance" }' ``` ```python ns.write( upsert_rows=[ { 'id': 1, 'vector': embed("walrus narwhal"), 'category': ["mammal"], 'public': True, 'text': "walrus narwhal", }, { 'id': 2, 'vector': embed("pufferfish clownfish swordfish"), 'category': ["fish"], 'public': False, 'text': "pufferfish clownfish swordfish", }, ], distance_metric='cosine_distance', schema={ "text": { "type": "string", "full_text_search": True, "regex": True, }, "category": { "type": "[]string", "full_text_search": True, }, } ) ``` ```typescript await ns.write({ upsert_rows: [ { id: 1, vector: embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { type: "string", full_text_search: true, regex: true }, category: { type: "[]string", full_text_search: true }, }, }); ``` ```go _, err := ns.Write(ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": embed(ctx, "walrus narwhal"), "category": []string{"mammal"}, "public": true, "text": "walrus narwhal", }, { "id": 2, "vector": embed(ctx, "pufferfish clownfish swordfish"), "category": []string{"fish"}, "public": false, "text": "pufferfish clownfish swordfish", }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "text": { Type: "string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, Regex: param.NewOpt(true), }, "category": { Type: "[]string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, }, }, }) if err != nil { panic(err) } ``` ```java ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("vector", embed("walrus narwhal")) .put("category", List.of("mammal")) .put("public", true) .put("text", "walrus narwhal") .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("vector", embed("pufferfish clownfish swordfish")) .put("category", List.of("fish")) .put("public", false) .put("text", "pufferfish clownfish swordfish") .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .schema( Schema.builder() .put( "text", AttributeSchemaConfig.builder() .type("string") .fullTextSearch(FullTextSearchConfig.defaults()) .regex(true) .build() ) .put( "category", AttributeSchemaConfig.builder() .type("[]string") .fullTextSearch(FullTextSearchConfig.defaults()) .build() ) .build() ) .build() ); ``` ```cs await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("vector", Embed("walrus narwhal")) .Set("category", new[] { "mammal" }) .Set("public", true) .Set("text", "walrus narwhal"), new Row() .Set("id", 2) .Set("vector", Embed("pufferfish clownfish swordfish")) .Set("category", new[] { "fish" }) .Set("public", false) .Set("text", "pufferfish clownfish swordfish"), ], DistanceMetric = DistanceMetric.CosineDistance, Schema = new Dictionary { ["text"] = new AttributeSchemaConfig { Type = "string", FullTextSearch = true, Regex = true, }, ["category"] = new AttributeSchemaConfig { Type = "[]string", FullTextSearch = true }, }, } ); ``` ```ruby ns.write( upsert_rows: [ { id: 1, vector: embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal" }, { id: 2, vector: embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish" }, ], distance_metric: "cosine_distance", schema: { text: { type: "string", full_text_search: true, regex: true }, category: { type: "[]string", full_text_search: true }, }, ) ``` ## Search Find documents by [vector similarity](/docs/vector) with filters, by [full-text search](/docs/fts) with a [boosted](/docs/query#field-weightsboosts) `category` field, or by [regex](/docs/query#param-Regex) (`\w+fish` matches "pufferfish", "swordfish", "clownfish"). To combine vector and FTS concurrently, see [hybrid search](/docs/hybrid). ```bash # Vector search with a filter curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": ["vector", "ANN", [0.1, 0.2]], "limit": 10, "filters": ["public", "Eq", true] }' # Full-text search with boosted category field curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "limit": 10, "filters": ["public", "Eq", true], "rank_by": ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"] ]] }' # Regex filter — matches "pufferfish", "swordfish", "clownfish" curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "limit": 10, "filters": ["text", "Regex", "\\w+fish"] }' ``` ```python # Vector search with a filter print(ns.query( rank_by=("vector", "ANN", embed("arctic sea mammal")), limit=10, filters=("public", "Eq", True), )) # Full-text search with boosted category field print(ns.query( limit=10, filters=("public", "Eq", True), rank_by=("Sum", [ ("Product", 2, ("category", "BM25", "mammal")), ("text", "BM25", "quick walrus"), ]), )) # Regex filter — matches "pufferfish", "swordfish", "clownfish" print(ns.query( limit=10, filters=("text", "Regex", "\\w+fish"), )) ``` ```typescript // Vector search with a filter let result = await ns.query({ rank_by: ["vector", "ANN", embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], }); console.log(result.rows); // Full-text search with boosted category field result = await ns.query({ limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], }); console.log(result.rows); // Regex filter — matches "pufferfish", "swordfish", "clownfish" result = await ns.query({ limit: 10, filters: ["text", "Regex", "\\w+fish"], }); console.log(result.rows); ``` ```go res, err := ns.Query(ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAnn("vector", embed(ctx, "arctic sea mammal")), Limit: turbopuffer.LimitParam{Total: 10}, Filters: turbopuffer.NewFilterEq("public", true), }) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // Full-text search with boosted category field res, err = ns.Query(ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{Total: 10}, Filters: turbopuffer.NewFilterEq("public", true), RankBy: turbopuffer.NewRankByTextSum([]turbopuffer.RankByText{ turbopuffer.NewRankByTextProduct(2, turbopuffer.NewRankByTextBM25("category", "mammal")), turbopuffer.NewRankByTextBM25("text", "quick walrus"), }), }) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // Regex filter — matches "pufferfish", "swordfish", "clownfish" res, err = ns.Query(ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{Total: 10}, Filters: turbopuffer.NewFilterRegex("text", `\w+fish`), }) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) ``` ```java // Vector search with a filter var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.ann("vector", embed("arctic sea mammal"))) .limit(10) .filters(Filter.eq("public", true)) .build() ); System.out.println(queryResult); // Full-text search with boosted category field var ftsResult = ns.query( NamespaceQueryParams.builder() .limit(10) .filters(Filter.eq("public", true)) .rankBy( RankByText.sum( RankByText.product(2, RankByText.bm25("category", "mammal")), RankByText.bm25("text", "quick walrus") ) ) .build() ); System.out.println(ftsResult); // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = ns.query( NamespaceQueryParams.builder().limit(10).filters(Filter.regex("text", "\\w+fish")).build() ); System.out.println(regexResult); ``` ```cs // Vector search with a filter var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", Embed("arctic sea mammal")), Limit = 10, Filters = Filter.Eq("public", true), } ); foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } // Full-text search with boosted category field var ftsResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Eq("public", true), RankBy = RankByText.Sum( RankByText.Product(2, RankByText.BM25("category", "mammal")), RankByText.BM25("text", "quick walrus") ), } ); foreach (var row in ftsResult.GetRows()) { Console.WriteLine(row); } // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Regex("text", "\\w+fish") } ); foreach (var row in regexResult.GetRows()) { Console.WriteLine(row); } ``` ```ruby # Vector search with a filter result = ns.query( rank_by: ["vector", "ANN", embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], ) puts result.rows # Full-text search with boosted category field result = ns.query( limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], ) puts result.rows # Regex filter — matches "pufferfish", "swordfish", "clownfish" result = ns.query( limit: 10, filters: ["text", "Regex", "\\w+fish"], ) puts result.rows ``` ## Aggregate [Count](/docs/query#aggregations) documents without returning rows, and use [grouped aggregations](/docs/query#group-by) to split the counts by attribute. Stay in the same namespace and count rows per `category`. ```bash curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "aggregate_by": {"count_by_category": ["Count"]}, "group_by": ["category"] }' # { # "aggregation_groups": [ # {"category": ["fish"], "count_by_category": 1}, # {"category": ["mammal"], "count_by_category": 1} # ] # } ``` ```python grouped = ns.query( aggregate_by={"count_by_category": ("Count",)}, group_by=["category"], ) print(grouped.aggregation_groups) # [Row(category=['fish'], count_by_category=1), Row(category=['mammal'], count_by_category=1)] ``` ```typescript const grouped = await ns.query({ aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], }); console.log(grouped.aggregation_groups); // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] ``` ```go groupedResult, err := ns.Query(ctx, turbopuffer.NamespaceQueryParams{ AggregateBy: map[string]turbopuffer.AggregateBy{ "count_by_category": turbopuffer.NewAggregateByCount(), }, GroupBy: []turbopuffer.GroupBy{turbopuffer.NewGroupByAttr("category")}, }) if err != nil { panic(err) } fmt.Println(turbopuffer.PrettyPrint(groupedResult.AggregationGroups)) // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] ``` ```java var groupedResult = ns.query( NamespaceQueryParams.builder() .aggregateBy(Map.of("count_by_category", AggregateBy.count("id"))) .groupBy(List.of(GroupBy.attr("category"))) .build() ); System.out.println(groupedResult.aggregationGroups().get()); // [{category=[fish], count_by_category=1}, {category=[mammal], count_by_category=1}] ``` ```cs var groupedResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["count_by_category"] = AggregateBy.Count("id"), }, GroupBy = [GroupBy.Attr("category")], } ); foreach (var group in groupedResult.GetAggregationGroups()) { Console.WriteLine(group); } // { "category": ["fish"], "count_by_category": 1 } // { "category": ["mammal"], "count_by_category": 1 } ``` ```ruby grouped = ns.query( aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], ) puts grouped.aggregation_groups # {category: ["fish"], count_by_category: 1} # {category: ["mammal"], count_by_category: 1} ``` ## Full runnable example Prefer one copy-paste program for the core loop? This version covers connect, write, search, and aggregate in one file. Then continue below with the smaller conditional-write and branching snippets. ```bash api_key="${TURBOPUFFER_API_KEY:-your-api-key}" # created here: https://turbopuffer.com/dashboard TPUF_URL="https://gcp-us-central1.turbopuffer.com" # choose best region: https://turbopuffer.com/docs/regions TPUF_NAMESPACE="${TPUF_NAMESPACE:-quickstart-$(date +%s)-$$}" # Upsert documents with vectors and attributes curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ { "id": 1, "vector": [0.1, 0.2], "name": "foo", "public": true, "text": "walrus narwhal" }, { "id": 2, "vector": [0.3, 0.4], "name": "foo", "public": false, "text": "elephant walrus rhino" } ], "schema": { "text": { "type": "string", "full_text_search": true, "regex": true }, "category": { "type": "[]string", "full_text_search": true } }, "distance_metric": "cosine_distance" }' # Query nearest neighbors with a filter curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "rank_by": ["vector", "ANN", [0.1, 0.2]], "limit": 10, "filters": ["public", "Eq", true] }' # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "limit": 10, "filters": ["public", "Eq", true], "rank_by": ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"] ]] }' # Regex filter — matches "pufferfish", "swordfish", "clownfish" curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "limit": 10, "filters": ["text", "Regex", "\\w+fish"] }' # Count documents grouped by category curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "aggregate_by": { "count_by_category": ["Count"] }, "group_by": ["category"] }' # { # "aggregation_groups": [ # {"category": "fish", "count_by_category": 1}, # {"category": "mammal", "count_by_category": 1} # ] # } ``` ```python # $ pip install turbopuffer # Sample Python notebook: # https://colab.research.google.com/drive/17i4sfFTeJQkINCxjBaOGOZeENZr4ZaTE import os import uuid import random from typing import List import turbopuffer tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) # Use provider-free random vectors for the quickstart. # Switch the embedding provider dropdown to see real embedding API calls. def embed(_text: str) -> List[float]: return [random.random(), random.random()] # Upsert documents with vectors and attributes ns.write( upsert_rows=[ { 'id': 1, 'vector': embed("walrus narwhal"), 'category': ["mammal"], 'public': True, 'text': "walrus narwhal", }, { 'id': 2, 'vector': embed("pufferfish clownfish swordfish"), 'category': ["fish"], 'public': False, 'text': "pufferfish clownfish swordfish", }, ], distance_metric='cosine_distance', schema={ "text": { # Configure FTS/BM25. Other attributes get inferred types. "type": "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema "full_text_search": True, "regex": True, }, "category": { "type": "[]string", "full_text_search": True, }, } ) # Query nearest neighbors with a filter print(ns.query( rank_by=("vector", "ANN", embed("arctic sea mammal")), limit=10, filters=("public", "Eq", True), )) # [Row(id=1, vector=None, $dist=0.42773545)] # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search print(ns.query( limit=10, filters=("public", "Eq", True), rank_by=("Sum", [ ("Product", 2, ("category", "BM25", "mammal")), ("text", "BM25", "quick walrus"), ]), )) # [Row(id=1, vector=None, $dist=0.7549128)] # Regex filter — matches "pufferfish", "swordfish", "clownfish" print(ns.query( limit=10, filters=("text", "Regex", "\\w+fish"), )) # Count documents grouped by category grouped_result = ns.query( aggregate_by={"count_by_category": ("Count",)}, group_by=["category"], ) print(grouped_result.aggregation_groups) # [Row(category=['fish'], count_by_category=1), Row(category=['mammal'], count_by_category=1)] ``` ```python # $ pip install turbopuffer sentence-transformers # Sample Python notebook: # https://colab.research.google.com/drive/17i4sfFTeJQkINCxjBaOGOZeENZr4ZaTE import os import uuid from typing import List import turbopuffer from sentence_transformers import SentenceTransformer tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) # Local embeddings with BGE — no API key needed. # Model is downloaded on first run (~130 MB). bge = SentenceTransformer("BAAI/bge-small-en-v1.5") def embed(text: str) -> List[float]: return bge.encode(text).tolist() # Upsert documents with vectors and attributes ns.write( upsert_rows=[ { 'id': 1, 'vector': embed("walrus narwhal"), 'category': ["mammal"], 'public': True, 'text': "walrus narwhal", }, { 'id': 2, 'vector': embed("pufferfish clownfish swordfish"), 'category': ["fish"], 'public': False, 'text': "pufferfish clownfish swordfish", }, ], distance_metric='cosine_distance', schema={ "text": { # Configure FTS/BM25. Other attributes get inferred types (`public`: int). "type": "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema "full_text_search": True, "regex": True, }, "category": { "type": "[]string", "full_text_search": True, }, } ) # Query nearest neighbors with a filter print(ns.query( rank_by=("vector", "ANN", embed("arctic sea mammal")), limit=10, filters=("public", "Eq", True), )) # [Row(id=1, vector=None, $dist=0.42773545)] # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search print(ns.query( limit=10, filters=("public", "Eq", True), rank_by=("Sum", [ ("Product", 2, ("category", "BM25", "mammal")), ("text", "BM25", "quick walrus"), ]), )) # [Row(id=1, vector=None, $dist=0.7549128)] # Regex filter — matches "pufferfish", "swordfish", "clownfish" print(ns.query( limit=10, filters=("text", "Regex", "\\w+fish"), )) # Count documents grouped by category grouped_result = ns.query( aggregate_by={"count_by_category": ("Count",)}, group_by=["category"], ) print(grouped_result.aggregation_groups) # [Row(category=['fish'], count_by_category=1), Row(category=['mammal'], count_by_category=1)] ``` ```python # $ pip install turbopuffer cohere # Sample Python notebook: # https://colab.research.google.com/drive/17i4sfFTeJQkINCxjBaOGOZeENZr4ZaTE import os import uuid from typing import List import turbopuffer from cohere import ClientV2 tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) cohere = ClientV2(api_key=os.environ["COHERE_API_KEY"]) # Create an embedding with Cohere. # Requires COHERE_API_KEY to be set (https://dashboard.cohere.com/api-keys) def embed(text: str) -> List[float]: return cohere.embed( model="embed-v4.0", input_type="search_document", texts=[text], embedding_types=["float"], ).embeddings.float[0] # Upsert documents with vectors and attributes ns.write( upsert_rows=[ { 'id': 1, 'vector': embed("walrus narwhal"), 'category': ["mammal"], 'public': True, 'text': "walrus narwhal", }, { 'id': 2, 'vector': embed("pufferfish clownfish swordfish"), 'category': ["fish"], 'public': False, 'text': "pufferfish clownfish swordfish", }, ], distance_metric='cosine_distance', schema={ "text": { # Configure FTS/BM25. Other attributes get inferred types (`public`: int). "type": "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema "full_text_search": True, "regex": True, }, "category": { "type": "[]string", "full_text_search": True, }, } ) # Query nearest neighbors with a filter print(ns.query( rank_by=("vector", "ANN", embed("arctic sea mammal")), limit=10, filters=("public", "Eq", True), )) # [Row(id=1, vector=None, $dist=0.42773545)] # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search print(ns.query( limit=10, filters=("public", "Eq", True), rank_by=("Sum", [ ("Product", 2, ("category", "BM25", "mammal")), ("text", "BM25", "quick walrus"), ]), )) # [Row(id=1, vector=None, $dist=0.7549128)] # Regex filter — matches "pufferfish", "swordfish", "clownfish" print(ns.query( limit=10, filters=("text", "Regex", "\\w+fish"), )) # Count documents grouped by category grouped_result = ns.query( aggregate_by={"count_by_category": ("Count",)}, group_by=["category"], ) print(grouped_result.aggregation_groups) # [Row(category=['fish'], count_by_category=1), Row(category=['mammal'], count_by_category=1)] ``` ```python # $ pip install turbopuffer google-genai # Sample Python notebook: # https://colab.research.google.com/drive/17i4sfFTeJQkINCxjBaOGOZeENZr4ZaTE import os import uuid from typing import List import turbopuffer from google.genai import Client from google.genai.types import EmbedContentConfig tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) gemini = Client(api_key=os.environ["GEMINI_API_KEY"]) # Create an embedding with Gemini. # Requires GEMINI_API_KEY to be set (https://aistudio.google.com/app/apikey) def embed(text: str) -> List[float]: return gemini.models.embed_content( model="gemini-embedding-001", contents=text, config=EmbedContentConfig(task_type="RETRIEVAL_DOCUMENT"), ).embeddings[0].values # Upsert documents with vectors and attributes ns.write( upsert_rows=[ { 'id': 1, 'vector': embed("walrus narwhal"), 'category': ["mammal"], 'public': True, 'text': "walrus narwhal", }, { 'id': 2, 'vector': embed("pufferfish clownfish swordfish"), 'category': ["fish"], 'public': False, 'text': "pufferfish clownfish swordfish", }, ], distance_metric='cosine_distance', schema={ "text": { # Configure FTS/BM25. Other attributes get inferred types (`public`: int). "type": "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema "full_text_search": True, "regex": True, }, "category": { "type": "[]string", "full_text_search": True, }, } ) # Query nearest neighbors with a filter print(ns.query( rank_by=("vector", "ANN", embed("arctic sea mammal")), limit=10, filters=("public", "Eq", True), )) # [Row(id=1, vector=None, $dist=0.42773545)] # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search print(ns.query( limit=10, filters=("public", "Eq", True), rank_by=("Sum", [ ("Product", 2, ("category", "BM25", "mammal")), ("text", "BM25", "quick walrus"), ]), )) # [Row(id=1, vector=None, $dist=0.7549128)] # Regex filter — matches "pufferfish", "swordfish", "clownfish" print(ns.query( limit=10, filters=("text", "Regex", "\\w+fish"), )) # Count documents grouped by category grouped_result = ns.query( aggregate_by={"count_by_category": ("Count",)}, group_by=["category"], ) print(grouped_result.aggregation_groups) # [Row(category=['fish'], count_by_category=1), Row(category=['mammal'], count_by_category=1)] ``` ```python # $ pip install turbopuffer openai # Sample Python notebook: # https://colab.research.google.com/drive/17i4sfFTeJQkINCxjBaOGOZeENZr4ZaTE import os import uuid from typing import List import turbopuffer from openai import OpenAI tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) fireworks = OpenAI( api_key=os.environ["FIREWORKS_API_KEY"], base_url="https://api.fireworks.ai/inference/v1", ) # Create an embedding with Qwen on Fireworks. # Requires FIREWORKS_API_KEY to be set (https://fireworks.ai/settings/users/api-keys) def embed(text: str) -> List[float]: return fireworks.embeddings.create( model="fireworks/qwen3-embedding-8b", input=text, ).data[0].embedding # Upsert documents with vectors and attributes ns.write( upsert_rows=[ { 'id': 1, 'vector': embed("walrus narwhal"), 'category': ["mammal"], 'public': True, 'text': "walrus narwhal", }, { 'id': 2, 'vector': embed("pufferfish clownfish swordfish"), 'category': ["fish"], 'public': False, 'text': "pufferfish clownfish swordfish", }, ], distance_metric='cosine_distance', schema={ "text": { # Configure FTS/BM25. Other attributes get inferred types (`public`: int). "type": "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema "full_text_search": True, "regex": True, }, "category": { "type": "[]string", "full_text_search": True, }, } ) # Query nearest neighbors with a filter print(ns.query( rank_by=("vector", "ANN", embed("arctic sea mammal")), limit=10, filters=("public", "Eq", True), )) # [Row(id=1, vector=None, $dist=0.42773545)] # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search print(ns.query( limit=10, filters=("public", "Eq", True), rank_by=("Sum", [ ("Product", 2, ("category", "BM25", "mammal")), ("text", "BM25", "quick walrus"), ]), )) # [Row(id=1, vector=None, $dist=0.7549128)] # Regex filter — matches "pufferfish", "swordfish", "clownfish" print(ns.query( limit=10, filters=("text", "Regex", "\\w+fish"), )) # Count documents grouped by category grouped_result = ns.query( aggregate_by={"count_by_category": ("Count",)}, group_by=["category"], ) print(grouped_result.aggregation_groups) # [Row(category=['fish'], count_by_category=1), Row(category=['mammal'], count_by_category=1)] ``` ```python # $ pip install turbopuffer voyageai # Sample Python notebook: # https://colab.research.google.com/drive/17i4sfFTeJQkINCxjBaOGOZeENZr4ZaTE import os import uuid from typing import List import turbopuffer import voyageai tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"quickstart-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) voyage = voyageai.Client(api_key=os.environ["VOYAGE_API_KEY"]) # Create an embedding with Voyage. # Requires VOYAGE_API_KEY to be set: # https://dashboard.voyageai.com/organization/api-keys def embed(text: str) -> List[float]: return voyage.embed([text], model="voyage-4-lite").embeddings[0] # Upsert documents with vectors and attributes ns.write( upsert_rows=[ { 'id': 1, 'vector': embed("walrus narwhal"), 'category': ["mammal"], 'public': True, 'text': "walrus narwhal", }, { 'id': 2, 'vector': embed("pufferfish clownfish swordfish"), 'category': ["fish"], 'public': False, 'text': "pufferfish clownfish swordfish", }, ], distance_metric='cosine_distance', schema={ "text": { # Configure FTS/BM25. Other attributes get inferred types. "type": "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema "full_text_search": True, "regex": True, }, "category": { "type": "[]string", "full_text_search": True, }, } ) # Query nearest neighbors with a filter print(ns.query( rank_by=("vector", "ANN", embed("arctic sea mammal")), limit=10, filters=("public", "Eq", True), )) # [Row(id=1, vector=None, $dist=0.42773545)] # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search print(ns.query( limit=10, filters=("public", "Eq", True), rank_by=("Sum", [ ("Product", 2, ("category", "BM25", "mammal")), ("text", "BM25", "quick walrus"), ]), )) # [Row(id=1, vector=None, $dist=0.7549128)] # Regex filter — matches "pufferfish", "swordfish", "clownfish" print(ns.query( limit=10, filters=("text", "Regex", "\\w+fish"), )) # Count documents grouped by category grouped_result = ns.query( aggregate_by={"count_by_category": ("Count",)}, group_by=["category"], ) print(grouped_result.aggregation_groups) # [Row(category=['fish'], count_by_category=1), Row(category=['mammal'], count_by_category=1)] ``` ```typescript // $ npm install @turbopuffer/turbopuffer import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); // Use provider-free random vectors for the quickstart. // Switch the embedding provider dropdown to see real embedding API calls. function embed(_text: string): number[] { return [Math.random(), Math.random()]; } // Upsert documents with vectors and attributes await ns.write({ upsert_rows: [ { id: 1, vector: embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { // Configure FTS/BM25. Other attributes have inferred types. type: "string", // More schema & FTS options: // https://turbopuffer.com/docs/write#schema full_text_search: true, regex: true, }, category: { type: "[]string", full_text_search: true }, }, }); // Query nearest neighbors with a filter let result = await ns.query({ rank_by: ["vector", "ANN", embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], }); console.log(result.rows); // [{ '$dist': 0.42773545, id: 1 }] // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search result = await ns.query({ limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], }); console.log(result.rows); // [{ '$dist': 0.7549128, id: 1 }] // Regex filter — matches "pufferfish", "swordfish", "clownfish" result = await ns.query({ limit: 10, filters: ["text", "Regex", "\\w+fish"], }); console.log(result.rows); // Count documents grouped by category const groupedResult = await ns.query({ aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], }); console.log(groupedResult.aggregation_groups); // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] ``` ```typescript // $ npm install @turbopuffer/turbopuffer cohere-ai import { Turbopuffer } from "@turbopuffer/turbopuffer"; import { CohereClient } from "cohere-ai"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); const cohere = new CohereClient({ token: process.env.COHERE_API_KEY }); // Create an embedding with Cohere. // Requires COHERE_API_KEY to be set (https://dashboard.cohere.com/api-keys) async function embed(text: string): Promise { return (await cohere.v2.embed({ model: "embed-v4.0", inputType: "search_document", texts: [text], embeddingTypes: ["float"], })).embeddings.float![0]; } // Upsert documents with vectors and attributes await ns.write({ upsert_rows: [ { id: 1, vector: await embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: await embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { // Configure FTS/BM25. Other attributes have inferred types (`public`: int). type: "string", // More schema & FTS options: // https://turbopuffer.com/docs/write#schema full_text_search: true, regex: true, }, category: { type: "[]string", full_text_search: true }, }, }); // Query nearest neighbors with a filter let result = await ns.query({ rank_by: ["vector", "ANN", await embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], }); console.log(result.rows); // [{ '$dist': 0.42773545, id: 1 }] // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search result = await ns.query({ limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], }); console.log(result.rows); // [{ '$dist': 0.7549128, id: 1 }] // Regex filter — matches "pufferfish", "swordfish", "clownfish" result = await ns.query({ limit: 10, filters: ["text", "Regex", "\\w+fish"], }); console.log(result.rows); // Count documents grouped by category const groupedResult = await ns.query({ aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], }); console.log(groupedResult.aggregation_groups); // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] ``` ```typescript // $ npm install @turbopuffer/turbopuffer @google/genai import { Turbopuffer } from "@turbopuffer/turbopuffer"; import { GoogleGenAI } from "@google/genai"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); const gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); // Create an embedding with Gemini. // Requires GEMINI_API_KEY to be set (https://aistudio.google.com/app/apikey) async function embed(text: string): Promise { return ( await gemini.models.embedContent({ model: "gemini-embedding-001", contents: text, config: { taskType: "RETRIEVAL_DOCUMENT" }, }) ).embeddings![0].values!; } // Upsert documents with vectors and attributes await ns.write({ upsert_rows: [ { id: 1, vector: await embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: await embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { // Configure FTS/BM25. Other attributes have inferred types (`public`: int). type: "string", // More schema & FTS options: // https://turbopuffer.com/docs/write#schema full_text_search: true, regex: true, }, category: { type: "[]string", full_text_search: true }, }, }); // Query nearest neighbors with a filter let result = await ns.query({ rank_by: ["vector", "ANN", await embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], }); console.log(result.rows); // [{ '$dist': 0.42773545, id: 1 }] // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search result = await ns.query({ limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], }); console.log(result.rows); // [{ '$dist': 0.7549128, id: 1 }] // Regex filter — matches "pufferfish", "swordfish", "clownfish" result = await ns.query({ limit: 10, filters: ["text", "Regex", "\\w+fish"], }); console.log(result.rows); // Count documents grouped by category const groupedResult = await ns.query({ aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], }); console.log(groupedResult.aggregation_groups); // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] ``` ```typescript // $ npm install @turbopuffer/turbopuffer openai import { Turbopuffer } from "@turbopuffer/turbopuffer"; import OpenAI from "openai"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); const fireworks = new OpenAI({ apiKey: process.env.FIREWORKS_API_KEY, baseURL: "https://api.fireworks.ai/inference/v1", }); // Create an embedding with Qwen on Fireworks. // Requires FIREWORKS_API_KEY to be set (https://fireworks.ai/settings/users/api-keys) async function embed(text: string): Promise { return ( await fireworks.embeddings.create({ model: "fireworks/qwen3-embedding-8b", input: text, encoding_format: "float", }) ).data[0].embedding; } // Upsert documents with vectors and attributes await ns.write({ upsert_rows: [ { id: 1, vector: await embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: await embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { // Configure FTS/BM25. Other attributes have inferred types (`public`: int). type: "string", // More schema & FTS options: // https://turbopuffer.com/docs/write#schema full_text_search: true, regex: true, }, category: { type: "[]string", full_text_search: true }, }, }); // Query nearest neighbors with a filter let result = await ns.query({ rank_by: ["vector", "ANN", await embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], }); console.log(result.rows); // [{ '$dist': 0.42773545, id: 1 }] // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search result = await ns.query({ limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], }); console.log(result.rows); // [{ '$dist': 0.7549128, id: 1 }] // Regex filter — matches "pufferfish", "swordfish", "clownfish" result = await ns.query({ limit: 10, filters: ["text", "Regex", "\\w+fish"], }); console.log(result.rows); // Count documents grouped by category const groupedResult = await ns.query({ aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], }); console.log(groupedResult.aggregation_groups); // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] ``` ```typescript // $ npm install @turbopuffer/turbopuffer voyageai import { Turbopuffer } from "@turbopuffer/turbopuffer"; import { VoyageAIClient } from "voyageai"; const tpuf = new Turbopuffer({ apiKey: process.env.TURBOPUFFER_API_KEY, // created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const namespace = process.env.TURBOPUFFER_NAMESPACE ?? `quickstart-${Math.random().toString(36).slice(2, 10)}`; const ns = tpuf.namespace(namespace); const voyage = new VoyageAIClient({ apiKey: process.env.VOYAGE_API_KEY }); // Create an embedding with Voyage. // Requires VOYAGE_API_KEY to be set: // https://dashboard.voyageai.com/organization/api-keys async function embed(text: string): Promise { return (await voyage.embed({ input: text, model: "voyage-4-lite", })).data[0].embedding; } // Upsert documents with vectors and attributes await ns.write({ upsert_rows: [ { id: 1, vector: await embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: await embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { // Configure FTS/BM25. Other attributes have inferred // types (`category`: str, `public`: int). type: "string", // More schema & FTS options: // https://turbopuffer.com/docs/write#schema full_text_search: true, regex: true, }, category: { type: "[]string", full_text_search: true }, }, }); // Query nearest neighbors with a filter let result = await ns.query({ rank_by: ["vector", "ANN", await embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], }); console.log(result.rows); // [{ '$dist': 0.42773545, id: 1 }] // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search result = await ns.query({ limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], }); console.log(result.rows); // [{ '$dist': 0.7549128, id: 1 }] // Regex filter — matches "pufferfish", "swordfish", "clownfish" result = await ns.query({ limit: 10, filters: ["text", "Regex", "\\w+fish"], }); console.log(result.rows); // Count documents grouped by category const groupedResult = await ns.query({ aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], }); console.log(groupedResult.aggregation_groups); // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] ``` ```go package main import ( "context" "fmt" "math/rand" "os" "github.com/openai/openai-go" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) // Create an embedding with OpenAI, could be {Cohere, Voyage, Mixed Bread, ...} // Requires OPENAI_API_KEY to be set (https://platform.openai.com/settings/organization/api-keys) func openaiOrRandVector(ctx context.Context, text string) []float32 { if os.Getenv("OPENAI_API_KEY") == "" { fmt.Println("OPENAI_API_KEY not set, using random vectors") return []float32{rand.Float32(), rand.Float32()} } client := openai.NewClient() resp, err := client.Embeddings.New(ctx, openai.EmbeddingNewParams{ Input: openai.EmbeddingNewParamsInputUnion{OfString: openai.String(text)}, Model: openai.EmbeddingModelTextEmbedding3Small, }) if err != nil { fmt.Printf("OpenAI error, using random vectors: %v\n", err) return []float32{rand.Float32(), rand.Float32()} } embedding := make([]float32, len(resp.Data[0].Embedding)) for i, v := range resp.Data[0].Embedding { embedding[i] = float32(v) } return embedding } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( // API tokens are created in the dashboard: https://turbopuffer.com/dashboard option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // Pick the right region: https://turbopuffer.com/docs/regions option.WithRegion("gcp-us-central1"), ) ns := tpuf.Namespace("quickstart-example-go") // Upsert documents with vectors and attributes _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": openaiOrRandVector(ctx, "walrus narwhal"), "category": "mammal", "public": 1, "text": "walrus narwhal", }, { "id": 2, "vector": openaiOrRandVector(ctx, "pufferfish clownfish swordfish"), "category": "fish", "public": 0, "text": "pufferfish clownfish swordfish", }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ // Configure FTS/BM25, other attributes have inferred types (category: str, public: int) "text": { Type: "string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, }, }, }, ) if err != nil { panic(err) } // Query nearest neighbors with filter res, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAnn("vector", openaiOrRandVector(ctx, "arctic sea mammal")), Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterAnd([]turbopuffer.Filter{ turbopuffer.NewFilterEq("category", "mammal"), turbopuffer.NewFilterEq("public", 1), }), IncludeAttributes: turbopuffer.IncludeAttributesParam{StringArray: []string{"category"}}, }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.42773545, "category": "mammal"}] // Full-text search on an attribute // To combine FTS and vector search concurrently and fuse results, see https://turbopuffer.com/docs/hybrid-search res, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterEq("category", "mammal"), RankBy: turbopuffer.NewRankByTextBM25("text", "quick walrus"), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.7549128}] // Vectors can be updated by passing new data for an existing ID ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": openaiOrRandVector(ctx, "foo"), "name": "foo", "public": 1, }, { "id": 2, "vector": openaiOrRandVector(ctx, "foo"), "name": "foo", "public": 1, }, { "id": 3, "vector": openaiOrRandVector(ctx, "foo"), "name": "foo", "public": 1, }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, }, ) // Vectors are deleted by ID. _, err = ns.Write( ctx, turbopuffer.NamespaceWriteParams{ Deletes: []any{1, 3}, }, ) if err != nil { panic(err) } } ``` ```go // $ go get github.com/turbopuffer/turbopuffer-go github.com/cohere-ai/cohere-go/v2 package main import ( "context" "fmt" "os" "time" cohere "github.com/cohere-ai/cohere-go/v2" cohereclient "github.com/cohere-ai/cohere-go/v2/client" "github.com/turbopuffer/turbopuffer-go" "github.com/turbopuffer/turbopuffer-go/option" "github.com/turbopuffer/turbopuffer-go/packages/param" ) // Create an embedding with Cohere. // Requires COHERE_API_KEY to be set (https://dashboard.cohere.com/api-keys) func embed(ctx context.Context, text string) []float32 { resp, err := cohereclient.NewClient(cohereclient.WithToken(os.Getenv("COHERE_API_KEY"))).V2.Embed( ctx, &cohere.V2EmbedRequest{ Model: "embed-v4.0", InputType: cohere.EmbedInputTypeSearchDocument, Texts: []string{text}, EmbeddingTypes: []cohere.EmbeddingType{cohere.EmbeddingTypeFloat}, }, ) if err != nil { panic(err) } return toFloat32Slice(resp.GetEmbeddings().GetFloat()[0]) } func toFloat32Slice(values []float64) []float32 { out := make([]float32, len(values)) for i, value := range values { out[i] = float32(value) } return out } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // created here: https://turbopuffer.com/dashboard option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespace := os.Getenv("TURBOPUFFER_NAMESPACE") if namespace == "" { namespace = fmt.Sprintf("quickstart-%d", time.Now().UnixNano()) } ns := tpuf.Namespace(namespace) // Upsert documents with vectors and attributes _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": embed(ctx, "walrus narwhal"), "category": []string{"mammal"}, "public": true, "text": "walrus narwhal", }, { "id": 2, "vector": embed(ctx, "pufferfish clownfish swordfish"), "category": []string{"fish"}, "public": false, "text": "pufferfish clownfish swordfish", }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "text": { Type: "string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, Regex: param.NewOpt(true), }, "category": { Type: "[]string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, }, }, }, ) if err != nil { panic(err) } // Query nearest neighbors with a filter res, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByVector("vector", embed(ctx, "arctic sea mammal")), Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterEq("public", true), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.42773545}] // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search res, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterEq("public", true), RankBy: turbopuffer.NewRankBySum([]turbopuffer.RankBy{ turbopuffer.NewRankByProduct(2, turbopuffer.NewRankByTextBM25("category", "mammal")), turbopuffer.NewRankByTextBM25("text", "quick walrus"), }), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.7549128}] // Regex filter — matches "pufferfish", "swordfish", "clownfish" res, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{Total: 10}, Filters: turbopuffer.NewFilterRegex("text", `\w+fish`), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // Count documents grouped by category groupedResult, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ AggregateBy: map[string]turbopuffer.AggregateBy{ "count_by_category": turbopuffer.NewAggregateByCount(), }, GroupBy: []string{"category"}, }, ) if err != nil { panic(err) } fmt.Println(turbopuffer.PrettyPrint(groupedResult.AggregationGroups)) // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] } ``` ```go // $ go get github.com/turbopuffer/turbopuffer-go google.golang.org/genai package main import ( "context" "fmt" "os" "time" "github.com/turbopuffer/turbopuffer-go" "github.com/turbopuffer/turbopuffer-go/option" "github.com/turbopuffer/turbopuffer-go/packages/param" "google.golang.org/genai" ) // Create an embedding with Gemini. // Requires GEMINI_API_KEY to be set (https://aistudio.google.com/app/apikey) func embed(ctx context.Context, text string) []float32 { client, err := genai.NewClient(ctx, &genai.ClientConfig{ APIKey: os.Getenv("GEMINI_API_KEY"), Backend: genai.BackendGeminiAPI, }) if err != nil { panic(err) } defer client.Close() resp, err := client.Models.EmbedContent( ctx, "gemini-embedding-001", []*genai.Content{genai.NewContentFromText(text, genai.RoleUser)}, &genai.EmbedContentRequest{TaskType: genai.TaskTypeRetrievalDocument}, ) if err != nil { panic(err) } return resp.Embeddings[0].Values } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // created here: https://turbopuffer.com/dashboard option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespace := os.Getenv("TURBOPUFFER_NAMESPACE") if namespace == "" { namespace = fmt.Sprintf("quickstart-%d", time.Now().UnixNano()) } ns := tpuf.Namespace(namespace) // Upsert documents with vectors and attributes _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": embed(ctx, "walrus narwhal"), "category": []string{"mammal"}, "public": true, "text": "walrus narwhal", }, { "id": 2, "vector": embed(ctx, "pufferfish clownfish swordfish"), "category": []string{"fish"}, "public": false, "text": "pufferfish clownfish swordfish", }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "text": { Type: "string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, Regex: param.NewOpt(true), }, "category": { Type: "[]string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, }, }, }, ) if err != nil { panic(err) } // Query nearest neighbors with a filter res, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByVector("vector", embed(ctx, "arctic sea mammal")), Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterEq("public", true), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.42773545}] // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search res, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterEq("public", true), RankBy: turbopuffer.NewRankBySum([]turbopuffer.RankBy{ turbopuffer.NewRankByProduct(2, turbopuffer.NewRankByTextBM25("category", "mammal")), turbopuffer.NewRankByTextBM25("text", "quick walrus"), }), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.7549128}] // Regex filter — matches "pufferfish", "swordfish", "clownfish" res, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{Total: 10}, Filters: turbopuffer.NewFilterRegex("text", `\w+fish`), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // Count documents grouped by category groupedResult, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ AggregateBy: map[string]turbopuffer.AggregateBy{ "count_by_category": turbopuffer.NewAggregateByCount(), }, GroupBy: []string{"category"}, }, ) if err != nil { panic(err) } fmt.Println(turbopuffer.PrettyPrint(groupedResult.AggregationGroups)) // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] } ``` ```go // $ go get github.com/turbopuffer/turbopuffer-go github.com/openai/openai-go package main import ( "context" "fmt" "os" "time" "github.com/openai/openai-go" openaioption "github.com/openai/openai-go/option" "github.com/turbopuffer/turbopuffer-go" tpufoption "github.com/turbopuffer/turbopuffer-go/option" "github.com/turbopuffer/turbopuffer-go/packages/param" ) // Create an embedding with Qwen on Fireworks. // Requires FIREWORKS_API_KEY to be set (https://fireworks.ai/settings/users/api-keys) func embed(ctx context.Context, text string) []float32 { resp, err := openai.NewClient( openaioption.WithAPIKey(os.Getenv("FIREWORKS_API_KEY")), openaioption.WithBaseURL("https://api.fireworks.ai/inference/v1"), ).Embeddings.New(ctx, openai.EmbeddingNewParams{ Input: openai.EmbeddingNewParamsInputUnion{OfString: openai.String(text)}, Model: openai.EmbeddingModel("fireworks/qwen3-embedding-8b"), }) if err != nil { panic(err) } return toFloat32Slice(resp.Data[0].Embedding) } func toFloat32Slice(values []float64) []float32 { out := make([]float32, len(values)) for i, value := range values { out[i] = float32(value) } return out } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( tpufoption.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // created here: https://turbopuffer.com/dashboard tpufoption.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespace := os.Getenv("TURBOPUFFER_NAMESPACE") if namespace == "" { namespace = fmt.Sprintf("quickstart-%d", time.Now().UnixNano()) } ns := tpuf.Namespace(namespace) // Upsert documents with vectors and attributes _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": embed(ctx, "walrus narwhal"), "category": []string{"mammal"}, "public": true, "text": "walrus narwhal", }, { "id": 2, "vector": embed(ctx, "pufferfish clownfish swordfish"), "category": []string{"fish"}, "public": false, "text": "pufferfish clownfish swordfish", }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "text": { Type: "string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, Regex: param.NewOpt(true), }, "category": { Type: "[]string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, }, }, }, ) if err != nil { panic(err) } // Query nearest neighbors with a filter res, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByVector("vector", embed(ctx, "arctic sea mammal")), Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterEq("public", true), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.42773545}] // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search res, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterEq("public", true), RankBy: turbopuffer.NewRankBySum([]turbopuffer.RankBy{ turbopuffer.NewRankByProduct(2, turbopuffer.NewRankByTextBM25("category", "mammal")), turbopuffer.NewRankByTextBM25("text", "quick walrus"), }), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.7549128}] // Regex filter — matches "pufferfish", "swordfish", "clownfish" res, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{Total: 10}, Filters: turbopuffer.NewFilterRegex("text", `\w+fish`), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // Count documents grouped by category groupedResult, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ AggregateBy: map[string]turbopuffer.AggregateBy{ "count_by_category": turbopuffer.NewAggregateByCount(), }, GroupBy: []string{"category"}, }, ) if err != nil { panic(err) } fmt.Println(turbopuffer.PrettyPrint(groupedResult.AggregationGroups)) // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] } ``` ```go // $ go get github.com/turbopuffer/turbopuffer-go github.com/openai/openai-go package main import ( "context" "fmt" "os" "time" "github.com/openai/openai-go" openaioption "github.com/openai/openai-go/option" "github.com/turbopuffer/turbopuffer-go" tpufoption "github.com/turbopuffer/turbopuffer-go/option" "github.com/turbopuffer/turbopuffer-go/packages/param" ) // Create an embedding with Voyage. // Requires VOYAGE_API_KEY to be set: // https://dashboard.voyageai.com/organization/api-keys func embed(ctx context.Context, text string) []float32 { resp, err := openai.NewClient( openaioption.WithAPIKey(os.Getenv("VOYAGE_API_KEY")), openaioption.WithBaseURL("https://api.voyageai.com/v1"), ).Embeddings.New(ctx, openai.EmbeddingNewParams{ Input: openai.EmbeddingNewParamsInputUnion{OfString: openai.String(text)}, Model: openai.EmbeddingModel("voyage-4-lite"), }) if err != nil { panic(err) } return toFloat32Slice(resp.Data[0].Embedding) } func toFloat32Slice(values []float64) []float32 { out := make([]float32, len(values)) for i, value := range values { out[i] = float32(value) } return out } func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( tpufoption.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), // created here: https://turbopuffer.com/dashboard tpufoption.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) namespace := os.Getenv("TURBOPUFFER_NAMESPACE") if namespace == "" { namespace = fmt.Sprintf("quickstart-%d", time.Now().UnixNano()) } ns := tpuf.Namespace(namespace) // Upsert documents with vectors and attributes _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": embed(ctx, "walrus narwhal"), "category": []string{"mammal"}, "public": true, "text": "walrus narwhal", }, { "id": 2, "vector": embed(ctx, "pufferfish clownfish swordfish"), "category": []string{"fish"}, "public": false, "text": "pufferfish clownfish swordfish", }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "text": { Type: "string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, Regex: param.NewOpt(true), }, "category": { Type: "[]string", FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, }, }, }, ) if err != nil { panic(err) } // Query nearest neighbors with a filter res, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByVector("vector", embed(ctx, "arctic sea mammal")), Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterEq("public", true), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.42773545}] // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search res, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{ Total: 10, }, Filters: turbopuffer.NewFilterEq("public", true), RankBy: turbopuffer.NewRankBySum([]turbopuffer.RankBy{ turbopuffer.NewRankByProduct(2, turbopuffer.NewRankByTextBM25("category", "mammal")), turbopuffer.NewRankByTextBM25("text", "quick walrus"), }), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // [{"id": 1, "vector": null, "$dist": 0.7549128}] // Regex filter — matches "pufferfish", "swordfish", "clownfish" res, err = ns.Query( ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{Total: 10}, Filters: turbopuffer.NewFilterRegex("text", `\w+fish`), }, ) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(res.Rows)) // Count documents grouped by category groupedResult, err := ns.Query( ctx, turbopuffer.NamespaceQueryParams{ AggregateBy: map[string]turbopuffer.AggregateBy{ "count_by_category": turbopuffer.NewAggregateByCount(), }, GroupBy: []string{"category"}, }, ) if err != nil { panic(err) } fmt.Println(turbopuffer.PrettyPrint(groupedResult.AggregationGroups)) // [ // { category: ["fish"], count_by_category: 1 }, // { category: ["mammal"], count_by_category: 1 }, // ] } ``` ```java // dependencies { // implementation("com.turbopuffer:turbopuffer-java:+") // implementation("com.openai:openai-java:+") // } package com.turbopuffer.docs; import com.openai.client.okhttp.*; import com.openai.errors.*; import com.openai.models.embeddings.*; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class QuickStart { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() // API tokens are created in the dashboard: https://turbopuffer.com/dashboard .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // Pick the right region: https://turbopuffer.com/docs/regions .region("gcp-us-central1") .build(); var ns = tpuf.namespace("quickstart-example-java"); // Upsert documents with vectors and attributes ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("vector", openAiOrRandVector("walrus narwhal")) .put("category", "mammal") .put("public", 1) .put("text", "walrus narwhal") .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("vector", openAiOrRandVector("pufferfish clownfish swordfish")) .put("category", "fish") .put("public", 0) .put("text", "pufferfish clownfish swordfish") .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .schema( Schema.builder() .put( "text", AttributeSchemaConfig.builder() .type("string") // More schema & FTS options // https://turbopuffer.com/docs/write#schema .fullTextSearch(FullTextSearchConfig.defaults()) .build() ) .build() ) .build() ); // Query nearest neighbors with filter var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.ann("vector", openAiOrRandVector("arctic sea mammal"))) .limit(10) .filters(Filter.and(Filter.eq("category", "mammal"), Filter.eq("public", 1))) .includeAttributes("category") .build() ); System.out.println(queryResult); // NamespaceQueryResponse{rows=[{$dist=0.42773545, id=1, category=mammal}]} // Full-text search on an attribute // To combine FTS and vector search concurrently and fuse results, see https://turbopuffer.com/docs/hybrid-search var ftsResult = ns.query( NamespaceQueryParams.builder() .limit(10) .filters(Filter.eq("category", "mammal")) .rankBy(RankByText.bm25("text", "quick walrus")) .build() ); System.out.println(ftsResult); // NamespaceQueryResponse{rows=[{$dist=0.7549128, id=1}]} // Vectors can be updated by passing new data for an existing ID ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("vector", openAiOrRandVector("foo")) .put("name", "foo") .put("public", 1) .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("vector", openAiOrRandVector("foo")) .put("name", "foo") .put("public", 1) .build() ) .addUpsertRow( Row.builder() .put("id", 3) .put("vector", openAiOrRandVector("foo")) .put("name", "foo") .put("public", 1) .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .build() ); // Vectors are deleted by ID. ns.write(NamespaceWriteParams.builder().addDelete(1).addDelete(3).build()); } // Create an embedding with OpenAI, could be {Cohere, Voyage, Mixed Bread, ...} // Requires OPENAI_API_KEY to be set (https://platform.openai.com/settings/organization/api-keys) public static List openAiOrRandVector(String text) { if (System.getenv("OPENAI_API_KEY") == null) { System.out.println("OPENAI_API_KEY not set, using random vectors"); return randVector(); } var client = OpenAIOkHttpClient.fromEnv(); try { var params = EmbeddingCreateParams.builder() .input(text) .model(EmbeddingModel.TEXT_EMBEDDING_3_SMALL) .build(); var response = client.embeddings().create(params); return response.data().get(0).embedding(); } catch (OpenAIException e) { System.out.println("OpenAI error, using random vectors: " + e.getMessage()); return randVector(); } } public static List randVector() { Random rand = new Random(); List vector = new java.util.ArrayList<>(2); vector.add(rand.nextFloat()); vector.add(rand.nextFloat()); return vector; } } ``` ```java // Gradle: implementation("com.turbopuffer:turbopuffer-java:+"), implementation("com.cohere:cohere-java:+") package com.turbopuffer.docs; import com.cohere.api.Cohere; import com.cohere.api.resources.v2.requests.V2EmbedRequest; import com.cohere.api.types.EmbedInputType; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; class QuickStartCohere { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // created here: https://turbopuffer.com/dashboard .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespace = Optional.ofNullable(System.getenv("TURBOPUFFER_NAMESPACE")).orElse( "quickstart-" + UUID.randomUUID().toString().substring(0, 8) ); var ns = tpuf.namespace(namespace); // Upsert documents with vectors and attributes ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("vector", embed("walrus narwhal")) .put("category", List.of("mammal")) .put("public", true) .put("text", "walrus narwhal") .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("vector", embed("pufferfish clownfish swordfish")) .put("category", List.of("fish")) .put("public", false) .put("text", "pufferfish clownfish swordfish") .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .schema( Schema.builder() .put( "text", AttributeSchemaConfig.builder() .type("string") // More schema & FTS options: // https://turbopuffer.com/docs/write#schema .fullTextSearch(FullTextSearchConfig.defaults()) .regex(true) .build() ) .put( "category", AttributeSchemaConfig.builder() .type("[]string") .fullTextSearch(FullTextSearchConfig.defaults()) .build() ) .build() ) .build() ); // Query nearest neighbors with a filter var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.vector("vector", embed("arctic sea mammal"))) .limit(10) .filters(Filter.eq("public", true)) .build() ); System.out.println(queryResult); // NamespaceQueryResponse{rows=[{$dist=0.42773545, id=1}]} // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search var ftsResult = ns.query( NamespaceQueryParams.builder() .limit(10) .filters(Filter.eq("public", true)) .rankBy( RankByText.sum( RankByText.product(2, RankByText.bm25("category", "mammal")), RankByText.bm25("text", "quick walrus") ) ) .build() ); System.out.println(ftsResult); // NamespaceQueryResponse{rows=[{$dist=0.7549128, id=1}]} // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = ns.query( NamespaceQueryParams.builder().limit(10).filters(Filter.regex("text", "\\w+fish")).build() ); System.out.println(regexResult); // Count documents grouped by category var groupedResult = ns.query( NamespaceQueryParams.builder() .aggregateBy(Map.of("count_by_category", AggregateBy.count("id"))) .groupBy(List.of("category")) .build() ); System.out.println(groupedResult.aggregationGroups().get()); // [{category=[fish], count_by_category=1}, {category=[mammal], count_by_category=1}] } // Create an embedding with Cohere. // Requires COHERE_API_KEY to be set (https://dashboard.cohere.com/api-keys) public static List embed(String text) { var response = Cohere.builder() .token(System.getenv("COHERE_API_KEY")) .clientName("turbopuffer-quickstart") .build() .v2() .embed( V2EmbedRequest.builder() .model("embed-v4.0") .inputType(EmbedInputType.SEARCH_DOCUMENT) .texts(List.of(text)) .build() ); return response .getEmbeddings() .getFloat() .orElseThrow() .get(0) .stream() .map(Double::floatValue) .toList(); } } ``` ```java // Gradle: implementation("com.turbopuffer:turbopuffer-java:+"), implementation("com.google.genai:google-genai:+") package com.turbopuffer.docs; import com.google.genai.Client; import com.google.genai.types.EmbedContentConfig; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; class QuickStartGemini { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // created here: https://turbopuffer.com/dashboard .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespace = Optional.ofNullable(System.getenv("TURBOPUFFER_NAMESPACE")).orElse( "quickstart-" + UUID.randomUUID().toString().substring(0, 8) ); var ns = tpuf.namespace(namespace); // Upsert documents with vectors and attributes ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("vector", embed("walrus narwhal")) .put("category", List.of("mammal")) .put("public", true) .put("text", "walrus narwhal") .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("vector", embed("pufferfish clownfish swordfish")) .put("category", List.of("fish")) .put("public", false) .put("text", "pufferfish clownfish swordfish") .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .schema( Schema.builder() .put( "text", AttributeSchemaConfig.builder() .type("string") // More schema & FTS options: // https://turbopuffer.com/docs/write#schema .fullTextSearch(FullTextSearchConfig.defaults()) .regex(true) .build() ) .put( "category", AttributeSchemaConfig.builder() .type("[]string") .fullTextSearch(FullTextSearchConfig.defaults()) .build() ) .build() ) .build() ); // Query nearest neighbors with a filter var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.vector("vector", embed("arctic sea mammal"))) .limit(10) .filters(Filter.eq("public", true)) .build() ); System.out.println(queryResult); // NamespaceQueryResponse{rows=[{$dist=0.42773545, id=1}]} // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search var ftsResult = ns.query( NamespaceQueryParams.builder() .limit(10) .filters(Filter.eq("public", true)) .rankBy( RankByText.sum( RankByText.product(2, RankByText.bm25("category", "mammal")), RankByText.bm25("text", "quick walrus") ) ) .build() ); System.out.println(ftsResult); // NamespaceQueryResponse{rows=[{$dist=0.7549128, id=1}]} // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = ns.query( NamespaceQueryParams.builder().limit(10).filters(Filter.regex("text", "\\w+fish")).build() ); System.out.println(regexResult); // Count documents grouped by category var groupedResult = ns.query( NamespaceQueryParams.builder() .aggregateBy(Map.of("count_by_category", AggregateBy.count("id"))) .groupBy(List.of("category")) .build() ); System.out.println(groupedResult.aggregationGroups().get()); // [{category=[fish], count_by_category=1}, {category=[mammal], count_by_category=1}] } // Create an embedding with Gemini. // Requires GEMINI_API_KEY to be set (https://aistudio.google.com/app/apikey) public static List embed(String text) { try (Client client = Client.builder().apiKey(System.getenv("GEMINI_API_KEY")).build()) { return client.models .embedContent( "gemini-embedding-001", text, EmbedContentConfig.builder().taskType("RETRIEVAL_DOCUMENT").build() ) .embeddings() .orElseThrow() .get(0) .values() .orElseThrow(); } } } ``` ```java // Gradle: implementation("com.turbopuffer:turbopuffer-java:+"), implementation("com.openai:openai-java:+") package com.turbopuffer.docs; import com.openai.client.okhttp.*; import com.openai.models.embeddings.*; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; class QuickStartQwen { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // created here: https://turbopuffer.com/dashboard .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespace = Optional.ofNullable(System.getenv("TURBOPUFFER_NAMESPACE")).orElse( "quickstart-" + UUID.randomUUID().toString().substring(0, 8) ); var ns = tpuf.namespace(namespace); // Upsert documents with vectors and attributes ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("vector", embed("walrus narwhal")) .put("category", List.of("mammal")) .put("public", true) .put("text", "walrus narwhal") .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("vector", embed("pufferfish clownfish swordfish")) .put("category", List.of("fish")) .put("public", false) .put("text", "pufferfish clownfish swordfish") .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .schema( Schema.builder() .put( "text", AttributeSchemaConfig.builder() .type("string") // More schema & FTS options: // https://turbopuffer.com/docs/write#schema .fullTextSearch(FullTextSearchConfig.defaults()) .regex(true) .build() ) .put( "category", AttributeSchemaConfig.builder() .type("[]string") .fullTextSearch(FullTextSearchConfig.defaults()) .build() ) .build() ) .build() ); // Query nearest neighbors with a filter var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.vector("vector", embed("arctic sea mammal"))) .limit(10) .filters(Filter.eq("public", true)) .build() ); System.out.println(queryResult); // NamespaceQueryResponse{rows=[{$dist=0.42773545, id=1}]} // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search var ftsResult = ns.query( NamespaceQueryParams.builder() .limit(10) .filters(Filter.eq("public", true)) .rankBy( RankByText.sum( RankByText.product(2, RankByText.bm25("category", "mammal")), RankByText.bm25("text", "quick walrus") ) ) .build() ); System.out.println(ftsResult); // NamespaceQueryResponse{rows=[{$dist=0.7549128, id=1}]} // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = ns.query( NamespaceQueryParams.builder().limit(10).filters(Filter.regex("text", "\\w+fish")).build() ); System.out.println(regexResult); // Count documents grouped by category var groupedResult = ns.query( NamespaceQueryParams.builder() .aggregateBy(Map.of("count_by_category", AggregateBy.count("id"))) .groupBy(List.of("category")) .build() ); System.out.println(groupedResult.aggregationGroups().get()); // [{category=[fish], count_by_category=1}, {category=[mammal], count_by_category=1}] } // Create an embedding with Qwen on Fireworks. // Requires FIREWORKS_API_KEY to be set (https://fireworks.ai/settings/users/api-keys) public static List embed(String text) { var client = OpenAIOkHttpClient.builder() .apiKey(System.getenv("FIREWORKS_API_KEY")) .baseUrl("https://api.fireworks.ai/inference/v1") .build(); return client .embeddings() .create( EmbeddingCreateParams.builder() .input(text) .model(EmbeddingModel.of("fireworks/qwen3-embedding-8b")) .build() ) .data() .get(0) .embedding(); } } ``` ```java // Gradle: implementation("com.turbopuffer:turbopuffer-java:+"), implementation("com.openai:openai-java:+") package com.turbopuffer.docs; import com.openai.client.okhttp.*; import com.openai.models.embeddings.*; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; class QuickStartVoyage { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .apiKey(System.getenv("TURBOPUFFER_API_KEY")) // created here: https://turbopuffer.com/dashboard .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var namespace = Optional.ofNullable(System.getenv("TURBOPUFFER_NAMESPACE")).orElse( "quickstart-" + UUID.randomUUID().toString().substring(0, 8) ); var ns = tpuf.namespace(namespace); // Upsert documents with vectors and attributes ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("vector", embed("walrus narwhal")) .put("category", List.of("mammal")) .put("public", true) .put("text", "walrus narwhal") .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("vector", embed("pufferfish clownfish swordfish")) .put("category", List.of("fish")) .put("public", false) .put("text", "pufferfish clownfish swordfish") .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .schema( Schema.builder() .put( "text", AttributeSchemaConfig.builder() .type("string") // More schema & FTS options: // https://turbopuffer.com/docs/write#schema .fullTextSearch(FullTextSearchConfig.defaults()) .regex(true) .build() ) .put( "category", AttributeSchemaConfig.builder() .type("[]string") .fullTextSearch(FullTextSearchConfig.defaults()) .build() ) .build() ) .build() ); // Query nearest neighbors with a filter var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.vector("vector", embed("arctic sea mammal"))) .limit(10) .filters(Filter.eq("public", true)) .build() ); System.out.println(queryResult); // NamespaceQueryResponse{rows=[{$dist=0.42773545, id=1}]} // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search var ftsResult = ns.query( NamespaceQueryParams.builder() .limit(10) .filters(Filter.eq("public", true)) .rankBy( RankByText.sum( RankByText.product(2, RankByText.bm25("category", "mammal")), RankByText.bm25("text", "quick walrus") ) ) .build() ); System.out.println(ftsResult); // NamespaceQueryResponse{rows=[{$dist=0.7549128, id=1}]} // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = ns.query( NamespaceQueryParams.builder().limit(10).filters(Filter.regex("text", "\\w+fish")).build() ); System.out.println(regexResult); // Count documents grouped by category var groupedResult = ns.query( NamespaceQueryParams.builder() .aggregateBy(Map.of("count_by_category", AggregateBy.count("id"))) .groupBy(List.of("category")) .build() ); System.out.println(groupedResult.aggregationGroups().get()); // [{category=[fish], count_by_category=1}, {category=[mammal], count_by_category=1}] } // Create an embedding with Voyage. // Requires VOYAGE_API_KEY to be set: // https://dashboard.voyageai.com/organization/api-keys public static List embed(String text) { var client = OpenAIOkHttpClient.builder() .apiKey(System.getenv("VOYAGE_API_KEY")) .baseUrl("https://api.voyageai.com/v1") .build(); return client .embeddings() .create( EmbeddingCreateParams.builder() .input(text) .model(EmbeddingModel.of("voyage-4-lite")) .build() ) .data() .get(0) .embedding(); } } ``` ```cs // dotnet add package Turbopuffer // dotnet add package OpenAI using System; using System.Collections.Generic; using System.Linq; using OpenAI.Embeddings; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // API tokens are created in the dashboard: https://turbopuffer.com/dashboard // Loaded from TURBOPUFFER_API_KEY env var by default. Override if necessary: // ApiKey = "...", // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("quickstart-example-csharp"); // Upsert documents with vectors and attributes await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("vector", OpenAiOrRandVector("walrus narwhal")) .Set("category", "mammal") .Set("public", 1) .Set("text", "walrus narwhal"), new Row() .Set("id", 2) .Set("vector", OpenAiOrRandVector("pufferfish clownfish swordfish")) .Set("category", "fish") .Set("public", 0) .Set("text", "pufferfish clownfish swordfish"), ], DistanceMetric = DistanceMetric.CosineDistance, Schema = new Dictionary { ["text"] = new AttributeSchemaConfig { Type = "string", // More schema & FTS options // https://turbopuffer.com/docs/write#schema FullTextSearch = true, }, }, } ); // Query nearest neighbors with filter var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", OpenAiOrRandVector("arctic sea mammal")), Limit = 10, Filters = Filter.And(Filter.Eq("category", "mammal"), Filter.Eq("public", 1)), IncludeAttributes = new List { "category" }, } ); foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } // {"$dist": 0.42773545, "id": 1, "category": "mammal"} // Full-text search on an attribute // To combine FTS and vector search concurrently and fuse results, see https://turbopuffer.com/docs/hybrid-search var ftsResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Eq("category", "mammal"), RankBy = RankByText.BM25("text", "quick walrus"), } ); foreach (var row in ftsResult.GetRows()) { Console.WriteLine(row); } // {"$dist": 0.7549128, "id": 1} // Vectors can be updated by passing new data for an existing ID await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row().Set("id", 1).Set("vector", OpenAiOrRandVector("foo")).Set("name", "foo").Set("public", 1), new Row().Set("id", 2).Set("vector", OpenAiOrRandVector("foo")).Set("name", "foo").Set("public", 1), new Row().Set("id", 3).Set("vector", OpenAiOrRandVector("foo")).Set("name", "foo").Set("public", 1), ], DistanceMetric = DistanceMetric.CosineDistance, } ); // Vectors are deleted by ID. await ns.Write(new NamespaceWriteParams { Deletes = [1L, 3L] }); // Create an embedding with OpenAI, could be {Cohere, Voyage, Mixed Bread, ...} // Requires OPENAI_API_KEY to be set (https://platform.openai.com/settings/organization/api-keys) static List OpenAiOrRandVector(string text) { if (Environment.GetEnvironmentVariable("OPENAI_API_KEY") == null) { Console.WriteLine("OPENAI_API_KEY not set, using random vectors"); return RandVector(); } try { var client = new EmbeddingClient( "text-embedding-3-small", Environment.GetEnvironmentVariable("OPENAI_API_KEY") ); return [.. client.GenerateEmbedding(text).Value.ToFloats().Span]; } catch (Exception e) { Console.WriteLine($"OpenAI error, using random vectors: {e.Message}"); return RandVector(); } } static List RandVector() { return new List { (float)Random.Shared.NextDouble(), (float)Random.Shared.NextDouble() }; } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { Region = "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }; var namespaceName = Environment.GetEnvironmentVariable("TURBOPUFFER_NAMESPACE") ?? $"quickstart-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; var ns = tpuf.Namespace(namespaceName); // Upsert documents with vectors and attributes await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("vector", Embed("walrus narwhal")) .Set("category", new[] { "mammal" }) .Set("public", true) .Set("text", "walrus narwhal"), new Row() .Set("id", 2) .Set("vector", Embed("pufferfish clownfish swordfish")) .Set("category", new[] { "fish" }) .Set("public", false) .Set("text", "pufferfish clownfish swordfish"), ], DistanceMetric = DistanceMetric.CosineDistance, Schema = new Dictionary { ["text"] = new AttributeSchemaConfig { Type = "string", // More schema & FTS options: // https://turbopuffer.com/docs/write#schema FullTextSearch = true, Regex = true, }, ["category"] = new AttributeSchemaConfig { Type = "[]string", FullTextSearch = true }, }, } ); // Query nearest neighbors with a filter var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", Embed("arctic sea mammal")), Limit = 10, Filters = Filter.Eq("public", true), } ); foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search var ftsResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Eq("public", true), RankBy = RankByText.Sum( RankByText.Product(2, RankByText.BM25("category", "mammal")), RankByText.BM25("text", "quick walrus") ), } ); foreach (var row in ftsResult.GetRows()) { Console.WriteLine(row); } // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Regex("text", "\\w+fish") } ); foreach (var row in regexResult.GetRows()) { Console.WriteLine(row); } // Count documents grouped by category var groupedResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["count_by_category"] = AggregateBy.Count("id"), }, GroupBy = [GroupBy.Attr("category")], } ); foreach (var group in groupedResult.GetAggregationGroups()) { Console.WriteLine(group); } // Create an embedding with Cohere. // Requires COHERE_API_KEY to be set (https://dashboard.cohere.com/api-keys) static List Embed(string text) { using var http = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Post, "https://api.cohere.com/v2/embed") { Content = new StringContent( JsonSerializer.Serialize( new { model = "embed-v4.0", input_type = "search_document", texts = new[] { text }, embedding_types = new[] { "float" }, } ), Encoding.UTF8, "application/json" ), }; request.Headers.Authorization = new AuthenticationHeaderValue( "Bearer", Environment.GetEnvironmentVariable("COHERE_API_KEY") ); var response = http.Send(request); response.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(response.Content.ReadAsStream()); return doc .RootElement.GetProperty("embeddings") .GetProperty("float")[0] .EnumerateArray() .Select(v => v.GetSingle()) .ToList(); } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Text.Json; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { Region = "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }; var namespaceName = Environment.GetEnvironmentVariable("TURBOPUFFER_NAMESPACE") ?? $"quickstart-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; var ns = tpuf.Namespace(namespaceName); // Upsert documents with vectors and attributes await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("vector", Embed("walrus narwhal")) .Set("category", new[] { "mammal" }) .Set("public", true) .Set("text", "walrus narwhal"), new Row() .Set("id", 2) .Set("vector", Embed("pufferfish clownfish swordfish")) .Set("category", new[] { "fish" }) .Set("public", false) .Set("text", "pufferfish clownfish swordfish"), ], DistanceMetric = DistanceMetric.CosineDistance, Schema = new Dictionary { ["text"] = new AttributeSchemaConfig { Type = "string", // More schema & FTS options: // https://turbopuffer.com/docs/write#schema FullTextSearch = true, Regex = true, }, ["category"] = new AttributeSchemaConfig { Type = "[]string", FullTextSearch = true }, }, } ); // Query nearest neighbors with a filter var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", Embed("arctic sea mammal")), Limit = 10, Filters = Filter.Eq("public", true), } ); foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search var ftsResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Eq("public", true), RankBy = RankByText.Sum( RankByText.Product(2, RankByText.BM25("category", "mammal")), RankByText.BM25("text", "quick walrus") ), } ); foreach (var row in ftsResult.GetRows()) { Console.WriteLine(row); } // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Regex("text", "\\w+fish") } ); foreach (var row in regexResult.GetRows()) { Console.WriteLine(row); } // Count documents grouped by category var groupedResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["count_by_category"] = AggregateBy.Count("id"), }, GroupBy = [GroupBy.Attr("category")], } ); foreach (var group in groupedResult.GetAggregationGroups()) { Console.WriteLine(group); } // Create an embedding with Gemini. // Requires GEMINI_API_KEY to be set (https://aistudio.google.com/app/apikey) static List Embed(string text) { var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); using var http = new HttpClient(); var request = new HttpRequestMessage( HttpMethod.Post, "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=" + apiKey ) { Content = new StringContent( JsonSerializer.Serialize( new { model = "models/gemini-embedding-001", content = new { parts = new[] { new { text } } }, taskType = "RETRIEVAL_DOCUMENT", } ), Encoding.UTF8, "application/json" ), }; var response = http.Send(request); response.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(response.Content.ReadAsStream()); return doc .RootElement.GetProperty("embedding") .GetProperty("values") .EnumerateArray() .Select(v => v.GetSingle()) .ToList(); } ``` ```cs // dotnet add package Turbopuffer // dotnet add package OpenAI using System; using System.ClientModel; using System.Collections.Generic; using System.Linq; using OpenAI; using OpenAI.Embeddings; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { Region = "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }; var namespaceName = Environment.GetEnvironmentVariable("TURBOPUFFER_NAMESPACE") ?? $"quickstart-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; var ns = tpuf.Namespace(namespaceName); // Upsert documents with vectors and attributes await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("vector", Embed("walrus narwhal")) .Set("category", new[] { "mammal" }) .Set("public", true) .Set("text", "walrus narwhal"), new Row() .Set("id", 2) .Set("vector", Embed("pufferfish clownfish swordfish")) .Set("category", new[] { "fish" }) .Set("public", false) .Set("text", "pufferfish clownfish swordfish"), ], DistanceMetric = DistanceMetric.CosineDistance, Schema = new Dictionary { ["text"] = new AttributeSchemaConfig { Type = "string", // More schema & FTS options: // https://turbopuffer.com/docs/write#schema FullTextSearch = true, Regex = true, }, ["category"] = new AttributeSchemaConfig { Type = "[]string", FullTextSearch = true }, }, } ); // Query nearest neighbors with a filter var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", Embed("arctic sea mammal")), Limit = 10, Filters = Filter.Eq("public", true), } ); foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search var ftsResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Eq("public", true), RankBy = RankByText.Sum( RankByText.Product(2, RankByText.BM25("category", "mammal")), RankByText.BM25("text", "quick walrus") ), } ); foreach (var row in ftsResult.GetRows()) { Console.WriteLine(row); } // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Regex("text", "\\w+fish") } ); foreach (var row in regexResult.GetRows()) { Console.WriteLine(row); } // Count documents grouped by category var groupedResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["count_by_category"] = AggregateBy.Count("id"), }, GroupBy = [GroupBy.Attr("category")], } ); foreach (var group in groupedResult.GetAggregationGroups()) { Console.WriteLine(group); } // Create an embedding with Qwen on Fireworks. // Requires FIREWORKS_API_KEY to be set (https://fireworks.ai/settings/users/api-keys) static List Embed(string text) { var client = new EmbeddingClient( "fireworks/qwen3-embedding-8b", new ApiKeyCredential(Environment.GetEnvironmentVariable("FIREWORKS_API_KEY")!), new OpenAIClientOptions { Endpoint = new Uri("https://api.fireworks.ai/inference/v1") } ); return [.. client.GenerateEmbedding(text).Value.ToFloats().Span]; } ``` ```cs // dotnet add package Turbopuffer // dotnet add package OpenAI using System; using System.ClientModel; using System.Collections.Generic; using System.Linq; using OpenAI; using OpenAI.Embeddings; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { Region = "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }; var namespaceName = Environment.GetEnvironmentVariable("TURBOPUFFER_NAMESPACE") ?? $"quickstart-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; var ns = tpuf.Namespace(namespaceName); // Upsert documents with vectors and attributes await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("vector", Embed("walrus narwhal")) .Set("category", new[] { "mammal" }) .Set("public", true) .Set("text", "walrus narwhal"), new Row() .Set("id", 2) .Set("vector", Embed("pufferfish clownfish swordfish")) .Set("category", new[] { "fish" }) .Set("public", false) .Set("text", "pufferfish clownfish swordfish"), ], DistanceMetric = DistanceMetric.CosineDistance, Schema = new Dictionary { ["text"] = new AttributeSchemaConfig { Type = "string", // More schema & FTS options: // https://turbopuffer.com/docs/write#schema FullTextSearch = true, Regex = true, }, ["category"] = new AttributeSchemaConfig { Type = "[]string", FullTextSearch = true }, }, } ); // Query nearest neighbors with a filter var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", Embed("arctic sea mammal")), Limit = 10, Filters = Filter.Eq("public", true), } ); foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } // Full-text search on an attribute // To combine FTS and vector search concurrently, see: // https://turbopuffer.com/docs/hybrid-search var ftsResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Eq("public", true), RankBy = RankByText.Sum( RankByText.Product(2, RankByText.BM25("category", "mammal")), RankByText.BM25("text", "quick walrus") ), } ); foreach (var row in ftsResult.GetRows()) { Console.WriteLine(row); } // Regex filter — matches "pufferfish", "swordfish", "clownfish" var regexResult = await ns.Query( new NamespaceQueryParams { Limit = 10, Filters = Filter.Regex("text", "\\w+fish") } ); foreach (var row in regexResult.GetRows()) { Console.WriteLine(row); } // Count documents grouped by category var groupedResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["count_by_category"] = AggregateBy.Count("id"), }, GroupBy = [GroupBy.Attr("category")], } ); foreach (var group in groupedResult.GetAggregationGroups()) { Console.WriteLine(group); } // Create an embedding with Voyage. // Requires VOYAGE_API_KEY to be set: // https://dashboard.voyageai.com/organization/api-keys static List Embed(string text) { var client = new EmbeddingClient( "voyage-4-lite", new ApiKeyCredential(Environment.GetEnvironmentVariable("VOYAGE_API_KEY")!), new OpenAIClientOptions { Endpoint = new Uri("https://api.voyageai.com/v1") } ); return [.. client.GenerateEmbedding(text).Value.ToFloats().Span]; } ``` ```ruby # $ gem install turbopuffer require "turbopuffer" require "securerandom" require "json" tpuf = Turbopuffer::Client.new( api_key: ENV["TURBOPUFFER_API_KEY"], # created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = ENV["TURBOPUFFER_NAMESPACE"] || "quickstart-#{SecureRandom.hex(4)}" ns = tpuf.namespace(namespace) # Use provider-free random vectors for the quickstart. # Switch the embedding provider dropdown to see real embedding API calls. def embed(_text) [rand, rand] end # Upsert documents with vectors and attributes ns.write( upsert_rows: [ { id: 1, vector: embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { # Configure FTS/BM25. Other attributes have inferred types. type: "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema full_text_search: true, regex: true, }, category: { type: "[]string", full_text_search: true }, }, ) # Query nearest neighbors with a filter result = ns.query( rank_by: ["vector", "ANN", embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], ) puts result.rows # {id: 1, "$dist": 0.42773545} # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search result = ns.query( limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], ) puts result.rows # {id: 1, "$dist": 0.7549128} # Regex filter — matches "pufferfish", "swordfish", "clownfish" result = ns.query( limit: 10, filters: ["text", "Regex", "\\w+fish"], ) puts result.rows # Count documents grouped by category grouped_result = ns.query( aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], ) puts grouped_result.aggregation_groups # {category: ["fish"], count_by_category: 1} # {category: ["mammal"], count_by_category: 1} ``` ```ruby # $ gem install turbopuffer cohere-ruby require "turbopuffer" require "securerandom" require "cohere" tpuf = Turbopuffer::Client.new( api_key: ENV["TURBOPUFFER_API_KEY"], # created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = ENV["TURBOPUFFER_NAMESPACE"] || "quickstart-#{SecureRandom.hex(4)}" ns = tpuf.namespace(namespace) # Create an embedding with Cohere. # Requires COHERE_API_KEY to be set (https://dashboard.cohere.com/api-keys) def embed(text) Cohere::Client .new(api_key: ENV["COHERE_API_KEY"]) .embed( model: "embed-v4.0", texts: [text], input_type: "search_document", embedding_types: ["float"], ) .embeddings .float .first end # Upsert documents with vectors and attributes ns.write( upsert_rows: [ { id: 1, vector: embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { # Configure FTS/BM25. Other attributes have inferred types (`public`: int). type: "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema full_text_search: true, regex: true, }, category: { type: "[]string", full_text_search: true }, }, ) # Query nearest neighbors with a filter result = ns.query( rank_by: ["vector", "ANN", embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], ) puts result.rows # {id: 1, "$dist": 0.42773545} # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search result = ns.query( limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], ) puts result.rows # {id: 1, "$dist": 0.7549128} # Regex filter — matches "pufferfish", "swordfish", "clownfish" result = ns.query( limit: 10, filters: ["text", "Regex", "\\w+fish"], ) puts result.rows # Count documents grouped by category grouped_result = ns.query( aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], ) puts grouped_result.aggregation_groups # {category: ["fish"], count_by_category: 1} # {category: ["mammal"], count_by_category: 1} ``` ```ruby # $ gem install turbopuffer openai require "turbopuffer" require "securerandom" require "openai" tpuf = Turbopuffer::Client.new( api_key: ENV["TURBOPUFFER_API_KEY"], # created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = ENV["TURBOPUFFER_NAMESPACE"] || "quickstart-#{SecureRandom.hex(4)}" ns = tpuf.namespace(namespace) # Create an embedding with Qwen on Fireworks. # Requires FIREWORKS_API_KEY to be set (https://fireworks.ai/settings/users/api-keys) def embed(text) OpenAI::Client .new( api_key: ENV["FIREWORKS_API_KEY"], base_url: "https://api.fireworks.ai/inference/v1", ) .embeddings .create(model: "fireworks/qwen3-embedding-8b", input: text) .data[0] .embedding end # Upsert documents with vectors and attributes ns.write( upsert_rows: [ { id: 1, vector: embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { # Configure FTS/BM25. Other attributes have inferred types (`public`: int). type: "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema full_text_search: true, regex: true, }, category: { type: "[]string", full_text_search: true }, }, ) # Query nearest neighbors with a filter result = ns.query( rank_by: ["vector", "ANN", embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], ) puts result.rows # {id: 1, "$dist": 0.42773545} # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search result = ns.query( limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], ) puts result.rows # {id: 1, "$dist": 0.7549128} # Regex filter — matches "pufferfish", "swordfish", "clownfish" result = ns.query( limit: 10, filters: ["text", "Regex", "\\w+fish"], ) puts result.rows # Count documents grouped by category grouped_result = ns.query( aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], ) puts grouped_result.aggregation_groups # {category: ["fish"], count_by_category: 1} # {category: ["mammal"], count_by_category: 1} ``` ```ruby # $ gem install turbopuffer openai require "turbopuffer" require "securerandom" require "openai" tpuf = Turbopuffer::Client.new( api_key: ENV["TURBOPUFFER_API_KEY"], # created here: https://turbopuffer.com/dashboard region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = ENV["TURBOPUFFER_NAMESPACE"] || "quickstart-#{SecureRandom.hex(4)}" ns = tpuf.namespace(namespace) # Create an embedding with Voyage. # Requires VOYAGE_API_KEY to be set: # https://dashboard.voyageai.com/organization/api-keys def embed(text) OpenAI::Client .new( api_key: ENV["VOYAGE_API_KEY"], base_url: "https://api.voyageai.com/v1", ) .embeddings .create(model: "voyage-4-lite", input: text) .data[0] .embedding end # Upsert documents with vectors and attributes ns.write( upsert_rows: [ { id: 1, vector: embed("walrus narwhal"), category: ["mammal"], public: true, text: "walrus narwhal", }, { id: 2, vector: embed("pufferfish clownfish swordfish"), category: ["fish"], public: false, text: "pufferfish clownfish swordfish", }, ], distance_metric: "cosine_distance", schema: { text: { # Configure FTS/BM25. Other attributes have inferred types (`public`: int). type: "string", # More schema & FTS options: # https://turbopuffer.com/docs/write#schema full_text_search: true, regex: true, }, category: { type: "[]string", full_text_search: true }, }, ) # Query nearest neighbors with a filter result = ns.query( rank_by: ["vector", "ANN", embed("arctic sea mammal")], limit: 10, filters: ["public", "Eq", true], ) puts result.rows # {id: 1, "$dist": 0.42773545} # Full-text search on an attribute # To combine FTS and vector search concurrently, see: # https://turbopuffer.com/docs/hybrid-search result = ns.query( limit: 10, filters: ["public", "Eq", true], rank_by: ["Sum", [ ["Product", 2, ["category", "BM25", "mammal"]], ["text", "BM25", "quick walrus"], ]], ) puts result.rows # {id: 1, "$dist": 0.7549128} # Regex filter — matches "pufferfish", "swordfish", "clownfish" result = ns.query( limit: 10, filters: ["text", "Regex", "\\w+fish"], ) puts result.rows # Count documents grouped by category grouped_result = ns.query( aggregate_by: { count_by_category: ["Count"] }, group_by: ["category"], ) puts grouped_result.aggregation_groups # {category: ["fish"], count_by_category: 1} # {category: ["mammal"], count_by_category: 1} ``` ## Conditional writes Only update a document when a condition is met -- for example, keep only the newest [timestamped write](/docs/write#conditional-writes). Continue from the same namespace and only apply the write when the new `updated_at` is newer than the stored one, or when the row has no timestamp yet. ```bash curl $TPUF_URL/v2/namespaces/$TPUF_NAMESPACE \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ {"id": 1, "vector": [0.5, 0.6], "category": ["mammal"], "updated_at": "2024-04-16T09:27:32Z"} ], "upsert_condition": [ "Or", [ ["updated_at", "Lt", {"$ref_new": "updated_at"}], ["updated_at", "Eq", null] ] ], "distance_metric": "cosine_distance" }' # {"rows_affected": 1, ...} ``` ```python # Only update if this write has a newer timestamp result = ns.write( upsert_rows=[{ 'id': 1, 'vector': embed("updated walrus"), 'category': ["mammal"], 'updated_at': "2024-04-16T09:27:32Z", }], upsert_condition=( 'Or', [ ('updated_at', 'Lt', {'$ref_new': 'updated_at'}), ('updated_at', 'Eq', None), ] ), distance_metric='cosine_distance', ) print(result.rows_affected) # 1 ``` ```typescript // Only update if this write has a newer timestamp const writeResult = await ns.write({ upsert_rows: [{ id: 1, vector: embed("updated walrus"), category: ["mammal"], updated_at: "2024-04-16T09:27:32Z", }], upsert_condition: [ "Or", [ ["updated_at", "Lt", { $ref_new: "updated_at" }], ["updated_at", "Eq", null], ], ], distance_metric: "cosine_distance", }); console.log(writeResult.rows_affected); // 1 ``` ```go res, err := ns.Write(ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{{ "id": 1, "vector": embed(ctx, "updated walrus"), "category": []string{"mammal"}, "updated_at": "2024-04-16T09:27:32Z", }}, UpsertCondition: turbopuffer.NewFilterOr([]turbopuffer.Filter{ turbopuffer.NewFilterLt("updated_at", turbopuffer.NewExprRefNew("updated_at")), turbopuffer.NewFilterEq("updated_at", nil), }), DistanceMetric: turbopuffer.DistanceMetricCosineDistance, }) if err != nil { panic(err) } fmt.Println(res.RowsAffected) // 1 ``` ```java var writeResult = ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("vector", embed("updated walrus")) .put("category", List.of("mammal")) .put("updated_at", "2024-04-16T09:27:32Z") .build() ) .upsertCondition( Filter.or( Filter.lt("updated_at", Expr.refNew("updated_at")), Filter.eq("updated_at", null) ) ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .build() ); System.out.println(writeResult.rowsAffected()); // 1 ``` ```cs // Only update if this write has a newer timestamp var writeResult = await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("vector", Embed("updated walrus")) .Set("category", new[] { "mammal" }) .Set("updated_at", "2024-04-16T09:27:32Z"), ], UpsertCondition = Filter.Or( Filter.Lt("updated_at", Expr.RefNew("updated_at")), Filter.Eq("updated_at", null) ), DistanceMetric = DistanceMetric.CosineDistance, } ); Console.WriteLine(writeResult.RowsAffected); // 1 ``` ```ruby # Only update if this write has a newer timestamp result = ns.write( upsert_rows: [{ id: 1, vector: embed("updated walrus"), category: ["mammal"], updated_at: "2024-04-16T09:27:32Z", }], upsert_condition: [ "Or", [ ["updated_at", "Lt", { '$ref_new': "updated_at" }], ["updated_at", "Eq", nil], ], ], distance_metric: "cosine_distance", ) puts result.rows_affected # 1 ``` ## Branching Instantly [clone a namespace](/docs/branching) with copy-on-write. Use it to spin up isolated test environments, keep lightweight versioned copies, or take snapshots before risky changes. Constant-time regardless of size, and fully independent after creation. Finally, branch the same namespace into a fresh copy and query it independently. ```bash # Branch from the quickstart namespace curl $TPUF_URL/v2/namespaces/$TPUF_BRANCH_NAMESPACE \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d "{\"branch_from_namespace\": \"$TPUF_NAMESPACE\"}" # Query the branch independently curl $TPUF_URL/v2/namespaces/$TPUF_BRANCH_NAMESPACE/query \ -X POST --fail-with-body \ -H "Authorization: Bearer $api_key" \ -H 'Content-Type: application/json' \ -d '{"rank_by": ["vector", "ANN", [0.1, 0.2]], "limit": 5}' ``` ```python branch_namespace = f"{namespace}-branch" branch = tpuf.namespace(branch_namespace) branch.write(branch_from_namespace=namespace) # Query the branch independently print(branch.query( rank_by=("vector", "ANN", embed("sea creature")), limit=5, )) ``` ```typescript const branchNamespace = `${namespace}-branch`; const branch = tpuf.namespace(branchNamespace); await branch.write({ branch_from_namespace: namespace, }); // Query the branch independently const branchResult = await branch.query({ rank_by: ["vector", "ANN", embed("sea creature")], limit: 5, }); console.log(branchResult.rows); ``` ```go branchNamespace := namespace + "-branch" branch := tpuf.Namespace(branchNamespace) if _, err := branch.Write(ctx, turbopuffer.NamespaceWriteParams{ BranchFromNamespace: turbopuffer.BranchFromNamespaceParams{ SourceNamespace: namespace, }, }); err != nil { panic(err) } // Query the branch independently branchResult, err := branch.Query(ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAnn("vector", embed(ctx, "sea creature")), Limit: turbopuffer.LimitParam{Total: 5}, }) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(branchResult.Rows)) ``` ```java var branchNamespace = namespace + "-branch"; var branch = tpuf.namespace(branchNamespace); branch.branchFrom(NamespaceBranchFromParams.builder().sourceNamespace(namespace).build()); // Query the branch independently var branchResult = branch.query( NamespaceQueryParams.builder() .rankBy(RankBy.ann("vector", embed("sea creature"))) .limit(5) .build() ); System.out.println(branchResult); ``` ```cs var branchName = namespaceName + "-branch"; var branch = tpuf.Namespace(branchName); await branch.BranchFrom(new NamespaceBranchFromParams { SourceNamespace = namespaceName }); // Query the branch independently var branchResult = await branch.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", Embed("sea creature")), Limit = 5 } ); foreach (var row in branchResult.GetRows()) { Console.WriteLine(row); } ``` ```ruby branch_namespace = "#{namespace}-branch" branch = tpuf.namespace(branch_namespace) branch.write(branch_from_namespace: namespace) # Query the branch independently result = branch.query( rank_by: ["vector", "ANN", embed("sea creature")], limit: 5, ) puts result.rows ``` ## What's next * [Write docs](/docs/write) -- schema, patches, deletes, delete-by-filter * [Query docs](/docs/query) -- kNN, hybrid search, ordering, grouped aggregations * [Concepts](/docs/concepts) -- namespaces, attributes, distance metrics * [Architecture](/docs/architecture) -- how object storage makes this work --- This page: [/docs/quickstart.md](https://turbopuffer.com/docs/quickstart.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Evaluate recall POST /v1/namespaces/:namespace/_debug/recall Evaluate recall for documents in a namespace. When you call this endpoint, it selects `num` random vectors that were previously inserted. For each of these vectors, it performs an ANN index search as well as a ground truth exhaustive search. Recall is calculated as the ratio of matching vectors between the two search results. This endpoint also returns the average number of results returned from both the ANN index search and the exhaustive search (ideally, these are equal). Example of 90% recall@10: ``` ANN Exact ┌────────────────────────────┐ ┌────────────────────────────┐ │id: 9, score: 0.12 │▒ │id: 9, score: 0.12 │▒ ├────────────────────────────┤▒ ├────────────────────────────┤▒ │id: 2, score: 0.18 │▒ │id: 2, score: 0.18 │▒ ├────────────────────────────┤▒ ├────────────────────────────┤▒ │id: 8, score: 0.29 │▒ │id: 8, score: 0.29 │▒ ┌────────────────────────────┤▒ ├────────────────────────────┤▒ │id: 1, score: 0.55 │▒ │id: 1, score: 0.55 │▒ ┣─━─━─━─━─━─━─━─━─━─━─━─━─━─━┘▒ Mismatch ┣─━─━─━─━─━─━─━─━─━─━─━─━─━─━┘▒ id: 0, score: 0.90 ┃▒◀─────────────▶ id: 4, score: 0.85 ┃▒ ┗ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ▒ ┗ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ``` We use this endpoint internally to measure recall. See this [blog post](/blog/continuous-recall) for more. This endpoint can run asynchronously. See [Asynchronous requests](/docs/overview#asynchronous-requests). ## Request **num** number default: 25 number of searches to run. --- **top_k** number default: 10 search for top_k nearest neighbors. --- **filters** object optional filter by attributes, see [filtering parameters](/docs/reference/query#filter-parameters) for more info. --- **rank_by** array The [ranking function](/docs/query#param-rank_by) to evaluate recall for. If this field is provided `num` must be either `null` or `1`. ## Response **avg_recall** number The average recall across all sampled queries, expressed as a decimal between 0 and 1. A value of 1.0 indicates perfect recall (100% of exhaustive search results were found by the approximate nearest neighbour search). **avg_exhaustive_count** number The average number of results returned by the exhaustive search across all queries. This represents the ideal number of results that should be returned. **avg_ann_count** number The average number of results returned by the approximate nearest neighbor index search across all queries. In most cases this should equal `avg_exhaustive_count`. ## Examples ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v1/namespaces/recall-example-curl/_debug/recall \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "num": 5, "top_k": 10 }' # Response payload # { # "avg_recall": 1.0, # "avg_exhaustive_count": 10.0, # "avg_ann_count": 10.0 # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'recall-example-py') # If an error occurs, this call raises a turbopuffer.APIError if a retry was not successful. recall = ns.recall(num=5, top_k=10) print(recall) # NamespaceRecallResponse(avg_ann_count=10.0, avg_exhaustive_count=10.0, avg_recall=1.0) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`recall-example-ts`); const recall = await ns.recall({ num: 5, top_k: 10, }); console.log(recall); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("recall-example-go") // If an error occurs, this call raises an error if a retry was not successful. recall, err := ns.Recall(ctx, turbopuffer.NamespaceRecallParams{ Num: turbopuffer.Int(5), TopK: turbopuffer.Int(10), }) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(recall)) // returns a NamespaceRecallResult } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class Recall { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("recall-example-java"); // If an error occurs, this call raises a TurbopufferServiceException if // a retry was not successful. var recall = ns.recall(NamespaceRecallParams.builder().num(5).topK(10).build()); System.out.println(recall); // NamespaceRecallResponse{avgAnnCount=10.0, avgExhaustiveCount=10.0, avgRecall=1.0} } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("recall-example-csharp"); // If an error occurs, this call raises a TurbopufferApiException if // a retry was not successful. var recall = await ns.Recall(new NamespaceRecallParams { Num = 5, TopK = 10 }); Console.WriteLine(recall); // {"avg_recall": 1.0, "avg_exhaustive_count": 10.0, "avg_ann_count": 10.0} ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("recall-example-rb") # If an error occurs, this call raises a Turbopuffer::Errors::APIError if a retry was not successful. recall = ns.recall(num: 5, top_k: 10) puts recall # {avg_ann_count: 10.0, avg_exhaustive_count: 10.0, avg_recall: 1.0} ``` How to interpret this response: - A recall of 1.0 means that 100% of the ideal results (from the exhaustive search) were also present in the approximate ANN results - `avg_ann_count` equals `avg_exhaustive_count`, meaning the approximate search returned the same number of results as the exhaustive ## Billing Billed as queries when `avg_recall` is at least 0.9 and the namespace is not empty. The number of queries is one per sample per 100K documents, with a minimum of `num` queries. For example, `num=30` on a 1M document namespace is billed as 300 queries. On a smaller namespace with under 100K documents and `num=30`, it would be 30 queries. --- This page: [/docs/recall.md](https://turbopuffer.com/docs/recall.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Regions We support Azure for "Deploy in your VPC", but no public regions yet. [Contact us](/contact) if you need a public Azure region. In addition to these public clusters, we run dedicated clusters in various other regions for single-tenancy customers and in any region inside your VPC in AWS, GCP and Azure (BYOC). We can spin up dedicated or BYOC clusters in hours upon request, [contact us](/contact). We will continue to expand public regions with demand. ```bash REGION_URL="https://gcp-us-east4.turbopuffer.com" # choose best region: https://turbopuffer.com/docs/regions # First, create a namespace and add some data curl --fail-with-body -X POST "$REGION_URL/v2/namespaces/region-example-curl" \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "upsert_rows": [ {"id": 1, "vector": [0.1, 0.2, 0.3]}, {"id": 2, "vector": [0.4, 0.5, 0.6]} ], "distance_metric": "cosine_distance" }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-east4', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'region-example-py') ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-east4", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`region-example-ts`); ``` ```go package main import ( "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-east4"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("region-example-go") // Use the namespace to avoid "declared and not used" error fmt.Printf("Created namespace in region: %v\n", ns) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.util.*; import java.util.stream.*; public class Region { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-east4") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("region-example-java"); } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-east4", }; var ns = tpuf.Namespace("region-example-csharp"); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-east4", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("region-example-rb") ``` To move data between regions, use [`copy_from_namespace`](/docs/write#param-copy_from_namespace) with a cross-region source. For granular control, use the [export](/docs/export) and [write](/docs/write) APIs with a client for each region. ## Cross-Cloud Latency Since response times for vector search are typically above 10ms, the contribution of cross-cloud latency is generally acceptable. Traffic within a cloud provider's region is lower latency (\< 1ms) than cross-cloud traffic (1-10ms), even if the providers are geographically close. For larger customers, cross-cloud interconnects can be set up to reduce network latency. ## Cross-Cloud Egress Fees A common misconception is that as long as your vendor is in the same Cloud as you (e.g. AWS ↔️ AWS), you will be charged lower networking fees. This is generally not the case, as most providers' API endpoints point to public IPs that route through the public internet, unless you've set up a private connect (see below; you'll know if you have). Any traffic leaving your VPC incurs \$0.05-0.09/GB Internet egress fees ([AWS](https://aws.amazon.com/ec2/pricing/on-demand/) / [GCP](https://cloud.google.com/vpc/network-pricing#all-networking-pricing)/ [Azure](https://azure.microsoft.com/en-us/pricing/details/bandwidth/)). Egress networking fees are charged to you on your bill by your provider. For larger customers, we will work with you to set up AWS Private Link, GCP Private Service Connect, Azure Private Link or an interconnect to reduce networking fees to \$0.01/GB. Unless you're transferring tens of billions of vectors per month, this is unlikely to have a large effect on your bill (1B vectors = 6TB would be \$600 of egress, not a significant issue). ## All Regions | Region | URL | Location | | --- | --- | --- | | aws-ap-southeast-2 | https://aws-ap-southeast-2.turbopuffer.com | Sydney, Australia | | aws-ca-central-1 | https://aws-ca-central-1.turbopuffer.com | Montreal, Canada | | aws-eu-central-1 | https://aws-eu-central-1.turbopuffer.com | Frankfurt, Germany | | aws-eu-west-1 | https://aws-eu-west-1.turbopuffer.com | Dublin, Ireland | | aws-eu-west-2 | https://aws-eu-west-2.turbopuffer.com | London, UK | | aws-us-east-1 | https://aws-us-east-1.turbopuffer.com | N. Virginia, US | | aws-us-east-2 | https://aws-us-east-2.turbopuffer.com | Ohio, US | | aws-us-west-2 | https://aws-us-west-2.turbopuffer.com | Oregon, US | | aws-ap-south-1 | https://aws-ap-south-1.turbopuffer.com | Mumbai, India | | aws-sa-east-1 | https://aws-sa-east-1.turbopuffer.com | São Paulo, Brazil | | gcp-us-central1 | https://gcp-us-central1.turbopuffer.com | Iowa, US | | gcp-us-east1 | https://gcp-us-east1.turbopuffer.com | South Carolina, US | | gcp-us-west1 | https://gcp-us-west1.turbopuffer.com | Oregon, US | | gcp-us-east4 | https://gcp-us-east4.turbopuffer.com | N. Virginia, US | | gcp-northamerica-northeast2 | https://gcp-northamerica-northeast2.turbopuffer.com | Toronto, Canada | | gcp-europe-west3 | https://gcp-europe-west3.turbopuffer.com | Frankfurt, Germany | | gcp-europe-west1 | https://gcp-europe-west1.turbopuffer.com | St. Ghislain, Belgium | | gcp-asia-southeast1 | https://gcp-asia-southeast1.turbopuffer.com | Jurong West, Singapore | | gcp-asia-northeast3 | https://gcp-asia-northeast3.turbopuffer.com | Seoul, South Korea | --- This page: [/docs/regions.md](https://turbopuffer.com/docs/regions.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Roadmap & Changelog **Last updated:** June 8, 2026 ## Roadmap - ⚡ Faster & smarter cache warming - 📊 Major dashboard improvements - 📉 Query and indexing performance, _always_ - 📕 More full-text search features ([~rank by attribute~](#february-2026), [~rank by distance~](#february-2026), highlighting, [~fuzzy search~](#may-2026), native search-as-you-type, ...) - ∑ More aggregate functions ([~count~](#may-2025), [~group by~](#august-2025), [~sum~](#november-2025), distinct, min, max...) - ⏱️ Late interaction support - 🗂️ [~Multiple vector columns~](#march-2026) - 🪆 Nested attributes - 🌿 [~Namespace branching~](#may-2026) - 🪓 Automatic namespace sharding - 🫛 Hosted vector embedding ## Changelog ### June 2026 - 🧮 [`i8` vector type](/docs/write#param-type) for [quantization-aware models](/docs/performance#use-smaller-vectors) (75% reduced storage and query cost compared to `f32`) ### May 2026 - 🌿 [Namespace branching](/docs/branching): instant copy-on-write namespace cloning - ✏️ Typo-tolerant string matching with the [Fuzzy filter](/docs/fts#fuzzy-matching) - 🗺️ View [namespace metadata](https://x.com/turbopuffer/status/2054275230584201665) in the dashboard - #️⃣ [C# API client](https://github.com/turbopuffer/turbopuffer-csharp) - 🔤 [`word_v4` tokenizer](/docs/fts#tokenizers), ~3x faster than `word_v3` - ⏱️ Make [asynchronous requests](/docs/overview#asynchronous-requests) to `copy_from_namespace` and `recall` endpoints - 📋 New API key permission to [`list` namespaces](/docs/namespaces) ### April 2026 - 📌 [Pin a namespace to cache](/docs/pinning) for lower cost at high QPS - 🕸️ Support for [sparse vector search](/docs/query/#sparse-vector-search) - ↔️ GCP <-> AWS namespace copies with [`copy_from_namespace`](/docs/write#param-copy_from_namespace) - 🔎 [Search](https://x.com/turbopuffer/status/2049175568621650275) the turbopuffer docs (type cmd+K!) - 🏃‍♂️ Faster commit cadence on AWS for [2.5x lower write latency](https://x.com/turbopuffer/status/2042256535989125461) - 💗 Increased full-text query length limit to [8,192 chars](/docs/limits) - 🦣 Increased attributes per namespace limit to [1,024](/docs/limits) - 🌎 3 new [regions](/docs/regions) (São Paulo, South Carolina, Belgium) - 🧱 Drop #002 (lil block puff) now live on [turbopuffer.supply](https://turbopuffer.supply) ### March 2026 - 🗂️ [Multiple vectors per document](/docs/write#multiple-vector-columns) now available for everyone - 🔐 [Audit logs](/docs/audit-logs) with SIEM integration [opt-in, beta] - ⚡️ [Up to 30% faster AND queries](https://x.com/turbopuffer/status/2029580228121800909) - 📄 [Copy docs as markdown](https://x.com/turbopuffer/status/2036458185591234891) with token counts - 3️⃣ Control term frequency's influence on BM25 scores with [k3](/docs/fts#advanced-tuning) ### February 2026 - 💵 [Query pricing reduced by up to 94%](/docs/pricing-log) - 🔤 [Regex index](https://x.com/turbopuffer/status/2031097396743336409) for much faster `Regex`, `Glob`, and `IGlob` filters - 🍵 [Up to 20% faster filtered FTS queries](https://x.com/turbopuffer/status/2023783644704452759) - 💎 [Use attribute values](/docs/query#rank-by-attribute) to influence full-text search ranking - 📐 Boost result recency with [distance ranking](/docs/query#rank-by-distance) in full-text search - 🗂️ Store and query multiple vectors per document [opt-in, beta] - 🇬🇧 [AWS eu-west-2 (London) region](/docs/regions) - 🐡 [turbopuffer.supply](https://turbopuffer.supply) - the official tpuf store ### January 2026 - 🔡 [FTS v2](/blog/fts-v2): up to 20x faster full-text search, now live for everyone - ☯️ Up to 26% faster FTS queries on high-frequency terms with [dynamic bit set encoding](https://x.com/turbopuffer/status/2012205150669086892) - 🔌 [turbopuffer MCP Server](https://github.com/turbopuffer/turbopuffer-typescript/tree/main/packages/mcp-server) [beta] - 🏷️ Match documents on any token with [`ContainsAnyToken`](/docs/query#param-ContainsAnyToken) - 🔐 [Permissions guide](/docs/permissions) for document-level access control using filters - 📝 [`remove_stopwords`](/docs/write#param-full_text_search) now defaults to `false` for more predictable FTS behavior - 📊 Increase aggregate [`group_by` limit](/docs/query#param-group_by) to 10k ### December 2025 - 🧱 [Redesigned inverted index structure](/blog/fts-v2-postings) for faster full-text search queries - 📤 New [object storage-native indexing queue](https://x.com/turbopuffer/status/2003504825817006549) for up to 10x faster queue time - 🔦 [`kNN` exact search](/docs/query#knn-exact-search) for 100% recall on filtered vector search queries - 🪣 Return a max number of search results per attribute value using [`limit.per`](/docs/query#param-limit) - 🇨🇦 [AWS ca-central-1 (Montreal) region](/docs/regions) - 🌏 [Cross-region backups guide](/docs/backups) - 🤝 Link multiple orgs to a single account for unified billing, SSO, and roles [opt-in, beta] ### November 2025 - 🏎️ [FTS v2](/blog/fts-v2): up to 20x faster full-text search [opt-in, beta] - 🔑 `copy_from_namespace` can now [encrypt with a different key into the destination](/docs/encryption#does-turbopuffer-support-key-rotation) - ✈️ [Cross-region, cross-org `copy_from_namespace`](/docs/write#param-copy_from_namespace) for testing, backups, branching - ⬆️ Max [limit.total](/docs/query#param-limit) raised from 1,200 to 10,000 - ➕ [`Sum` aggregate function](/docs/query#aggregations) - 🔗 [`ContainsTokenSequence` filter](/docs/query#phrase-matching) for full-text phrase matching - 🔡 [`word_v3` tokenizer](/docs/fts#tokenizers) with Unicode-aware segmentation - 🪭 [`ascii_folding` option](/docs/write#param-full_text_search) for full-text search ### October 2025 - ⏫ [Rank by filter](/docs/query#rank-by-filter) for full-text search - 🧩 [`patch_by_filter`](/docs/write#param-patch_by_filter) - 🔘 [`[]bool` support](/docs/write#schema) - 🏎️ [Improved performance](https://x.com/turbopuffer/status/1989306083517804937) for [order-by queries](/docs/query#ordering-by-attributes) - 👁️ View [indexing state](/docs/metadata#responsefield-index) in metadata API - 📚 [Read replicas](/docs/limits) for scalable read throughput (opt-in) - 🔐 Cross-region [PrivateLink connectivity](/docs/security#private-networking) - 🏛️ [FIPS-compliant AWS endpoints](https://aws.amazon.com/compliance/fips/) available for BYOC deployments ### September 2025 - 🧮 [ANN v3](https://x.com/turbopuffer/status/1978173877571441135): query 100B+ vectors with p99 of 200ms [opt-in, beta] - 🚀 [5x object storage throughput](https://x.com/turbopuffer/status/1977751292891234453) for faster cold queries and indexing - 🔍 [Prefix queries](/docs/query#prefix-queries) for full-text search - 💧 [Disable backpressure](/docs/write#param-disable_backpressure) for large scale ingestions - 🔐 Org-level option to [enforce private networking](/docs/private-networking#enforcement) - 💎 Ruby client gem renamed from `turbopuffer-ruby` to [turbopuffer](https://rubygems.org/gems/turbopuffer) - 📝 [2025 SOC 2 Type 2 audit report](/docs/security#soc2) - 🇮🇪 [Ireland region](/docs/regions) ### August 2025 - 🟰 [`Eq` operator](/docs/query#filtering) for array attributes - 🗂️ [Grouped aggregates](/docs/query#group-by) (facets) - 🇰🇷 [South Korea region](/docs/regions) - 🇮🇳 [India region](/docs/regions) - 🔀 [`Any*` filter operators](/docs/query#filtering) for array attributes (e.g. `AnyLt`, `AnyLte`, `AnyGt`, `AnyGte`) ### July 2025 - 🇸🇬 [Singapore region](/docs/regions) - 🇨🇦 [Canada region](/docs/regions) - 🕵️‍♀️ [Private Service Connect + PrivateLink support](/pricing) - 🎈 [`float` type](/docs/write#param-type) - 🕳️ [`exclude_attributes` query parameter](/docs/query#param-exclude_attributes) - 🪢 [`Regex` filter operator](/docs/query#param-Regex) - 📋 [Listing namespaces](/docs/namespaces) is now consistent - 💎 [Ruby API client](https://github.com/turbopuffer/turbopuffer-ruby) GA release ### June 2025 - 👩🏽‍⚖️ [Conditional writes](/docs/write#conditional-writes) - 🔣 [Multi-query API](/docs/query#multi-queries) - 📝 [`Contains` and `ContainsAny` filter operators](/docs/query#param-Contains) - 🐍 [Python async API client](https://github.com/turbopuffer/turbopuffer-python?tab=readme-ov-file#async-usage) - ☕ [Java API client](https://github.com/turbopuffer/turbopuffer-java) GA release (with improved ergonomics) - 🦫 [Go API client](https://github.com/turbopuffer/turbopuffer-go) GA release - 💸 [Discount](/docs/pricing-log) queries on large namespaces (80% discount after 32GB) ### May 2025 - 🐡 [turbopuffer is generally available][ga] - 🎊 [v2 query API](/docs/query) (unifies vector and full-text ranking) - ✌️ `Count` [aggregate function](/docs/query#aggregations) - 🦫 [Go API client][go-client] beta release - ⏩ [Up to 4x faster filtering and full-text search ranking][batched-iterators] [ga]: https://x.com/turbopuffer/status/1922658719231562151 [go-client]: https://github.com/turbopuffer/turbopuffer-go [batched-iterators]: https://x.com/turbopuffer/status/1930274776779530393 ### April 2025 - 🥳 [v2 write API](/docs/write) (includes [patch support](/docs/write#param-patch_columns)) - 💾 [Up to 33% reduction in p90 query latency by using Direct I/O for local SSD cache][nathandirectio] - 🔼 `Max` [operator](/docs/query#fts-operators) for full-text search - 🙅 `Not` [filtering parameter](/docs/query#filtering-parameters) - ☀️ [Warm cache](/docs/warm-cache) endpoint - ☁️ [AWS us-east-2 region](/docs/regions) [nathandirectio]: https://x.com/turbopuffer/status/1919869269623316631 ### March 2025 - ☁️ [Public AWS regions](/docs/regions) - 🐜 [`f16` vector type](/docs/upsert#param-vectors) (50% reduced storage and query cost compared to `f32`) - 🔢 [`i64` type](/docs/write#param-type) (alongside existing `u64`) - ⏰ [`datetime` type](/docs/write#param-type) - 🔤 [Custom tokenizers](/docs/fts#tokenizers) for full-text search - 📝 [`ContainsAllTokens` filter operator](/docs/query#param-ContainsAllTokens) for full-text indexed attributes - 📉 Up to 50% faster vector bulk upserts with client-side [base64-encoding](/docs/upsert#param-vectors) (default in new API clients) ### February 2025 - ❌ [delete_by_filter](/docs/upsert#delete-by-filter) - ⚖️ `Product` operator for weighted/boosted [full-text search queries](/docs/query#full-text-search) - 🌊 [Add or update full-text indexes on existing attributes](/docs/upsert#schema) - 🦾 ARM support on GCP ([increases end-to-end indexing throughput by 70%](https://x.com/turbopuffer/status/1894871601633800276)) - 🤖 [Java API client](https://github.com/turbopuffer/turbopuffer-java) beta release ### January 2025 - 🧮 Type checking for query filters against the namespace [schema](/docs/write#schema) - 📝 Blog post on [Native filtering](/blog/native-filtering) - ⏰ Configurable consistency (strong or eventual) on [queries](/docs/query) (21ms -> 11ms p90 for 1M vectors) - 🔒 Per-namespace Customer-Managed Encryption Key (CMEK) support ### December 2024 - 🔢 [Order by attributes](https://turbopuffer.com/docs/query#ordering-by-attributes) - 🔄 `/v1/vectors` deprecated in favor of `/v1/namespaces` ### November 2024 - ✨ [Support for `Eq null` and `NotEq null` filters](https://turbopuffer.com/docs/query) - 📑 [All filter operators now supported in Filter-Only Search](https://turbopuffer.com/docs/query) - 📉 Faster queries during high write throughput (\<100ms p90 consistent reads during 200+ WPS) - 📉 Faster large namespaces (\<100ms p50 on namespaces with 10M+ documents) - 📉 Faster filters with 10-100k ids (50ms for 100k ids) - 📕 [Rewritten API docs, and new performance guide!](https://turbopuffer.com/docs/performance) ### October 2024 - 📈 Improved write throughput, up to 10x faster in some cases - 📊 Time-series data in dashboard (and faster!) - 📜 Allow [schema changes](/docs/upsert#schema) in upsert ### September 2024 - 🔒 [SOC 2 Type 2](/docs/security) - 📝 Blog post on [Continuous Recall Measurement](/blog/continuous-recall) - 🦣 8 MiB attribute value limit (up from 64 KiB) - 🚁 Add [`copy_from_namespace`](/docs/upsert) to create a namespace by copying another namespace (50% discount relative to upserting from scratch) - 🔳 [Add `uuid` type (55% discount from string) and `bool` type](/docs/write#schema) ### August 2024 - 📑 Support for range operators (Lt, Lte, Gt, Gte) within [Filter-Only Search](/docs/query#lookups) - 📉 [Faster queries with the TypeScript client](https://github.com/turbopuffer/turbopuffer-typescript/pull/26) (Observed 40% faster P99, 25% faster P90) - 📉 2-3x RTT faster queries on high-latency links from TCP tuning (e.g. dev machines, edge devices, AltClouds, co-los) - 📜 [Schema endpoint](https://turbopuffer.com/docs/write#schema) - 🔄 Allow certain schema updates (e.g. marking field as [non-filterable for 50% discount](https://turbopuffer.com/docs/write#passing-a-schema)) - 📉 Dashboard faster for filtering millions of namespaces ### July 2024 - 📉 Up to [10-100x faster document exporting](https://turbopuffer.com/docs/export) - 🏥 HIPAA compliance - 🌐 More [public regions][regions] (us-west, us-east, europe-west) - 📑 Mark attributes as unindexed in the [schema](/docs/upsert#schema) for a 50% discount - 📉 2x faster [P90 for id queries](https://x.com/turbopuffer/status/1814727719281692979) - 📉 2-10x higher maximum write throughput [regions]: /docs/regions ### May 2024 - 🔍 [BM25/Hybrid Search](/docs/hybrid-search) - 📉 [Up to 2x faster queries on large namespaces with lots of attributes](https://x.com/pushrax/status/1799156380059967856) (zero-copy storage) - 📉 Up to 2x faster filtering for large namespaces (faster zero-copy bitmaps) - 💰 [Updated pricing to take attributes into account](/) - 🤖 [Typescript client v0.5 with better connection pooling](https://github.com/turbopuffer/turbopuffer-typescript/pull/12) - 📊 [Dashboard](https://share.cleanshot.com/g6nRqjMx) and [API](/docs/namespaces) support for prefix filtering of namespaces - 📊 [Named API Keys](https://share.cleanshot.com/swgFTcfD) ### April 2024 - 🔒 SOC2 Type 1 certification - 📉 [50% drop in all latency p50, p90, p99][jlilmao] - 🐡 [Media kit][media] [jlilmao]: https://x.com/turbopuffer/status/1781337784977850645 [media]: /press ### March 2024 - 📉 [Faster cache fills and up to ~70% faster cold queries][jlitweet] - 📉 [Faster range queries][bojantweet] - 🔍 [Complex \{And,Or,Range,Intersection\} queries via new query planner][query] - 🔢 [Number and Array attribute types][upsert] - 🌐 [Official TypeScript Client][typescriptlink] - 🔑 [API Key read/write permissions][apipermission] - 📐 [Automatic recall measurement and evaluation][automaticrecall] [bojantweet]: https://x.com/Bojan93112526/status/1773412444355829952 [jlitweet]: https://x.com/pushrax/status/1772374078709530724 [typescriptlink]: https://www.npmjs.com/package/@turbopuffer/turbopuffer [apipermission]: https://x.com/turbopuffer/status/1772717717545426997 [automaticrecall]: https://x.com/turbopuffer/status/1773111405924741194 [query]: /docs/query [upsert]: /docs/upsert ### February 2024 - 🔍 [And and Or filters][orfilter] - 🤖 [Row-based upsert API][rowupsert] - 🤖 [Namespace list][namespacelist] - 📊 [Web dashboard][dash] [orfilter]: https://x.com/Bojan93112526/status/1754952458898383012 [rowupsert]: https://x.com/Bojan93112526/status/1754905892267405464 [namespacelist]: /docs/reference/namespaces [dash]: https://x.com/turbopuffer/status/1756057099698631103 ### January 2024 - 📉 [`<= 1s` P99 cold query performance on 1M vectors][2xcoldperf] - 🤖 String ids - 🔍 Pre-filtering - 🔍 Case insensitive filtering globs [2xcoldperf]: https://x.com/Sirupsen/status/1742617541099573635 ### December 2023 - 🤖 [Python client][pypi] - 🔍 Filter by id - 🌐 Architectural improvements for scalability - 📚 Better docs [pypi]: https://pypi.org/project/turbopuffer/ ### November 2023 - 🤖 String attributes - 🔍 Filters (Glob, Exact) - 🤖 Mutable Namespaces - 🌐 New Website ### October 2023 - 📉 [Improved performance by 30-80%][jli-magic] - 🚀 Launch [jli-magic]: https://x.com/pushrax/status/1719419280788189645 --- This page: [/docs/roadmap.md](https://turbopuffer.com/docs/roadmap.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Security & Compliance ## Hosting All customer data is hosted exclusively in the [region you select](/docs/regions). Customer data inserted into one region remains in that region, except when requested by the customer via the turbopuffer API. Customer data and usage data is always encrypted in transit with TLS1.2+. Customer data is always encrypted at rest with AES-256, and optionally with a [customer's key](#customer-managed-encryption-cmek). ## SOC2 turbopuffer undergoes System and Organization Controls (SOC) 2 Type 2 audits of the design and operational effectiveness of security and availability controls. You can request a copy of the latest SOC 2 report and Penetration Test from our [Trust Center](https://app.drata.com/trust/b4dc7714-f52d-4f50-97e3-ff56a41c2b5c). ## Data Protection (GDPR & CCPA) turbopuffer provides a [Data Processing Addendum (DPA)](/dpa) for all customers to enable compliance with the General Data Protection Regulation (GDPR) and the California Consumer Privacy Act (CCPA). The DPA describes our commitment to policies that comply with the requirements of privacy laws on data processors, such as data deletion, breach notification, and subprocessor management policies. We're happy to assist with any additional questions you may have as part of your privacy compliance processes. ## HIPAA Customers who wish to store protected health information (PHI) in turbopuffer may request a Business Associate Agreement (BAA) with turbopuffer, under which turbopuffer commits to compliance with the requirements of HIPAA on business associates that store and process PHI. [Contact us](/contact) if you require a BAA or have further questions. ## Vulnerability Disclosure See our [Vulnerability Disclosure policy](/docs/vdp). ## Customer managed encryption (CMEK) turbopuffer offers support for [customer managed encryption keys](/docs/encryption) (CMEK), allowing customers on the [Enterprise](/pricing) plan to ensure their data is encrypted using keys from their Key Management System (KMS)/Enterprise Key Manager (EKM). This _also_ allow customer's customers to use their own KMS to encrypt their data, as the [encryption key is defined at the namespace level.](/docs/write) This gives a customer or a customer's customer the same control over their data as they would have if they were to host their own data in their own bucket. [Get started with CMEK.](/docs/encryption) ## Private networking turbopuffer supports private network connections between customer VPCs and our [multi-tenant regions](/docs/regions). This feature is available to customers on the [Enterprise](/pricing) plan. * AWS regions use [AWS PrivateLink](https://aws.amazon.com/privatelink/) * GCP regions use [GCP Private Service Connect](https://cloud.google.com/vpc/docs/private-service-connect) [Get started with private networking.](/docs/private-networking) ## Single Sign-On (SSO) turbopuffer supports Single Sign-On (SSO) for dashboard access on the Scale and Enterprise plans. [Contact us](/contact) to enable SSO for your organization. ## Privileged access management turbopuffer only accesses customer data with the customer's written consent. All access is logged, including the accessor, the resources accessed, and the reason. For single-tenant clusters, infrastructure-level controls prevent any turbopuffer employee from accessing customer data or systems without internal approval. Privileged access management is available as an add-on that gates access behind multi-party approval, enforces time limits on approved sessions, and exposes the audit log to customers. turbopuffer can customize these policies to meet your security requirements. [Contact us](/contact) to enable privileged access management. ## Subprocessors for Customer Data | Subprocessor | Purpose of Processing | Subprocessor Country | Data Hosting Location | | ----------------------------- | --------------------- | -------------------- | ------------------------ | | **Google LLC** (GCP) | Compute and storage | United States | Customer-selected region | | **Amazon Web Services** (AWS) | Compute and storage | United States | Customer-selected region | Subscribe to subprocessor update notifications for when we engage new customer data subprocessors. --- This page: [/docs/security.md](https://turbopuffer.com/docs/security.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Testing **Branch Latency** (constant-time regardless of namespace size) - p50: 440ms - p90: 557ms - p99: 1034ms In your tests and development environment we suggest hitting production turbopuffer for the best end to end testing. Since creating a namespace in turbopuffer is virtually free, you can create a namespace for each test with a random name, and simply delete it after the test. For tests against real data, or to give each developer their own copy, use [branching](/docs/branching) to instantly clone a production namespace. Delete the branch when done — the source namespace is unaffected. To separate test and production infrastructure, consider creating a separate organization in the dashboard. For cross-region or cross-org copies, use [`copy_from_namespace`](/docs/backups). ```python # tpuf_test.py # Run with `pytest tpuf_test.py`. import pytest import string import random import turbopuffer from turbopuffer.lib import namespace tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) # Create a namespace for each test, and always delete it afterwards @pytest.fixture def tpuf_ns(): random_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) ns_name = f"test-{random_suffix}" ns = tpuf.namespace(ns_name) try: yield ns finally: try: ns.delete_all() except turbopuffer.NotFoundError: # If the namespace never got created, no cleanup is needed. pass def test_query(tpuf_ns: namespace.Namespace): tpuf_ns.write( upsert_rows=[ {"id": 1, "vector": [1, 1]}, {"id": 2, "vector": [2, 2]} ], distance_metric="cosine_distance", ) res = tpuf_ns.query(rank_by=("vector", "ANN", [1.1, 1.1]), limit=10) assert res.rows[0].id == 1 ``` ```typescript // tpuf.test.ts // Run with `vitest ./tpuf.test.ts`. import { expect, test, beforeEach, afterEach, describe } from 'vitest' import { NotFoundError, Turbopuffer } from "@turbopuffer/turbopuffer"; import { Namespace } from "@turbopuffer/turbopuffer/resources"; import * as crypto from "crypto"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); describe("Turbopuffer namespace tests", () => { let ns: Namespace; beforeEach(async () => { const randomSuffix = crypto.randomBytes(16).toString("hex"); const nsName = `test-${randomSuffix}`; ns = tpuf.namespace(nsName); }); afterEach(async () => { try { await ns.deleteAll(); } catch (e: any) { if (!(e instanceof NotFoundError)) { // If the namespace never got created, no cleanup is needed. throw e; } } }); test("query test", async () => { await ns.write({ upsert_rows: [ { id: 1, vector: [1, 1] }, { id: 2, vector: [2, 2] }, ], distance_metric: "euclidean_squared", }); const res = await ns.query({ rank_by: ["vector", "ANN", [1.1, 1.1]], limit: 10, }); expect(res.rows![0].id).toBe(1); }); }); ``` ```go // tpuf_test.go // Run with `go test ./yourpkg`. package yourpkg_tests import ( "context" "errors" "fmt" "math/rand" "testing" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" "github.com/turbopuffer/turbopuffer-go/v2/packages/respjson" ) // Helper function that invokes a test function f with a turbopuffer namespace // and ensures the namespace is deleted after f returns (even if it fails). func runTurbopufferTest(t *testing.T, f func(ctx context.Context, ns turbopuffer.Namespace)) { ctx := context.Background() client := turbopuffer.NewClient(option.WithRegion("gcp-us-central1")) // choose best region: https://turbopuffer.com/docs/regions // Generate a random name for the test namespace. name := fmt.Sprintf("test-%016x%016x", rand.Uint64(), rand.Uint64()) namespace := client.Namespace(name) // Delete the namespace after the test, even if the test fails. defer func() { _, err := namespace.DeleteAll(ctx, turbopuffer.NamespaceDeleteAllParams{}) if err != nil { var apiError *turbopuffer.Error if errors.As(err, &apiError) && apiError.StatusCode == 404 { // Ignore errors due to the namespace being deleted. The test // might have failed before creating it. return } t.Fatalf("unable to delete test namespace: %s: %v", name, err) } }() f(ctx, namespace) } func TestQuery(t *testing.T) { runTurbopufferTest(t, func(ctx context.Context, ns turbopuffer.Namespace) { _, err := ns.Write(ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": []float32{1.1, 1.1}, }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, }) if err != nil { t.Fatal(err) } res, err := ns.Query(ctx, turbopuffer.NamespaceQueryParams{ RankBy: turbopuffer.NewRankByAnn("vector", []float32{1.1, 1.1}), Limit: turbopuffer.LimitParam{ Total: 10, }, }) if err != nil { t.Fatal(err) } if res.Rows[0]["id"] != respjson.Number("1") { t.Fatal("wrong row returned") } }) } func TestOther(t *testing.T) { runTurbopufferTest(t, func(ctx context.Context, ns turbopuffer.Namespace) { // Your test code here. }) } ``` ```java // TpufTest.java package com.turbopuffer.docs; import static org.junit.jupiter.api.Assertions.*; import com.turbopuffer.client.*; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; import org.junit.jupiter.api.*; public class TpufTest { Namespace ns; @BeforeEach public void setUp() { var rand = new Random(); var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); ns = tpuf.namespace(String.format("test-%016x%016x", rand.nextLong(), rand.nextLong())); } @AfterEach public void tearDown() { ns.deleteAll(); } @Test public void testQuery() { ns.write( NamespaceWriteParams.builder() .addUpsertRow(Row.builder().put("id", 1).put("vector", List.of(1, 1)).build()) .addUpsertRow(Row.builder().put("id", 2).put("vector", List.of(2, 2)).build()) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .build() ); var result = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.ann("vector", List.of(1.0f, 1.0f))) .limit(10) .build() ); var rows = result.rows().get(); assertEquals(2, rows.size()); assertEquals(1, rows.get(0).get("id").asNumber().get()); } } ``` ```cs // TpufTest.cs // dotnet add package Turbopuffer // dotnet add package xunit // // Run with `dotnet test`. using System; using System.Threading.Tasks; using Turbopuffer; using Turbopuffer.Exceptions; using Turbopuffer.Models.Namespaces; using Turbopuffer.Services; using Xunit; public class TpufTest : IAsyncLifetime { private readonly TurbopufferClient _tpuf = new() { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; private INamespaceService _ns = null!; // Create a namespace for each test, and always delete it afterwards. public Task InitializeAsync() { _ns = _tpuf.Namespace($"test-{Guid.NewGuid():N}"); return Task.CompletedTask; } public async Task DisposeAsync() { try { await _ns.DeleteAll(new NamespaceDeleteAllParams()); } catch (TurbopufferNotFoundException) { // If the namespace never got created, no cleanup is needed. } _tpuf.Dispose(); } [Fact] public async Task TestQuery() { await _ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row().Set("id", 1).Set("vector", new[] { 1.0f, 1.0f }), new Row().Set("id", 2).Set("vector", new[] { 2.0f, 2.0f }), ], DistanceMetric = DistanceMetric.CosineDistance, } ); var result = await _ns.Query( new NamespaceQueryParams { RankBy = RankBy.Ann("vector", new[] { 1.1f, 1.1f }), Limit = 10, } ); var rows = result.GetRows(); Assert.Equal(2, rows.Count); Assert.Equal(1, rows[0].Get("id")); } } ``` ```ruby # tpuf_test.rb # Run with `ruby tpuf_test.rb`. require "minitest/autorun" require "securerandom" require "turbopuffer" class TestTpuf < Minitest::Test # Create a namespace for each test def setup tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) @ns = tpuf.namespace("test-#{SecureRandom.alphanumeric(32)}") end # Always delete the namespace after the test def teardown @ns.delete_all rescue Turbopuffer::Errors::NotFoundError # If the namespace never got created, no cleanup is needed. end def test_query @ns.write( upsert_rows: [ { id: 1, vector: [1, 1] }, { id: 2, vector: [2, 2] }, ], distance_metric: Turbopuffer::DistanceMetric::COSINE_DISTANCE, ) res = @ns.query(rank_by: ["vector", "ANN", [1.1, 1.1]], limit: 10) assert_equal(res.rows.first.id, 1) end end ``` --- This page: [/docs/testing.md](https://turbopuffer.com/docs/testing.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Tradeoffs Every technology has tradeoffs. This document outlines turbopuffer's key design choices to help inform your evaluation. ## High Latency, High Throughput Writes turbopuffer prioritizes simplicity, durability, and scalability by using object storage as a write-ahead log, keeping nodes stateless. While this means writes take up to 200ms to commit, the system supports thousands of writes per second per namespace. Despite this latency, our consistent read model makes documents visible to queries faster than eventually consistent search engines. This architecture choice enables our cost-effective scaling and is particularly well-suited for search workloads. ## Focused on First-Stage Retrieval turbopuffer focuses on efficient first-stage retrieval, providing a simple API to filter millions of documents down to a smaller candidate set. In a 2nd stage, you can then refine and rerank results using familiar programming languages, making your search logic easier to develop and maintain. Learn more about this approach in our [Hybrid Search](/docs/hybrid-search) guide. We've found that it's difficult to maintain search applications in mountains of idiosyncratic query language. ## Optimized for Accuracy turbopuffer delivers high recall out of the box, maintaining this quality even with complex filters. We prioritize consistent, accurate results over configurable performance optimizations. ## Consistent Reads Have a ~10ms Latency Floor turbopuffer's reads are consistent by default, requiring object storage checks for the latest writes. This baseline latency aligns with object storage's `GET IF-NOT-MATCH` latency and should improve as object storage technology advances. For workloads requiring sub-10ms latency, you can [enable eventual consistency](/docs/query). S3's metadata p50=10ms p90=17ms, GCS's metadata p50=12-18ms p90=15-25ms (more region-dependent). ## Occasional Cold Queries Since all data is not always in memory or disk, turbopuffer will occasionally do cold queries directly on object storage and rehydrate the cache. This means that e.g. P999 queries may be in the 100s of milliseconds range (see cold/hot [performance](/) on the landing page). Our storage layer is optimized for this use-case, and does direct ranged reads on object storage in the fewest round-trips possible for the fastest cold queries. Many applications can [prewarm namespaces](/docs/warm-cache) so users never observe cold latency. ## Scales to Millions of Namespaces turbopuffer scales to trillions of documents across hundreds of millions of namespaces. While you can create unlimited namespaces, individual namespaces have ever-expanding [size guidelines](/docs/limits). Namespacing your data means benefiting natural data partitioning (e.g. tenancy) for performance and cost. ## Focused on Paid Customers For the current phase of our company we have chosen a commercial-only model to maintain high-quality support and rapid development. While we don't offer a free tier or open source version, you can run turbopuffer in your own cloud--[contact us](/contact/sales) for details. ## Comparison | turbopuffer excels at | turbopuffer may not currently be the best fit for | |---------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------| | Large scale (100B+ documents/vectors) with lots of namespaces (tens of millions) | Low scale, free tier | | Naturally sharded data (e.g. B2B where each tenant's data is isolated in its own namespace) | Extensive 1st-stage ranking (we encourage generating a candidate set with hybrid search and refining/re-ranking further in your own 2nd stage) | | Cost-effectiveness | Built-in 2nd-stage re-ranking (we encourage you to do it in `{search.py,search.ts,..}`) | | Fast cold starts | Built-in embedding (this is a few lines of code at most) | | Reliability | Open Source | | Hybrid search (BM25 + vector search) | | | Support from DB Engineers | | | Deploy into your VPC (BYOC) | | | Heavy writes (Appends, Updates and Deletes) | | For more details, see [Guarantees](/docs/guarantees), [Limits](/docs/limits), and [Architecture](/docs/architecture) pages. --- This page: [/docs/tradeoffs.md](https://turbopuffer.com/docs/tradeoffs.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Vulnerability Disclosure ## In Scope turbopuffer is seeking vulnerability reports for: - Dashboard: the website hosted at https://turbopuffer.com/dashboard, including the authentication process and the process of managing API keys. - Database: the turbopuffer database API. - Client SDKs: the turbopuffer client libraries, which can be found on [our GitHub](https://github.com/orgs/turbopuffer/repositories). Our focus is on unauthorized access to user data. ## Out of Scope The following issues are considered out of scope: - Clickjacking on pages with no sensitive actions. - CSRF on forms that are available to anonymous users or forms with no sensitive actions. - Flags on cookies that are not sensitive. - TLS, DNS, and security header configuration suggestions on the marketing website. - Any activity that could lead to denial of service (DoS) by sending a flood of requests. ## How to Report If you believe you have found a vulnerability, please submit your findings to [security@turbopuffer.com](mailto:security@turbopuffer.com). To expedite triage and resolution, please include: - A detailed description of the vulnerability. - How you found the vulnerability, including any relevant software you used. - Steps to reproduce the vulnerability, or a working proof-of-concept. If your report is clear and in scope, you can expect a timely response. We will update you when the vulnerability has been validated, when more information is needed from you, or when you have qualified for a bounty. We do not yet have a standardized framework for determining monetary rewards, and are currently assessing rewards on a case-by-case basis. ## Program Policy To promote the security of our platform, we ask that you: - Allow us reasonable time to respond to the report before disclosing any information about it publicly, and collaborate with us to make reports public. - Do not access or modify our data or our users' data, unless you have explicit permission of the owner. Only interact with your own accounts for security research purposes. - If you do inadvertently encounter user data, contact us immediately. Do not view, alter, save, store, transfer, or otherwise access the data, and immediately purge the data from your machine. - Act in good faith to avoid violating privacy, destroying data, or otherwise disrupting our services. - Do not attempt any form of social engineering (e.g. phishing, smishing). - Comply with all applicable laws. ## Safe Harbor Activities conducted in a manner consistent with this policy will be considered authorized conduct and we will not initiate legal action against you. If legal action is initiated by a third party against you in connection with activities conducted under this policy, we will take steps to make it known that your actions were conducted in compliance with this policy. --- This page: [/docs/vdp.md](https://turbopuffer.com/docs/vdp.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Vector Search Guide **Vector Query** (768 dimensions, f16, 10M docs, ~15GB. Strongly consistent.) - warm (10M docs): p50=14ms, p90=17ms, p99=27ms - cold (10M docs): p50=874ms, p90=1214ms, p99=1686ms turbopuffer supports vector search with [filtering](/docs/query#filtering). Vectors are incrementally indexed in an SPFresh vector index for performant search. Writes appear in search results immediately. The vector index is automatically tuned for 90-100% recall ("accuracy"). We automatically [monitor recall](/blog/continuous-recall) for production queries. You can use the [recall endpoint](/docs/recall) to test yourself. Choose an embedding provider. Pick from the dropdown in the code sample below, or use random vectors to start (don't use in production or for benchmarking). ```python # $ pip install turbopuffer sentence-transformers import os import uuid from typing import List import turbopuffer from sentence_transformers import SentenceTransformer tpuf = turbopuffer.Turbopuffer( api_key=os.getenv("TURBOPUFFER_API_KEY"), # created here: https://turbopuffer.com/dashboard region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) namespace = os.getenv("TURBOPUFFER_NAMESPACE", f"vector-search-{uuid.uuid4().hex[:8]}") ns = tpuf.namespace(namespace) # Local embeddings with BGE -- no API key needed. # Model is downloaded on first run (~130 MB). bge = SentenceTransformer("BAAI/bge-small-en-v1.5") def embed(text: str) -> List[float]: return bge.encode(text).tolist() # Upsert documents with vectors and attributes ns.write( upsert_rows=[ { "id": 1, "vector": embed("A cat sleeping on a windowsill"), "text": "A cat sleeping on a windowsill", "category": "animal", }, { "id": 2, "vector": embed("A playful kitten chasing a toy"), "text": "A playful kitten chasing a toy", "category": "animal", }, { "id": 3, "vector": embed("An airplane flying through clouds"), "text": "An airplane flying through clouds", "category": "vehicle", }, { "id": 4, "vector": embed("A shiny red sports car"), "description": "A shiny red sports car", "color": "red", "type": "car", "price": 50000, }, { "id": 5, "vector": embed("A sleek blue sedan"), "description": "A sleek blue sedan", "color": "blue", "type": "car", "price": 35000, }, { "id": 6, "vector": embed("A large red delivery truck"), "description": "A large red delivery truck", "color": "red", "type": "truck", "price": 80000, }, { "id": 7, "vector": embed("A blue pickup truck"), "description": "A blue pickup truck", "color": "blue", "type": "truck", "price": 45000, }, ], distance_metric="cosine_distance", ) # Basic vector search result = ns.query( rank_by=("vector", "ANN", embed("feline")), limit=2, include_attributes=["text"], ) print(result.rows) # Vector search with filters result = ns.query( rank_by=("vector", "ANN", embed("car")), limit=10, filters=("And", (("price", "Lt", 60000), ("color", "Eq", "blue"))), include_attributes=["description", "price"], ) print(result.rows) ``` --- This page: [/docs/vector.md](https://turbopuffer.com/docs/vector.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Warm cache GET /v1/namespaces/:namespace/hint_cache_warm **Hint Cache Warm** (Latency of the hint_cache_warm endpoint when warming is already in progress.) - Hint Latency Only (Warming in Background): p50=1ms, p90=1ms, p99=2ms Signal turbopuffer to prepare for low-latency requests. Hints turbopuffer that the client will send latency-sensitive requests in the near future, so that turbopuffer can get ready to serve those requests with low (warm) latency. turbopuffer responds by acknowledging the request. ## Billing If turbopuffer is ready to serve requests with low latency, or it is already getting the namespace ready for low-latency queries, this request is free. Otherwise, this request is billed as a query that returns zero rows. ## Use cases A common use case is for applications to send hints for all namespaces associated with a user whenever a user begins a new session, so that users don't experience cold latency when they trigger their first turbopuffer query. ## Examples ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v1/namespaces/warm-cache-example-curl/hint_cache_warm \ -X GET --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" # Response payload # { "status": "ACCEPTED", "message": "cache warm hint accepted" } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'warm-cache-example-py') print(ns.hint_cache_warm()) # NamespaceHintCacheWarmResponse(status='ACCEPTED', message='cache warm hint accepted') ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`warm-cache-example-ts`); console.log(await ns.hintCacheWarm()); // { "status": "ACCEPTED", "message": "cache warm hint accepted" } ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("warm-cache-example-go") result, err := ns.HintCacheWarm(ctx, turbopuffer.NamespaceHintCacheWarmParams{}) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(result)) // {"status": "ACCEPTED", "message": "cache warm hint accepted"} } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class WarmCache { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("warm-cache-example-java"); System.out.println(ns.hintCacheWarm()); // NamespaceHintCacheWarmResponse{status=ACCEPTED, message=cache warm hint accepted} } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("warm-cache-example-csharp"); Console.WriteLine(await ns.HintCacheWarm(new NamespaceHintCacheWarmParams())); // {"status": "ACCEPTED", "message": "cache warm hint accepted"} ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("warm-cache-example-rb") puts ns.hint_cache_warm # {status: :ACCEPTED, message: "cache warm hint accepted"} ``` --- This page: [/docs/warm-cache.md](https://turbopuffer.com/docs/warm-cache.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt) # Write documents POST /v2/namespaces/:namespace Creates, updates, or deletes documents. **Upsert** (Time for the batch to be durably acknowledged by object storage. Documents are immediately available to consistent reads after this.) - Upsert latency (512kb docs): p50=165ms, p90=248ms, p99=850ms A `:namespace` is an isolated set of documents and is implicitly created when the first document is inserted. Namespace names must match `[A-Za-z0-9-_.]{1,128}`. We recommend creating a namespace per isolated document space instead of filtering when possible. Large batches of writes are highly encouraged to maximize throughput and minimize cost. Write requests can have a payload size of up to 512 MB. See [Performance](/docs/performance). Within a namespace, documents are uniquely referred to by their ID. Document IDs are unsigned 64-bit integers, 128-bit UUIDs, or strings up to 64 bytes. [arch]: /architecture turbopuffer supports the following types of writes: - [Upserts](#param-upsert_rows): creates or overwrites an entire document. - [Patches](#param-patch_rows): updates one or more attributes of an existing document. - [Deletes](#param-deletes): deletes an entire document by ID. - [Conditional writes](#param-upsert_condition): upsert, patch, or delete a document only if a condition. - [Patch by filter](#param-patch_by_filter): patches documents that match a filter. - [Delete by filter](#param-delete_by_filter): deletes documents that match a filter. - [Copy from namespace](#param-copy_from_namespace): copies all documents from another namespace. - [Branch from namespace](#param-branch_from_namespace): instantly creates a copy-on-write clone of a namespace. ## Request **upsert_rows** array Upserts documents in a row-based format. Each row is an object with an `id` document ID, and any number of other [attribute](#attributes) fields. Existing documents with matching IDs are overwritten entirely. Use [patch_rows](#param-patch_rows) to update only specific attributes. A namespace may or may not have vector indexes. If it does, all documents must include all vector attributes. Example: ```json [ { "id": 1, "vector": [1, 2, 3], "name": "foo" }, { "id": 2, "vector": [4, 5, 6], "name": "bar" } ] ``` --- **upsert_columns** object Upserts documents in a column-based format. This field is an object, where each key is the name of a column, and each value is an array of values for that column. Existing documents with matching IDs are overwritten entirely. Use [patch_columns](#param-patch_columns) to update only specific attributes. The `id` key is required, and must contain an array of document IDs. All vector attribute columns are required if the namespace has vector indexes. Other keys will be stored as [attributes](#attributes). Each column must be the same length. When a document doesn't have a value for a given column, pass `null`. Example: ```json { "id": [1, 2], "vector": [[1, 2, 3], [4, 5, 6]], "name": ["foo", "bar"] } ``` --- **patch_rows** array Patches documents in a row-based format. Identical to [`upsert_rows`](#param-upsert_rows), but instead of overwriting entire documents, only the specified keys are written. Vector attributes currently cannot be patched. You currently need to retrieve and upsert the entire document. Any patches to IDs that don't already exist in the namespace will be ignored; patches will not create any missing documents. Example: ```json [ { "id": 1, "name": "baz" }, { "id": 2, "name": "qux" } ] ``` Patches are billed for the size of the patched attributes (not the full written documents), plus the cost of one query per write request (to read all the patched documents touched by the request). --- **patch_columns** object Patches documents in a column-based format. Identical to [`upsert_columns`](#param-upsert_columns), but instead of overwriting entire documents, only the specified keys are written. Vector attributes currently cannot be patched. You currently need to retrieve and upsert the entire document. Any patches to IDs that don't already exist in the namespace will be ignored; patches will not create any missing documents. Example: ```json { "id": [1, 2], "name": ["baz", "qux"] } ``` --- **deletes** array Deletes documents by ID. Must be an array of document IDs. Example: ```json [ 1, 2, 3 ] ``` --- **upsert_condition** object Makes each write in [`upsert_rows`](#param-upsert_rows) and [`upsert_columns`](#param-upsert_columns) [conditional](#conditional-writes) on the `upsert_condition` being satisfied for the document with the corresponding ID. The `upsert_condition` is evaluated before each write, using the current value of the document with the matching ID. * If the document exists and the condition is met, the write is applied (i.e. the document is updated). * If the document exists and the condition is not met, the write is skipped. * If the document does not exist, the write is applied unconditionally (i.e. the document is created). The condition syntax matches the [`filters` parameter in the query API](query#filtering), with an additional feature: you can reference the new value being written using `$ref_new` references. These look like `{"$ref_new": "attr_123"}` and can be used in place of value literals. Example (newer timestamp): ```json [ "Or", [ [ "updated_at", "Lt", { "$ref_new": "updated_at" } ], ["updated_at", "Eq", null] ] ] ``` Example (insert if not exists): ```json [ "id", "Eq", null ] ``` The `newer timestamp` example ensures that each upsert is only processed if the new document value has a newer `updated_at` timestamp than its current version. The `insert if not exists` example ensures that each upsert only inserts new documents, skipping any writes where a document with that ID already exists. Since existing documents always have a non-null `id`, this condition fails for them, while new documents are inserted unconditionally. --- **patch_condition** object Like `upsert_condition`, but for [`patch_rows`](#param-patch_rows) and [`patch_columns`](#param-patch_columns). Any patches to IDs that don't already exist in the namespace will be ignored without evaluating the condition; patches will not create any missing documents. Does not apply to `patch_by_filter`. Prefer this over `patch_by_filter` when the set of IDs to conditionally patch is known ahead of time. --- **delete_condition** object Like `upsert_condition`, but for [`deletes`](#param-deletes). `$ref_new` references are given a `null` value for all attributes. Does not apply to `delete_by_filter`. Prefer this over `delete_by_filter` when the set of IDs to conditionally delete is known ahead of time. --- **patch_by_filter** object You can patch documents that match a filter using [`patch_by_filter`](#patch-by-filter). It accepts an object with two fields: - `filters`: a filter expression (see [query filtering](query#filtering)) - `patch`: an object containing the the patch to apply to all documents matching the filter If `patch_by_filter` is used in the same request as other write operations, it is applied after `delete_by_filter` but before any other write operations. Vector attributes currently cannot be patched. You currently need to retrieve and upsert the entire document. Example: ```json { "filters": [ "page_id", "Eq", 123 ], "patch": { "page_id": 124 } } ``` `patch_by_filter` is billed as a write and two queries (one for the filter, one for the patch). --- **delete_by_filter** object You can delete documents that match a filter using [`delete_by_filter`](#delete-by-filter). It has the same syntax as the [`filters` parameter in the query API](query#filtering). If `delete_by_filter` is used in the same request as other write operations, `delete_by_filter` will be applied before the other operations. This allows you to delete rows that match a filter before writing new row with overlapping IDs. Note that patches to any deleted rows are ignored. `delete_by_filter` is different from `deletes` with a `delete_condition`: * `delete_by_filter`: searches across the namespace for any matching document IDs, deleting all matches that it finds. * `delete` + `delete_condition`: only evaluates the condition on the IDs identified in `deletes`. `delete_condition` does not apply to `delete_by_filter`. Example: ```json [ "page_id", "Eq", 123 ] ``` `delete_by_filter` is billed the same as normal deletes, plus the cost of one query per write request (to determine which IDs to delete). --- **patch_by_filter_allow_partial** boolean default: false Allows `patch_by_filter` operations to succeed when the filter matches more than the [maximum allowed](/docs/limits) number of documents. When set to `true`, a `patch_by_filter` will update up to the maximum allowed number of documents, and set `rows_remaining` to `true` if any additional documents could match this filter. You should issue another potentially duplicate request to update additional matching documents. When set to `false`, a `patch_by_filter` which matches more than the maximum allowed number of documents will *fail* and update no documents. --- **delete_by_filter_allow_partial** boolean default: false Allows `delete_by_filter` operations to succeed when the filter matches more than the [maximum allowed](/docs/limits) number of documents. When set to `true`, a `delete_by_filter` will delete up to the maximum allowed number of documents, and set `rows_remaining` to `true` if any additional documents could match this filter. You should issue another potentially duplicate request to delete additional matching documents. When set to `false`, a `delete_by_filter` which matches more than the maximum allowed number of documents will *fail* and update no documents. --- **return_affected_ids** boolean default: false If `true`, the response will include `upserted_ids`, `patched_ids`, and `deleted_ids` arrays containing the IDs of documents that were successfully written. For conditional writes and filter-based operations, only IDs for writes that succeeded will be included. --- **distance_metric** cosine_distance | euclidean_squared required unless copy_from_namespace or branch_from_namespace is set or the namespace has no vector columns The function used to calculate vector similarity. Possible values are `cosine_distance` or `euclidean_squared`. `cosine_distance` is defined as `1 - cosine_similarity` and ranges from 0 to 2. Lower is better. `euclidean_squared` is defined as `sum((x - y)^2)`. Lower is better. **NOTE:** This distance metric will apply to all vector columns configured for this namespace. --- **copy_from_namespace** string | object Copy all documents from another namespace into this one. The destination namespace you are copying into must be empty. The initial request currently cannot make schema changes or contain documents. Copying is billed at up to a 75% write discount (a 50% copy discount that stacks with the up to 50% discount for batched writes). This is a faster, cheaper alternative to re-upserting documents for backups and namespaces that share documents. See the [cross-region backups guide](/docs/backups) for an example. For same-region use cases, consider [`branch_from_namespace`](/docs/branching) which completes instantly regardless of namespace size. For copies from another region, the logical size copied is also billed as returned bytes. Same-region copies do not bill returned bytes. To copy a namespace from a different organization, region, or cloud provider, instead of providing the namespace as a string, provide an object with the following fields: - `source_namespace` (string): the namespace to copy from - `source_api_key` (string, optional): an API key for the organization containing the source namespace. Omit to copy from the same organization as the target namespace. - `source_region` (string, optional): the [region](/docs/regions) of the source namespace (e.g. `"aws-us-east-1"`). Omit to copy from the same region as the target namespace. Source and destination can be in different cloud providers (e.g. `aws-us-east-1` → `gcp-us-central1`). By default, the destination namespace will inherit the source namespace's encryption configuration. You can optionally specify a different [encryption](#param-encryption) for the destination namespace. This allows you to copy from a namespace with default encryption to a namespace with customer-managed encryption, or vice-versa, or to use a different CMEK key than the source. For cross-region copies from a namespace with customer-managed encryption, you must explicitly specify a destination encryption key available in the destination region. Example (basic copy): ```json "source-namespace" ``` Example (cross-region, cross-org copy): ```json { "source_namespace": "source-namespace", "source_api_key": "tpuf_A1...", "source_region": "aws-us-east-1" } ``` Copies of large namespaces can run asynchronously. See [Asynchronous requests](/docs/overview#asynchronous-requests). --- **branch_from_namespace** string Creates an instant copy-on-write clone of the source namespace. The destination namespace must be empty. After branching, both namespaces are fully independent — reads, writes, queries, and deletes on one namespace do not affect the other. Branching is billed at a flat rate of $0.032. See the [branching guide](/docs/branching) for details, examples, and guidance on when to use branching vs `copy_from_namespace`. **Example:** `"source-namespace"` --- **schema** object By default, the schema is inferred from the passed data. See [Schema](#schema) below. There are cases where you want to manually specify the schema because turbopuffer can't automatically infer it. For example, to specify UUID types, configure full-text search for an attribute, or disable filtering for an attribute. Example: ```json { "permissions": "[]uuid", "text": { "type": "string", "full_text_search": true }, "encrypted_blob": { "type": "string", "filterable": false } } ``` --- **encryption** object optional Only available as part of our scale and enterprise [plans](/pricing). Setting a [Customer Managed Encryption Key (CMEK)](/docs/encryption) will encrypt all data in a namespace using a secret coming from your cloud KMS. Once set, all subsequent writes to this namespace will be encrypted, but data written prior to this upsert will be unaffected. Currently, turbopuffer does not re-encrypt data when you rotate key versions, meaning old data will remain encrypted using older key verisons, while fresh writes will be encrypted using the latest versions. **Revoking old key versions will cause data loss.** To re-encrypt your data using a more recent key, use the [export](/docs/export) API to re-upsert into a new namespace, or use [`copy_from_namespace`](#param-copy_from_namespace) with a different `encryption` key to copy to a newly encrypted namespace. Example (GCP): ```json { "mode": "customer-managed", "key_name": "projects/myproject/locations/us-central1/keyRings/EXAMPLE/cryptoKeys/KEYNAME" } ``` Example (AWS): ```json { "mode": "customer-managed", "key_name": "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" } ``` --- **disable_backpressure** boolean default: false Disables HTTP 429 backpressure on writes when unindexed data exceeds 2 GiB. Useful for initial data loading or bulk updates. When disabled, strongly consistent queries return errors above this threshold, so use [eventual consistency](/docs/query#param-consistency) instead. Eventually consistent queries search only the first 128 MiB of unindexed data. Only takes effect for upserts and delete-by-id. Ignored for patch-by-id, patch-by-filter, delete-by-filter, and [conditional writes](#conditional-writes), since those operations require a strongly consistent read of existing rows. Indexing progress can be tracked through the `unindexed_bytes` field in the [metadata endpoint](/docs/metadata#responsefield-index). Note that while data is being indexed, the following will not be updated: - [`approx_row_count`](/docs/metadata#responsefield-approx_row_count) and [`approx_logical_bytes`](/docs/metadata#responsefield-approx_logical_bytes) in the metadata endpoint - Namespace row counts and sizes in the dashboard ## Response **rows_affected** number The total number of rows affected by the write request (sum of upserted, patched, and deleted rows). **rows_upserted** number The number of rows upserted by the write request. Only present when [upsert_rows](#param-upsert_rows) or [upsert_columns](#param-upsert_columns) is used. **rows_patched** number The number of rows patched by the write request. Only present when [patch_rows](#param-patch_rows) or [patch_columns](#param-patch_columns) or [patch_by_filter](#param-patch_by_filter) is used. When using [`patch_condition`](#param-patch_condition), this reflects only the rows where the condition was met and the patch was applied. Other patches were skipped. **rows_deleted** number The number of rows deleted by the write request. Only present when [deletes](#param-deletes) or [delete_by_filter](#param-delete_by_filter) is used. When using [`delete_condition`](#param-delete_condition), this reflects only the rows where the condition was met and the deletion occurred. Other deletes were skipped. **rows_remaining** boolean Filter-based writes like `delete_by_filter` and `patch_by_filter` have a maximum number of documents modified per write request. This ensures indexing and consistent reads can keep up with writes & deletes. If this response field is set to `true` there are more documents that match the `delete_by_filter` or `patch_by_filter`. You should issue another potentially duplicate request to update additional matching documents. The [limits](/docs/limits) are currently: - 5M documents for `delete_by_filter` - 50k documents for `patch_by_filter` **upserted_ids** array The IDs of documents that were upserted. Only present when `return_affected_ids` is `true` and at least one document was upserted. **patched_ids** array The IDs of documents that were patched. Only present when `return_affected_ids` is `true` and at least one document was patched. **deleted_ids** array The IDs of documents that were deleted. Only present when `return_affected_ids` is `true` and at least one document was deleted. **billing** object The billable resources consumed by the write. The object contains the following fields: * `billable_logical_bytes_written` (uint): the number of logical bytes written to the namespace * `query` (object, optional): query billing information when the write involves a query-like operation (for a conditional write, `patch_by_filter`, `delete_by_filter`, or a cross-region `copy_from_namespace`): * `billable_logical_bytes_queried` (uint): the number of logical bytes processed by queries * `billable_logical_bytes_returned` (uint): the number of logical bytes returned by queries **performance** object The performance metrics for the write. The object currently contains the following fields, but these fields may change name, type, or meaning in the future: * `server_total_ms` (uint): request time measured on the server, in milliseconds ## Attributes Documents are composed of attributes. All documents must have a unique `id` attribute. Attribute names can be up to 128 characters in length and must not start with a `$` character. By default, attributes are indexed and thus queries can [filter](/docs/query#filtering) or [sort](/docs/query#ordering-by-attributes) by them. To disable indexing for an attribute, set `filterable` to `false` in the [schema](/docs/write#param-filterable) for a 50% discount and improved indexing performance. The attribute can still be returned, but not used for filtering or sorting. Attributes must have consistent value types, and are nullable. The type is inferred from the first occurrence of the attribute. Certain non-inferrable types, e.g. `uuid` or `datetime`, must be specified in the [schema](/docs/write#schema). Some limits apply to attribute sizes and number of attribute names per namespace. See [Limits](/docs/limits). ### Vectors Vectors are attributes with a vector type (`[N]f32`, `[N]f16`, or `[N]i8` where N is the number of dimensions), encoded as either a JSON array of numbers, or as a base64-encoded string. Attributes named `vector` will automatically be inferred as having vector types, additional vector columns must be explicitly declared in the [schema](/docs/write#schema). If using the base64 encoding, the vector must be serialized in little-endian float32 binary format, then base64-encoded. The base64 string encoding can be more efficient on both the client and server. Elements of a vector attribute must have the same number of dimensions. A namespace can currently be created with up to vector columns. The number of vector columns cannot be changed after namespace creation. To use `f16` or `i8` vectors within the database, the relevant vector attribute must be [explicitly specified in the schema](/docs/write#param-type) with an `f16` or `i8` type (e.g. `[512]f16` or `[512]i8`) when first creating the namespace. This does not affect the base64 vector encoding in the API, which always uses a little-endian float32 binary format, regardless of the schema's element type. Vector attributes require an ANN index, configured via the [`ann` schema parameter](/docs/write#param-ann). #### Multiple vector columns A namespace can have multiple vector columns, each with independent dimensions and types. Vector columns must be declared in the schema and are fixed at namespace creation time. **Pricing:** Each vector column has its own ANN index. Filterable attributes are indexed per ANN index, so their write and storage costs scale with the number of vector columns. Non-filterable attributes are stored once regardless of the number of vector columns. See [attribute billing](/pricing#faq-attribute-billing) for details. ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-multivec-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ {"id": 1, "title_embedding": [0.1, 0.2, 0.3], "image_embedding": [0.4, 0.5], "title": "hello world"}, {"id": 2, "title_embedding": [0.4, 0.5, 0.6], "image_embedding": [0.7, 0.8], "title": "goodbye world"} ], "distance_metric": "cosine_distance", "schema": { "title_embedding": {"type": "[3]f32", "ann": true}, "image_embedding": {"type": "[2]f16", "ann": true} } }' ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'write-multivec-example-py') ns.write( upsert_rows=[ { 'id': 1, 'title_embedding': [0.1, 0.2, 0.3], 'image_embedding': [0.4, 0.5], 'title': 'hello world', }, { 'id': 2, 'title_embedding': [0.4, 0.5, 0.6], 'image_embedding': [0.7, 0.8], 'title': 'goodbye world', }, ], distance_metric='cosine_distance', schema={ 'title_embedding': { 'type': '[3]f32', 'ann': True, }, 'image_embedding': { 'type': '[2]f16', 'ann': True, }, }, ) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`write-multivec-example-ts`); await ns.write({ upsert_rows: [ { id: 1, title_embedding: [0.1, 0.2, 0.3], image_embedding: [0.4, 0.5], title: "hello world", }, { id: 2, title_embedding: [0.4, 0.5, 0.6], image_embedding: [0.7, 0.8], title: "goodbye world", }, ], distance_metric: "cosine_distance", schema: { title_embedding: { type: "[3]f32", ann: true, }, image_embedding: { type: "[2]f16", ann: true, }, }, }); ``` ```go package main import ( "context" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" "github.com/turbopuffer/turbopuffer-go/v2/packages/param" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("write-multivec-example-go") _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "title_embedding": []float32{0.1, 0.2, 0.3}, "image_embedding": []float32{0.4, 0.5}, "title": "hello world", }, { "id": 2, "title_embedding": []float32{0.4, 0.5, 0.6}, "image_embedding": []float32{0.7, 0.8}, "title": "goodbye world", }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "title_embedding": { Type: "[3]f32", Ann: param.Override[turbopuffer.AttributeSchemaConfigAnnParam](true), }, "image_embedding": { Type: "[2]f16", Ann: param.Override[turbopuffer.AttributeSchemaConfigAnnParam](true), }, }, }, ) if err != nil { panic(err) } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class WriteMultivec { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("write-multivec-example-java"); ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", 1) .put("title_embedding", List.of(0.1f, 0.2f, 0.3f)) .put("image_embedding", List.of(0.4f, 0.5f)) .put("title", "hello world") .build() ) .addUpsertRow( Row.builder() .put("id", 2) .put("title_embedding", List.of(0.4f, 0.5f, 0.6f)) .put("image_embedding", List.of(0.7f, 0.8f)) .put("title", "goodbye world") .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .schema( Schema.builder() .put( "title_embedding", AttributeSchemaConfig.builder().type("[3]f32").ann(true).build() ) .put( "image_embedding", AttributeSchemaConfig.builder().type("[2]f16").ann(true).build() ) .build() ) .build() ); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("write-multivec-example-csharp"); await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("title_embedding", new[] { 0.1f, 0.2f, 0.3f }) .Set("image_embedding", new[] { 0.4f, 0.5f }) .Set("title", "hello world"), new Row() .Set("id", 2) .Set("title_embedding", new[] { 0.4f, 0.5f, 0.6f }) .Set("image_embedding", new[] { 0.7f, 0.8f }) .Set("title", "goodbye world"), ], DistanceMetric = DistanceMetric.CosineDistance, Schema = new Dictionary { ["title_embedding"] = new AttributeSchemaConfig { Type = "[3]f32", Ann = true }, ["image_embedding"] = new AttributeSchemaConfig { Type = "[2]f16", Ann = true }, }, } ); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("write-multivec-example-rb") ns.write( upsert_rows: [ { id: 1, title_embedding: [0.1, 0.2, 0.3], image_embedding: [0.4, 0.5], title: "hello world", }, { id: 2, title_embedding: [0.4, 0.5, 0.6], image_embedding: [0.7, 0.8], title: "goodbye world", }, ], distance_metric: "cosine_distance", schema: { title_embedding: { type: "[3]f32", ann: true, }, image_embedding: { type: "[2]f16", ann: true, }, }, ) ``` ## Schema turbopuffer maintains a schema for each namespace with type and indexing behaviour for each attribute. By default, types are automatically inferred from the passed data and every attribute is indexed. To inspect the schema, use the [metadata endpoint](/docs/metadata). To customize indexing behavior or to specify types that cannot be automatically inferred (e.g. `uuid`), you can pass a `schema` object in a write request. This can be done on every write, or only the first; there's no performance difference. If a new attribute is added, this attribute will default to null for any documents that existed before the attribute was added. Changing the attribute type of an existing attribute is currently an error. For an example, see [Configuring the schema](/docs/write#configuring-the-schema). **type** string required The data type of the attribute. Supported types: - `string`: String - `int`: Signed integer (i64) - `uint`: Unsigned integer (u64) - `float`: Floating-point number (f64) - `uuid`: 128-bit UUID - `datetime`: Date and time - `bool`: Boolean - `[]string`: Array of strings - `[]int`: Array of signed integers - `[]uint`: Array of unsigned integers - `[]float`: Array of floating-point numbers - `[]uuid`: Array of UUIDs - `[]datetime`: Array of dates and times - `[]bool`: Array of booleans - `[N]f32`: `N` dimensional f32 vector - `[N]f16`: `N` dimensional f16 vector - `[N]i8`: `N` dimensional i8 vector - `{}f16`: Sparse vector with string keys and [16-bit floats](https://en.wikipedia.org/wiki/Half-precision_floating-point_format) as weights. Example: `{"dim0": 0.1, "dim1": 0.2}`. All attributes are nullable, except for `id`. `string`, `int` and `bool` types and their array variants can be inferred from the write payload. Other types, such as `uint` or `uuid` must be set explicitly in the schema. See [UUID values](/docs/write#configuring-the-schema) for an example. `datetime` values should be provided as an ISO 8601 formatted string with a mandatory date and optional time and time zone. Internally, these values are converted to UTC (if the time zone is specified) and stored as a 64-bit integer representing milliseconds since the epoch. Example: ```json [ "2015-01-20", "2015-01-20T12:34:56", "2015-01-20T12:34:56-04:00" ] ``` `{}f16` attributes are not filterable and require indexing for fast `SparseKNN` operations. --- **ann** boolean required true for vector types Must be set to `true` for vector type attributes (`[N]f32`, `[N]f16`, `[N]i8`). Builds an approximate nearest neighbor index for the vector column, enabling fast vector queries via [`rank_by`](/docs/query#param-rank_by). Example: ```json { "my_vector": { "type": "[512]f16", "ann": true } } ``` --- **filterable** boolean default: true (false if full-text search or regex is enabled) Whether or not the attribute can be used in [filters](/docs/query#filtering-parameters)/WHERE clauses. Filtered attributes are indexed into an inverted index. At query-time, the [filter evaluation is recall-aware](/blog/native-filtering) when used for vector queries. Unfiltered attributes don't have an index built for them, and are thus billed at a 50% discount (see [pricing](/#pricing)). --- **regex** boolean default: false Whether to enable [Regex](/docs/query#param-Regex) filters on this attribute. If set, `filterable` defaults to `false`; you can override this by setting `filterable: true`. --- **glob** boolean default: false Whether to enable [Glob](/docs/query#param-Glob) filters on this attribute. If set, `filterable` defaults to `false`; you can override this by setting `filterable: true`. --- **fuzzy** boolean default: false Whether to enable [Fuzzy](/docs/query#param-Fuzzy) filters on this attribute. If set, `filterable` defaults to `false`; you can override this by setting `filterable: true`. See the [Full-Text Search example](/docs/fts#fuzzy-matching) for more detail. --- **full_text_search** boolean | object default: false Whether this attribute can be used as part of a [BM25 full-text search](/docs/fts). Requires the `string` or `[]string` type, and by default, BM25-enabled attributes are not filterable. You can override this by setting `filterable: true`. Can either be a boolean for default settings, or an object with the following optional fields: - `tokenizer` (string): How to convert the text to a list of tokens. Defaults to `word_v4`. The default is periodically upgraded for new namespaces. See: [Supported tokenizers](/docs/fts#tokenizers) - `case_sensitive` (boolean): Whether searching is case-sensitive. Defaults to `false` (i.e. case-insensitive). - `language` (string): The language of the text. Defaults to `english`. See: [Supported languages](/docs/fts/#supported-languages) - `stemming` (boolean): Language-specific stemming for the text. Defaults to `false` (i.e. do not stem). - `remove_stopwords` (boolean): Removes [common words][stopwords] from the text based on `language`. Defaults to `false` (i.e. keep common words). - `ascii_folding` (boolean): Whether to convert each non-ASCII character in a token to its ASCII equivalent, if one exists (e.g., à -> a). Applied after stemming and stopword removal. Defaults to `false` (i.e., no folding). - `max_token_length` (int): Maximum length of a token in bytes. Tokens larger than this value during tokenization will be filtered out. Has to be between `1` and `254` (inclusive). Defaults to `39`. - `k1` (float): Term frequency saturation parameter for BM25 scoring. Must be greater than zero. Defaults to `1.2`. See: [Advanced tuning](/docs/fts#advanced-tuning) - `b` (float): Document length normalization parameter for BM25 scoring. Must be in the range `[0.0, 1.0]`. Defaults to `0.75`. See: [Advanced tuning](/docs/fts#advanced-tuning) - `k3` (float): Query term frequency saturation parameter for BM25 scoring. Must be greater than zero. Defaults to `8.0`. See: [Advanced tuning](/docs/fts#advanced-tuning) If you require other types of full-text search options, please [contact us](/contact). [stopwords]: https://snowballstem.org/algorithms/english/stop.txt --- **sparse_knn** object default: unset When configured, this attribute can be used as part of a `SparseKNN` query. This is only supported on the `{}f16` type. This requires a `distance_metric` string field, which only supports `dot_product` as a value at the moment. ### Updating attributes We support online, in-place changes of the following schema attributes: - `filterable` - `full_text_search` - `regex` - `glob` - `fuzzy` The write does not need to include any documents, i.e. `{"schema": ...}` is supported, provided the namespace already exists. Other index settings changes, attribute type changes, and attribute deletions currently cannot be done in-place. Consider [exporting](/docs/export) documents and upserting into a new namespace if you require a schema change. After enabling the `filterable`, `full_text_search`, `regex`, `glob`, or `fuzzy` setting for an existing attribute, the index needs time to build before queries that depend on the index can be executed. turbopuffer will respond with HTTP status 202 to queries that depend on an index that is not yet built. Changing full-text search parameters also requires that the index be rebuilt. turbopuffer will do this automatically in the background, during which time queries will continue returning results using the previous full-text search settings. ### Billing An unindexed attribute is billed at 50% of its logical size. Indexed attributes are based on their logical size multiplied by the number of indexes they have enabled. For example, an attribute with with `filterable: true` and `full_text_search: true` is billed at 200% of its logical size. ## Examples ### Row-based writes Row-based writes may be more convenient than column-based writes. You can pass any combination of `upsert_rows`, `patch_rows`, `patch_by_filter`, `deletes`, and `delete_by_filter` to the write request. If the same document ID appears multiple times in the request, the request will fail with an HTTP 400 error. ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-upsert-row-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "distance_metric": "cosine_distance", "upsert_rows": [ { "id": 1, "vector": [0.1, 0.1], "my-string": "one", "my-uint": 12, "my-bool": true, "my-string-array": ["a", "b"] }, { "id": 2, "vector": [0.2, 0.2], "my-string-array": ["b", "d"] } ], "patch_rows": [ { "id": 3, "my-bool": true } ], "deletes": [4] }' # Response payload # { # "status": "OK" # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'write-upsert-row-example-py') # If an error occurs, this call raises a turbopuffer.APIError if a retry was not successful. ns.write( upsert_rows=[ { 'id': 1, 'vector': [0.1, 0.1], 'my-string': 'one', 'my-uint': 12, 'my-bool': True, 'my-string-array': ['a', 'b'] }, { 'id': 2, 'vector': [0.2, 0.2], 'my-string-array': ['b', 'd'] }, ], patch_rows=[ { 'id': 3, 'my-bool': True }, ], deletes=[4], distance_metric='cosine_distance' ) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`write-upsert-row-example-ts`); await ns.write({ upsert_rows: [ { id: 1, vector: [0.1, 0.1], "my-string": "one", "my-uint": 12, "my-bool": true, "my-string-array": ["a", "b"], }, { id: 2, vector: [0.2, 0.2], "my-string-array": ["b", "d"], } ], patch_rows: [ { id: 3, "my-bool": true, } ], deletes: [4], distance_metric: "cosine_distance", }); ``` ```go package main import ( "context" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( // API tokens are created in the dashboard: https://turbopuffer.com/dashboard option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("write-upsert-row-example-go") // If an error occurs, this call raises an error if a retry was not successful. _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 1, "vector": []float32{0.1, 0.1}, "my-string": "one", "my-uint": 12, "my-bool": true, "my-string-array": []string{"a", "b"}, }, { "id": 2, "vector": []float32{0.2, 0.2}, "my-string-array": []string{"b", "d"}, }, }, PatchRows: []turbopuffer.RowParam{ { "id": 3, "my-bool": true, }, }, Deletes: []any{4}, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, }, ) if err != nil { panic(err) } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.util.*; import java.util.stream.*; public class WriteUpsertRow { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("write-upsert-row-example-java"); // If an error occurs, this call raises a TurbopufferServiceException if a retry was not successful. ns.write( NamespaceWriteParams.builder() .upsertRows( List.of( Row.builder() .put("id", 1) .put("vector", List.of(0.1f, 0.1f)) .put("my-string", "one") .put("my-uint", 12) .put("my-bool", true) .put("my-string-array", List.of("a", "b")) .build(), Row.builder() .put("id", 2) .put("vector", List.of(0.2f, 0.2f)) .put("my-string-array", List.of("b", "d")) .build() ) ) .patchRows(List.of(Row.builder().put("id", 3).put("my-bool", true).build())) .addDelete(4) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .build() ); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("write-upsert-row-example-csharp"); // If an error occurs, this call raises a TurbopufferApiException if a retry was not successful. await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 1) .Set("vector", new[] { 0.1f, 0.1f }) .Set("my-string", "one") .Set("my-uint", 12) .Set("my-bool", true) .Set("my-string-array", new[] { "a", "b" }), new Row() .Set("id", 2) .Set("vector", new[] { 0.2f, 0.2f }) .Set("my-string-array", new[] { "b", "d" }), ], PatchRows = [new Row().Set("id", 3).Set("my-bool", true)], Deletes = [4L], DistanceMetric = DistanceMetric.CosineDistance, } ); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("write-upsert-row-example-rb") # If an error occurs, this call raises a Turbopuffer::Errors::APIError if a retry was not successful. ns.write( upsert_rows: [ { id: 1, vector: [0.1, 0.1], 'my-string': "one", 'my-uint': 12, 'my-bool': true, 'my-string-array': ["a", "b"], }, { id: 2, vector: [0.2, 0.2], 'my-string-array': ["b", "d"], }, ], patch_rows: [ { id: 3, 'my-bool': true, }, ], deletes: [4], distance_metric: "cosine_distance", ) ``` ### Configuring the schema The [schema](/docs/write#schema) can be passed on writes to manually configure attribute types and indexing behavior. A few examples where manually configuring the schema is needed: - **UUID** values serialized as strings can be stored in turbopuffer in an optimized format. - Enabling **full-text search**, **regex**, **glob**, or **fuzzy** indexing for string attributes. - **Disabling indexing/filtering** (`filterable:false`) on an attribute, for a 50% discount and improved indexing performance. An example of (1), (2), and (3): ```bash # Make a POST request using curl # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-schema-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ {"id": "769c134d-07b8-4225-954a-b6cc5ffc320c", "vector": [0.1, 0.1], "text": "the fox is quick and brown", "string": "fox", "permissions": ["ee1f7c89-a3aa-43c1-8941-c987ee03e7bc", "95cdf8be-98a9-4061-8eeb-2702b6bbcb9e"]} ], "distance_metric": "cosine_distance", "schema": { "id": "uuid", "text": { "type": "string", "full_text_search": true }, "permissions": { "type": "[]uuid" } } }' # Response payload # { # "status": "OK" # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'write-schema-example-py') ns.write( upsert_rows=[ { 'id': "769c134d-07b8-4225-954a-b6cc5ffc320c", 'vector': [0.1, 0.1], 'text': 'the fox is quick and brown', 'string': 'fox', 'permissions': ['ee1f7c89-a3aa-43c1-8941-c987ee03e7bc', '95cdf8be-98a9-4061-8eeb-2702b6bbcb9e'] }, ], distance_metric='cosine_distance', schema={ 'id': 'uuid', 'text': { 'type': 'string', 'full_text_search': True # sets filterable: false, and enables FTS with default settings }, 'permissions': { 'type': '[]uuid', # otherwise inferred as slower/more expensive []string } } ) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`write-schema-example-ts`); await ns.write({ upsert_rows: [ { id: "769c134d-07b8-4225-954a-b6cc5ffc320c", vector: [0.1, 0.1], text: "the fox is quick and brown", string: "fox", permissions: ["ee1f7c89-a3aa-43c1-8941-c987ee03e7bc", "95cdf8be-98a9-4061-8eeb-2702b6bbcb9e"], }, ], distance_metric: "cosine_distance", schema: { id: "uuid", text: { type: "string", full_text_search: true, // sets filterable: false, and enables FTS with default settings }, permissions: { type: "[]uuid", // otherwise inferred as slower/more expensive []string }, }, }); ``` ```go package main import ( "context" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("write-schema-example-go") _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": "769c134d-07b8-4225-954a-b6cc5ffc320c", "vector": []float32{0.1, 0.1}, "text": "the fox is quick and brown", "string": "fox", "permissions": []string{"ee1f7c89-a3aa-43c1-8941-c987ee03e7bc", "95cdf8be-98a9-4061-8eeb-2702b6bbcb9e"}, }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, Schema: map[string]turbopuffer.AttributeSchemaConfigParam{ "id": {Type: "uuid"}, "text": { Type: "string", // sets filterable: false, and enables FTS with default settings FullTextSearch: &turbopuffer.FullTextSearchConfigParam{}, }, // Otherwise inferred as slower/more expensive []string "permissions": {Type: "[]uuid"}, }, }, ) if err != nil { panic(err) } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class WriteSchema { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("write-schema-example-java"); ns.write( NamespaceWriteParams.builder() .addUpsertRow( Row.builder() .put("id", "769c134d-07b8-4225-954a-b6cc5ffc320c") .put("vector", List.of(0.1f, 0.1f)) .put("text", "the fox is quick and brown") .put("string", "fox") .put( "permissions", List.of( "ee1f7c89-a3aa-43c1-8941-c987ee03e7bc", "95cdf8be-98a9-4061-8eeb-2702b6bbcb9e" ) ) .build() ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .schema( Schema.builder() .put("id", AttributeSchemaConfig.builder().type("uuid").build()) .put( "text", AttributeSchemaConfig.builder() .type("string") // Sets filterable(false), and enables FTS with default settings .fullTextSearch(FullTextSearchConfig.defaults()) .build() ) .put( "permissions", AttributeSchemaConfig.builder() .type("[]uuid") // otherwise inferred as slower/more expensive []string .build() ) .build() ) .build() ); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("write-schema-example-csharp"); await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", "769c134d-07b8-4225-954a-b6cc5ffc320c") .Set("vector", new[] { 0.1f, 0.1f }) .Set("text", "the fox is quick and brown") .Set("string", "fox") .Set( "permissions", new[] { "ee1f7c89-a3aa-43c1-8941-c987ee03e7bc", "95cdf8be-98a9-4061-8eeb-2702b6bbcb9e", } ), ], DistanceMetric = DistanceMetric.CosineDistance, Schema = new Dictionary { ["id"] = new AttributeSchemaConfig { Type = "uuid" }, ["text"] = new AttributeSchemaConfig { Type = "string", // Sets filterable(false), and enables FTS with default settings FullTextSearch = true, }, ["permissions"] = new AttributeSchemaConfig { Type = "[]uuid", // otherwise inferred as slower/more expensive []string }, }, } ); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("write-schema-example-rb") ns.write( upsert_rows: [ { id: "769c134d-07b8-4225-954a-b6cc5ffc320c", vector: [0.1, 0.1], text: "the fox is quick and brown", string: "fox", permissions: ["ee1f7c89-a3aa-43c1-8941-c987ee03e7bc", "95cdf8be-98a9-4061-8eeb-2702b6bbcb9e"], }, ], distance_metric: "cosine_distance", schema: { id: "uuid", text: { type: "string", full_text_search: true, # sets filterable: false, and enables FTS with default settings }, permissions: { type: "[]uuid", # otherwise inferred as slower/more expensive []string }, }, ) ``` ### Column-based writes Bulk document operations should use a column-oriented layout for best performance. You can pass any combination of `upsert_columns`, `patch_columns`, `deletes`, and `delete_by_filter` to the write request. If the same document ID appears multiple times in the request, the request will fail with an HTTP 400 error. ```bash # Make a POST request using curl # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-upsert-columns-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_columns": { "id": [1, 2, 3, 4], "vector": [[0.1, 0.1], [0.2, 0.2], [0.3, 0.3], [0.4, 0.4]], "my-string": ["one", null, "three", "four"], "my-uint": [12, null, 84, 39], "my-bool": [true, null, false, true], "my-string-array": [["a", "b"], ["b", "d"], [], ["c"]] }, "patch_columns": { "id": [5, 6], "my-bool": [true, false] }, "deletes": [7, 8], "distance_metric": "cosine_distance" }' # Response payload # { # "status": "OK" # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'write-upsert-columns-example-py') # If an error occurs, this call raises a turbopuffer.APIError if a retry was not successful. ns.write( upsert_columns={ 'id': [1, 2, 3, 4], 'vector': [[0.1, 0.1], [0.2, 0.2], [0.3, 0.3], [0.4, 0.4]], 'my-string': ['one', None, 'three', 'four'], 'my-uint': [12, None, 84, 39], 'my-bool': [True, None, False, True], 'my-string-array': [['a', 'b'], ['b', 'd'], [], ['c']] }, patch_columns={ 'id': [5, 6], 'my-bool': [True, False], }, deletes=[7, 8], distance_metric='cosine_distance' ) ``` ```typescript // $ npm install @turbopuffer/turbopuffer import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ // API tokens are created in the dashboard: https://turbopuffer.com/dashboard apiKey: process.env.TURBOPUFFER_API_KEY, region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`write-upsert-columns-example-ts`); await ns.write({ upsert_columns: { id: [1, 2, 3, 4], vector: [[0.1, 0.1], [0.2, 0.2], [0.3, 0.3], [0.4, 0.4]], "my-string": ["one", null, "three", "four"], "my-uint": [12, null, 84, 39], "my-bool": [true, null, false, true], "my-string-array": [["a", "b"], ["b", "d"], [], ["c"]], }, patch_columns: { id: [5, 6], "my-bool": [true, false], }, deletes: [7, 8], distance_metric: "cosine_distance", }); ``` ```go package main import ( "context" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( // API tokens are created in the dashboard: https://turbopuffer.com/dashboard option.WithAPIKey(os.Getenv("TURBOPUFFER_API_KEY")), option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("write-upsert-columns-example-go") // If an error occurs, this call raises an error if a retry was not successful. _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertColumns: map[string][]any{ "id": {1, 2, 3, 4}, "vector": {[]float32{0.1, 0.1}, []float32{0.2, 0.2}, []float32{0.3, 0.3}, []float32{0.4, 0.4}}, "my-string": {"one", nil, "three", "four"}, "my-uint": {12, nil, 84, 39}, "my-bool": {true, nil, false, true}, "my-string-array": {[]string{"a", "b"}, []string{"b", "d"}, []string{}, []string{"c"}}, }, PatchColumns: map[string][]any{ "id": {5, 6}, "my-bool": {true, false}, }, Deletes: []any{7, 8}, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, }, ) if err != nil { panic(err) } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.util.*; import java.util.stream.*; public class WriteUpsertColumns { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("write-upsert-columns-example-java"); // If an error occurs, this call raises a TurbopufferServiceException if a retry was not successful. ns.write( NamespaceWriteParams.builder() .upsertColumns( Columns.builder() .put("id", List.of(1, 2, 3, 4)) .put( "vector", List.of( List.of(0.1f, 0.1f), List.of(0.2f, 0.2f), List.of(0.3f, 0.3f), List.of(0.4f, 0.4f) ) ) .put("my-string", List.of("one", Optional.empty(), "three", "four")) .put("my-uint", List.of(12, Optional.empty(), 84, 39)) .put("my-bool", List.of(true, Optional.empty(), false, true)) .put( "my-string-array", List.of(List.of("a", "b"), List.of("b", "d"), List.of(), List.of("c")) ) .build() ) .patchColumns( Columns.builder().put("id", List.of(5, 6)).put("my-bool", List.of(true, false)).build() ) .deletes(List.of(7, 8)) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .build() ); } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("write-upsert-columns-example-csharp"); // If an error occurs, this call raises a TurbopufferApiException if a retry was not successful. await ns.Write( new NamespaceWriteParams { UpsertColumns = new Columns() .SetColumn("id", new[] { 1, 2, 3, 4 }) .SetColumn( "vector", new[] { new[] { 0.1f, 0.1f }, new[] { 0.2f, 0.2f }, new[] { 0.3f, 0.3f }, new[] { 0.4f, 0.4f }, } ) // For columns that contain nulls, use a nullable element type // (null = no value). .SetColumn("my-string", new[] { "one", null, "three", "four" }) .SetColumn("my-uint", new uint?[] { 12, null, 84, 39 }) .SetColumn("my-bool", new bool?[] { true, null, false, true }) .SetColumn( "my-string-array", new string[][] { new string[] { "a", "b" }, new string[] { "b", "d" }, Array.Empty(), new string[] { "c" }, } ), PatchColumns = new Columns() .SetColumn("id", new[] { 5, 6 }) .SetColumn("my-bool", new[] { true, false }), Deletes = [7L, 8L], DistanceMetric = DistanceMetric.CosineDistance, } ); ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("write-upsert-columns-example-rb") # If an error occurs, this call raises a Turbopuffer::Errors::APIError if a retry was not successful. ns.write( upsert_columns: { id: [1, 2, 3, 4], vector: [[0.1, 0.1], [0.2, 0.2], [0.3, 0.3], [0.4, 0.4]], 'my-string': ["one", nil, "three", "four"], 'my-uint': [12, nil, 84, 39], 'my-bool': [true, nil, false, true], 'my-string-array': [["a", "b"], ["b", "d"], [], ["c"]], }, patch_columns: { id: [5, 6], 'my-bool': [true, false], }, deletes: [7, 8], distance_metric: "cosine_distance", ) ``` ### Conditional writes To make writes conditional, use the `upsert_condition`, `patch_condition`, and `delete_condition` parameters. These let you specify a condition that must be satisfied for the write operation to each document to proceed. Conditional deletes are distinct from [`delete_by_filter`](#delete-by-filter), which should be used when the set of IDs to conditionally delete is not known ahead of time. Conditions are evaluated before each write, using the current value of the document with the matching ID. * If the document exists and the condition is met, the write is applied. * If the document exists and the condition is not met, the write is skipped. * If the document does not exist, the write is applied unconditionally for upserts and skipped unconditionally for patches and deletes. The operation will return the actual number of documents written (upserted, patched, or deleted). Internally, the operation performs a query (one per batch) to determine which documents match the condition, so it is billed as both a query and a write operation. However, if the condition is not met for a given document, that write is skipped and not billed. The condition syntax matches the [`filters` parameter in the query API](query#filtering), with an additional feature: you can reference the new value being written using `$ref_new` references. These look like `{"$ref_new": "attr_123"}` and can be used in place of value literals. This allows the condition to vary by document in a multi-document write request. Two common patterns: * **Version checks**: Set `upsert_condition` to `["version", "Lt", {"$ref_new": "version"}]` to only apply writes when the new document has a higher version than the existing one. * **Insert if not exists**: Set `upsert_condition` to `["id", "Eq", null]` to only insert documents that don't already exist. Since existing documents always have a non-null `id`, this condition fails for existing documents (skipping the write), while new documents are inserted unconditionally. ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-conditional-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ { "id": 101, "vector": [0.2, 0.8], "title": "LISP Guide for Beginners (draft_v2)", "version": 2 }, { "id": 102, "vector": [0.4, 0.4], "title": "AI for Practitioners (final)", "version": 5 } ], "distance_metric": "cosine_distance" }' # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-conditional-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ { "id": 101, "vector": [0.2, 0.8], "title": "LISP Guide for Beginners (final)", "version": 3 }, { "id": 102, "vector": [0.4, 0.4], "title": "AI for Practitioners (draft_v4)", "version": 4 }, { "id": 103, "vector": [0.6, 0.8], "title": "Database Internals (draft_v1)", "version": 1 } ], "upsert_condition": [ "version", "Lt", {"$ref_new": "version"} ], "distance_metric": "cosine_distance" }' # Response payload # { # "status": "OK", # "message": "documents committed successfully", # "rows_affected": 2 # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region='gcp-us-central1', # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'write-conditional-example-py') ns.write( upsert_rows=[ { 'id': 101, 'vector': [0.2, 0.8], 'title': 'LISP Guide for Beginners (draft_v2)', 'version': 2 }, { 'id': 102, 'vector': [0.4, 0.4], 'title': 'AI for Practitioners (final)', 'version': 5 } ], distance_metric='cosine_distance' ) # Conditionally upsert documents with news title, making sure no version # regression occurs. result = ns.write( upsert_rows=[ { 'id': 101, 'vector': [0.2, 0.8], 'title': 'LISP Guide for Beginners (final)', 'version': 3 }, { 'id': 102, 'vector': [0.4, 0.4], 'title': 'AI for Practitioners (draft_v4)', 'version': 4 }, { 'id': 103, 'vector': [0.6, 0.8], 'title': 'Database Internals (draft_v1)', 'version': 1 } ], upsert_condition=('version', 'Lt', {'$ref_new': 'version'}), distance_metric='cosine_distance' ) print(result.rows_affected) # 2 results = ns.query(limit=10, include_attributes=True) print(results.rows) ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`write-conditional-example-ts`); await ns.write({ upsert_rows: [ { id: 101, vector: [0.2, 0.8], title: "LISP Guide for Beginners (draft_v2)", version: 2, }, { id: 102, vector: [0.4, 0.4], title: "AI for Practitioners (final)", version: 5, }, ], distance_metric: "cosine_distance", }); // Conditionally upsert documents with news title, making sure no version // regression occurs. const writeResult = await ns.write({ upsert_rows: [ { id: 101, vector: [0.2, 0.8], title: "LISP Guide for Beginners (final)", version: 3, }, { id: 102, vector: [0.4, 0.4], title: "AI for Practitioners (draft_v4)", version: 4, }, { id: 103, vector: [0.6, 0.8], title: "Database Internals (draft_v1)", version: 1, }, ], upsert_condition: [ "version", "Lt", { $ref_new: "version" }, ], distance_metric: "cosine_distance", }); console.log(writeResult.rows_affected); // 2 const results = await ns.query({ rank_by: [ "id", "asc", ], limit: 10, }); console.log(results.rows!.length); // 3 ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("write-conditional-example-go") _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 101, "vector": []float32{0.2, 0.8}, "title": "LISP Guide for Beginners (draft_v2)", "version": 2, }, { "id": 102, "vector": []float32{0.4, 0.4}, "title": "AI for Practitioners (final)", "version": 5, }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, }, ) if err != nil { panic(err) } // Conditionally upsert documents with news title, making sure no version // regression occurs. res, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 101, "vector": []float32{0.2, 0.8}, "title": "LISP Guide for Beginners (final)", "version": 3, }, { "id": 102, "vector": []float32{0.4, 0.4}, "title": "AI for Practitioners (draft_v4)", "version": 4, }, { "id": 103, "vector": []float32{0.6, 0.8}, "title": "Database Internals (draft_v1)", "version": 1, }, }, UpsertCondition: turbopuffer.NewFilterLt("version", turbopuffer.NewExprRefNew("version")), }, ) if err != nil { panic(err) } fmt.Println("Rows affected:", res.RowsAffected) // 2 results, err := ns.Query(ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{ Total: 1000, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{Bool: turbopuffer.Bool(true)}, }) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(results.Rows)) } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class WriteConditional { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("write-conditional-example-java"); ns.write( NamespaceWriteParams.builder() .upsertRows( List.of( Row.builder() .put("id", 101) .put("vector", List.of(0.2, 0.8)) .put("title", "LISP Guide for Beginners (draft_v2)") .put("version", 2) .build(), Row.builder() .put("id", 102) .put("vector", List.of(0.4, 0.4)) .put("title", "AI for Practitioners (final)") .put("version", 5) .build() ) ) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .build() ); // Conditionally upsert documents with news title, making sure no version // regression occurs. var writeResult = ns.write( NamespaceWriteParams.builder() .upsertRows( List.of( Row.builder() .put("id", 101) .put("vector", List.of(0.2, 0.8)) .put("title", "LISP Guide for Beginners (final)") .put("version", 3) .build(), Row.builder() .put("id", 102) .put("vector", List.of(0.4, 0.4)) .put("title", "AI for Practitioners (draft_v4)") .put("version", 4) .build(), Row.builder() .put("id", 103) .put("vector", List.of(0.6, 0.8)) .put("title", "Database Internals (draft_v1)") .put("version", 1) .build() ) ) .upsertCondition(Filter.lt("version", Expr.refNew("version"))) .distanceMetric(DistanceMetric.COSINE_DISTANCE) .build() ); System.out.println(writeResult.rowsAffected()); // 2 var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.attribute("id", RankByAttributeOrder.ASC)) .limit(10) .includeAttributes(true) .build() ); System.out.println(queryResult.rows().get()); } } ``` ```cs // dotnet add package Turbopuffer using System; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("write-conditional-example-csharp"); await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 101) .Set("vector", new[] { 0.2f, 0.8f }) .Set("title", "LISP Guide for Beginners (draft_v2)") .Set("version", 2), new Row() .Set("id", 102) .Set("vector", new[] { 0.4f, 0.4f }) .Set("title", "AI for Practitioners (final)") .Set("version", 5), ], DistanceMetric = DistanceMetric.CosineDistance, } ); // Conditionally upsert documents with news title, making sure no version // regression occurs. var writeResult = await ns.Write( new NamespaceWriteParams { UpsertRows = [ new Row() .Set("id", 101) .Set("vector", new[] { 0.2f, 0.8f }) .Set("title", "LISP Guide for Beginners (final)") .Set("version", 3), new Row() .Set("id", 102) .Set("vector", new[] { 0.4f, 0.4f }) .Set("title", "AI for Practitioners (draft_v4)") .Set("version", 4), new Row() .Set("id", 103) .Set("vector", new[] { 0.6f, 0.8f }) .Set("title", "Database Internals (draft_v1)") .Set("version", 1), ], UpsertCondition = Filter.Lt("version", Expr.RefNew("version")), DistanceMetric = DistanceMetric.CosineDistance, } ); Console.WriteLine(writeResult.RowsAffected); // 2 var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Attribute("id", RankByAttributeOrder.ASC), Limit = 10, IncludeAttributes = true, } ); foreach (var row in queryResult.GetRows()) { Console.WriteLine(row); } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("write-conditional-example-rb") ns.write( upsert_rows: [ { id: 101, vector: [0.2, 0.8], title: "LISP Guide for Beginners (draft_v2)", version: 2, }, { id: 102, vector: [0.4, 0.4], title: "AI for Practitioners (final)", version: 5, }, ], distance_metric: "cosine_distance", ) # Conditionally upsert documents with news title, making sure no version # regression occurs. result = ns.write( upsert_rows: [ { id: 101, vector: [0.2, 0.8], title: "LISP Guide for Beginners (final)", version: 3, }, { id: 102, vector: [0.4, 0.4], title: "AI for Practitioners (draft_v4)", version: 4, }, { id: 103, vector: [0.6, 0.8], title: "Database Internals (draft_v1)", version: 1, }, ], upsert_condition: [ "version", "Lt", { '$ref_new': "version" }, ], distance_metric: "cosine_distance", ) puts result.rows_affected # 2 results = ns.query(limit: 10, include_attributes: true) puts results.rows ``` ### Delete by filter To delete documents that match a filter, use `delete_by_filter`. This operation will return the actual number of documents removed. Because the operation internally issues a query to determine which documents to delete, this operation is billed as both a query and a write operation. If `delete_by_filter` is used in the same request as other write operations, `delete_by_filter` will be applied before the other operations. This allows you to delete rows that match a filter before writing new row with overlapping IDs. Note that patches to any deleted rows are ignored. `delete_by_filter` has the same syntax as the [`filters` parameter in the query API](query#filtering). ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-delete-by-filter-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ {"id": 101, "vector": [0.2, 0.8], "title": "LISP Guide for Beginners", "views": 10}, {"id": 102, "vector": [0.4, 0.4], "title": "AI for Practitioners", "views": 2500} ], "distance_metric": "cosine_distance" }' # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-delete-by-filter-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "delete_by_filter": [ "And", [ ["title", "IGlob", "*guide*"], ["views", "Lte", 1000] ] ] }' # Response payload # { # "status": "OK", # "message": "documents committed successfully", # "rows_affected": 1 // number of rows that were deleted # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'write-delete-by-filter-example-py') ns.write( upsert_rows=[ { "id": 101, "vector": [0.2, 0.8], "title": "LISP Guide for Beginners", "views": 10, }, { "id": 102, "vector": [0.4, 0.4], "title": "AI for Practitioners", "views": 2500, }, ], distance_metric="cosine_distance", ) # Delete posts with titles that include the word "guide" # and have 1000 or less views result = ns.write( delete_by_filter=("And", [("title", "IGlob", "*guide*"), ("views", "Lte", 1000)]) ) print(result.rows_affected) # 1 results = ns.query(rank_by=("id", "asc"), limit=10) print(len(results.rows)) # 1 ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`write-delete-by-filter-example-ts`); await ns.write({ upsert_rows: [ { id: 101, vector: [0.2, 0.8], title: "LISP Guide for Beginners", views: 10 }, { id: 102, vector: [0.4, 0.4], title: "AI for Practitioners", views: 2500 }, ], distance_metric: "cosine_distance", }); // Delete posts with titles that include the word "guide" // and have 1000 or less views const writeResult = await ns.write({ delete_by_filter: [ "And", [ ["title", "IGlob", "*guide*"], ["views", "Lte", 1000], ], ], }); console.log(writeResult.rows_affected); // 1 const results = await ns.query({ rank_by: [ "id", "asc", ], limit: 10, }); console.log(results.rows!.length); // 1 ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("write-delete-by-filter-example-go") _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 101, "vector": []float32{0.2, 0.8}, "title": "LISP Guide for Beginners", "views": 10, }, { "id": 102, "vector": []float32{0.4, 0.4}, "title": "AI for Practitioners", "views": 2500, }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, }, ) if err != nil { panic(err) } // Delete posts with titles that include the word "guide" // and have 1000 or less views res, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ DeleteByFilter: turbopuffer.NewFilterAnd([]turbopuffer.Filter{ turbopuffer.NewFilterIGlob("title", "*guide*"), turbopuffer.NewFilterLte("views", 1000), }), }, ) if err != nil { panic(err) } fmt.Println("Rows affected:", res.RowsAffected) // 1 results, err := ns.Query(ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{ Total: 1000, }, }) if err != nil { panic(err) } fmt.Print(turbopuffer.PrettyPrint(results.Rows)) // returns only one result } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class WriteDeleteByFilter { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("write-delete-by-filter-example-java"); // Delete posts with titles that include the word "guide" // and have 1000 or less views var writeResult = ns.write( NamespaceWriteParams.builder() .deleteByFilter(Filter.and(Filter.iGlob("title", "*guide*"), Filter.lte("views", 1000))) .build() ); System.out.println(writeResult.rowsAffected()); // 1 var queryResult = ns.query( NamespaceQueryParams.builder().aggregateBy(Map.of("count", AggregateBy.count("id"))).build() ); var aggregations = queryResult.aggregations().get(); System.out.println(aggregations.get("count")); // 1 } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("write-delete-by-filter-example-csharp"); // Delete posts with titles that include the word "guide" // and have 1000 or less views var writeResult = await ns.Write( new NamespaceWriteParams { DeleteByFilter = Filter.And(Filter.IGlob("title", "*guide*"), Filter.Lte("views", 1000)), } ); Console.WriteLine(writeResult.RowsAffected); // 1 var queryResult = await ns.Query( new NamespaceQueryParams { AggregateBy = new Dictionary { ["count"] = AggregateBy.Count("id"), }, } ); var aggregations = queryResult.GetAggregations(); Console.WriteLine(aggregations["count"]); // 1 ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("write-delete-by-filter-example-rb") ns.write( upsert_rows: [ { id: 101, vector: [0.2, 0.8], title: "LISP Guide for Beginners", views: 10 }, { id: 102, vector: [0.4, 0.4], title: "AI for Practitioners", views: 2500 }, ], distance_metric: "cosine_distance", ) # Delete posts with titles that include the word "guide" # and have 1000 or less views result = ns.write( delete_by_filter: [ "And", [ ["title", "IGlob", "*guide*"], ["views", "Lte", 1000], ], ], ) puts result.rows_affected # 1 results = ns.query( rank_by: [ "id", "asc", ], limit: 10, ) puts results.rows.length # 1 ``` ### Patch by filter To patch a dynamically determined set of documents, use `patch_by_filter`. This operation will return the actual number of documents updated. When [`rows_remaining`](#responsefield-rows_remaining) is set to true in the response, there may be more documents matching your filter that can be patched, issue a follow up request to patch those documents. Because this operation uses a query to determine which rows need to be patched, it will be billed as a query & a patch operation (1 write, 2 queries total). If `patch_by_filter` is used in the same request as other write operations, it is applied after `delete_by_filter` but before any other write operations. `patch_by_filter` will not apply to any rows which were deleted. ```bash # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-patch-by-filter-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "upsert_rows": [ {"id": 101, "vector": [0.2, 0.8], "title": "LISP Guide for Beginners", "views": 10, "status": "published"}, {"id": 102, "vector": [0.4, 0.4], "title": "AI for Practitioners", "views": 2500, "status": "published"}, {"id": 103, "vector": [0.6, 0.3], "title": "Rust Basics", "views": 50, "status": "published"} ], "distance_metric": "cosine_distance" }' # choose best region: https://turbopuffer.com/docs/regions curl https://gcp-us-central1.turbopuffer.com/v2/namespaces/write-patch-by-filter-example-curl \ -X POST --fail-with-body \ -H "Authorization: Bearer $TURBOPUFFER_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "patch_by_filter": { "filters": [ "And", [ ["status", "Eq", "published"], ["views", "Lte", 100] ] ], "patch": {"status": "archived"} } }' # Response payload # { # "status": "OK", # "message": "documents committed successfully", # "rows_affected": 2 // number of rows that were patched # } ``` ```python import turbopuffer tpuf = turbopuffer.Turbopuffer( region="gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace(f'write-patch-by-filter-example-py') ns.write( upsert_rows=[ { "id": 101, "vector": [0.2, 0.8], "title": "LISP Guide for Beginners", "views": 10, "status": "published", }, { "id": 102, "vector": [0.4, 0.4], "title": "AI for Practitioners", "views": 2500, "status": "published", }, { "id": 103, "vector": [0.6, 0.3], "title": "Rust Basics", "views": 50, "status": "published", }, ], distance_metric="cosine_distance", ) # Archive posts that are published and have 100 or fewer views result = ns.write( patch_by_filter={ "filters": ("And", [("status", "Eq", "published"), ("views", "Lte", 100)]), "patch": {"status": "archived"}, } ) print(result.rows_affected) # 2 results = ns.query(rank_by=("id", "asc"), include_attributes=["status"], limit=10) for row in results.rows: print(f"ID {row['id']}: {row['status']}") # IDs 101 and 103 are now archived ``` ```typescript import { Turbopuffer } from "@turbopuffer/turbopuffer"; const tpuf = new Turbopuffer({ region: "gcp-us-central1", // choose best region: https://turbopuffer.com/docs/regions }); const ns = tpuf.namespace(`write-patch-by-filter-example-ts`); await ns.write({ upsert_rows: [ { id: 101, vector: [0.2, 0.8], title: "LISP Guide for Beginners", views: 10, status: "published" }, { id: 102, vector: [0.4, 0.4], title: "AI for Practitioners", views: 2500, status: "published" }, { id: 103, vector: [0.6, 0.3], title: "Rust Basics", views: 50, status: "published" }, ], distance_metric: "cosine_distance", }); // Archive posts that are published and have 100 or fewer views const writeResult = await ns.write({ patch_by_filter: { filters: [ "And", [ ["status", "Eq", "published"], ["views", "Lte", 100], ], ], patch: { status: "archived" }, }, }); console.log(writeResult.rows_affected); // 2 const results = await ns.query({ rank_by: [ "id", "asc", ], limit: 10, include_attributes: ["status"], }); results.rows!.forEach((row) => { console.log(`ID ${row.id}: ${row.status}`); // IDs 101 and 103 are now archived }); ``` ```go package main import ( "context" "fmt" "os" "github.com/turbopuffer/turbopuffer-go/v2" "github.com/turbopuffer/turbopuffer-go/v2/option" ) func main() { ctx := context.Background() tpuf := turbopuffer.NewClient( option.WithRegion("gcp-us-central1"), // choose best region: https://turbopuffer.com/docs/regions ) ns := tpuf.Namespace("write-patch-by-filter-example-go") _, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ UpsertRows: []turbopuffer.RowParam{ { "id": 101, "vector": []float32{0.2, 0.8}, "title": "LISP Guide for Beginners", "views": 10, "status": "published", }, { "id": 102, "vector": []float32{0.4, 0.4}, "title": "AI for Practitioners", "views": 2500, "status": "published", }, { "id": 103, "vector": []float32{0.6, 0.3}, "title": "Rust Basics", "views": 50, "status": "published", }, }, DistanceMetric: turbopuffer.DistanceMetricCosineDistance, }, ) if err != nil { panic(err) } // Archive posts that are published and have 100 or fewer views res, err := ns.Write( ctx, turbopuffer.NamespaceWriteParams{ PatchByFilter: turbopuffer.NamespaceWriteParamsPatchByFilter{ Filters: turbopuffer.NewFilterAnd([]turbopuffer.Filter{ turbopuffer.NewFilterEq("status", "published"), turbopuffer.NewFilterLte("views", 100), }), Patch: turbopuffer.RowParam{ "status": "archived", }, }, }, ) if err != nil { panic(err) } fmt.Println("Rows affected:", res.RowsAffected) // 2 results, err := ns.Query(ctx, turbopuffer.NamespaceQueryParams{ Limit: turbopuffer.LimitParam{ Total: 10, }, IncludeAttributes: turbopuffer.IncludeAttributesParam{StringArray: []string{"status"}}, }) if err != nil { panic(err) } for _, row := range results.Rows { fmt.Printf("ID %v: %v\n", row["id"], row["status"]) // IDs 101 and 103 are now archived } } ``` ```java package com.turbopuffer.docs; import com.turbopuffer.client.okhttp.*; import com.turbopuffer.core.*; import com.turbopuffer.models.namespaces.*; import java.util.*; public class WritePatchByFilter { public static void main(String[] args) { var tpuf = TurbopufferOkHttpClient.builder() .fromEnv() .region("gcp-us-central1") // choose best region: https://turbopuffer.com/docs/regions .build(); var ns = tpuf.namespace("write-patch-by-filter-example-java"); // Archive posts that are published and have 100 or fewer views var writeResult = ns.write( NamespaceWriteParams.builder() .patchByFilter( NamespaceWriteParams.PatchByFilter.builder() .filters(Filter.and(Filter.eq("status", "published"), Filter.lte("views", 100))) .patch( NamespaceWriteParams.PatchByFilter.Patch.builder() .putAdditionalProperty("status", JsonString.of("archived")) .build() ) .build() ) .build() ); System.out.println(writeResult.rowsAffected()); // 2 var queryResult = ns.query( NamespaceQueryParams.builder() .rankBy(RankBy.attribute("id", RankByAttributeOrder.ASC)) .limit(10) .includeAttributes("status") .build() ); for (var row : queryResult.rows().get()) { System.out.println("ID " + row.get("id") + ": " + row.get("status")); // IDs 101 and 103 are now archived } } } ``` ```cs // dotnet add package Turbopuffer using System; using System.Collections.Generic; using Turbopuffer; using Turbopuffer.Models.Namespaces; using var tpuf = new TurbopufferClient { // Pick the right region: https://turbopuffer.com/docs/regions Region = "gcp-us-central1", }; var ns = tpuf.Namespace("write-patch-by-filter-example-csharp"); // Archive posts that are published and have 100 or fewer views var writeResult = await ns.Write( new NamespaceWriteParams { PatchByFilter = new PatchByFilter { Filters = Filter.And(Filter.Eq("status", "published"), Filter.Lte("views", 100)), Patch = new Row().Set("status", "archived"), }, } ); Console.WriteLine(writeResult.RowsAffected); // 2 var queryResult = await ns.Query( new NamespaceQueryParams { RankBy = RankBy.Attribute("id", RankByAttributeOrder.ASC), Limit = 10, IncludeAttributes = new List { "status" }, } ); foreach (var row in queryResult.GetRows()) { Console.WriteLine($"ID {row.Get("id")}: {row.Get("status")}"); // IDs 101 and 103 are now archived } ``` ```ruby require "turbopuffer" tpuf = Turbopuffer::Client.new( region: "gcp-us-central1", # choose best region: https://turbopuffer.com/docs/regions ) ns = tpuf.namespace("write-patch-by-filter-example-rb") ns.write( upsert_rows: [ { id: 101, vector: [0.2, 0.8], title: "LISP Guide for Beginners", views: 10, status: "published" }, { id: 102, vector: [0.4, 0.4], title: "AI for Practitioners", views: 2500, status: "published" }, { id: 103, vector: [0.6, 0.3], title: "Rust Basics", views: 50, status: "published" }, ], distance_metric: "cosine_distance", ) # Archive posts that are published and have 100 or fewer views result = ns.write( patch_by_filter: { filters: [ "And", [ ["status", "Eq", "published"], ["views", "Lte", 100], ], ], patch: { status: "archived" }, }, ) puts result.rows_affected # 2 results = ns.query( rank_by: [ "id", "asc", ], limit: 10, include_attributes: ["status"], ) results.rows.each do |row| puts "ID #{row[:id]}: #{row[:status]}" # IDs 101 and 103 are now archived end ``` --- This page: [/docs/write.md](https://turbopuffer.com/docs/write.md) All documentation pages: [/llms.txt](https://turbopuffer.com/llms.txt) All documentation in one file: [/llms-full.txt](https://turbopuffer.com/llms-full.txt)