For AI agents: this page is also available as Markdown at /docs/compat-turbopuffer.md. Every docs page is: add .md to its URL. llms.txt lists them all.

turbopuffer compatibility

This page describes a compatibility layer for the turbopuffer SDKs. WarpSearch's own API under /v1 is the primary way to create collections and define schemas.

WarpSearch serves the turbopuffer HTTP API at https://api.warpsearch.io/compat/turbopuffer. The official turbopuffer SDKs for Python, TypeScript, Go, Java and Ruby work against it by changing configuration only: a base URL and a WarpSearch API key. Requests and responses keep turbopuffer's shapes, including $dist, rows, aggregations and multi-query results.

A turbopuffer namespace is a WarpSearch collection, so the same data can also be searched with the Elasticsearch, OpenSearch, Solr, SQL and MCP interfaces. Features that are not implemented yet fail with 501 and nothing is written, so you never get a silently wrong answer.

Connect

Create an API key in the management console and use it as the turbopuffer API key. The SDKs send it as Authorization: Bearer ws_live_..., which WarpSearch accepts.

SDK Base URL setting API key
Python Turbopuffer(base_url=...) or TURBOPUFFER_BASE_URL api_key= or TURBOPUFFER_API_KEY
TypeScript new Turbopuffer({ baseURL }) or TURBOPUFFER_BASE_URL apiKey or TURBOPUFFER_API_KEY
Go option.WithBaseURL(...) or TURBOPUFFER_BASE_URL option.WithAPIKey(...) or TURBOPUFFER_API_KEY
Java .baseUrl(...) on the client builder, TURBOPUFFER_BASE_URL or the turbopuffer.baseUrl system property .apiKey(...) or TURBOPUFFER_API_KEY
Ruby base_url: or TURBOPUFFER_BASE_URL api_key: or TURBOPUFFER_API_KEY

Remove the region. Every SDK refuses to start with region is set, but would be ignored when a region is configured and the base URL has no {region} placeholder. Delete the region argument and unset TURBOPUFFER_REGION.

  from turbopuffer import Turbopuffer

  tpuf = Turbopuffer(
-     region="gcp-us-central1",
-     api_key="tpuf_...",
+     base_url="https://api.warpsearch.io/compat/turbopuffer",
+     api_key="ws_live_...",
  )
import Turbopuffer from '@turbopuffer/turbopuffer';

const tpuf = new Turbopuffer({
  baseURL: 'https://api.warpsearch.io/compat/turbopuffer',
  apiKey: process.env['TURBOPUFFER_API_KEY'], // a ws_live_... key
});

If your code reads its settings from the environment, switching needs no code change at all:

export TURBOPUFFER_BASE_URL=https://api.warpsearch.io/compat/turbopuffer
export TURBOPUFFER_API_KEY=ws_live_...
unset TURBOPUFFER_REGION

Older or community clients that cannot change the URL can usually take a custom HTTP client (an httpx client in Python, a custom fetch in TypeScript, option.WithHTTPClient or option.WithMiddleware in Go) that rewrites the request host. Leave SDK compression at its default. The X-turbopuffer-Client-Telemetry header is accepted and ignored.

Namespaces and collections

A namespace whose name is a valid WarpSearch collection name (1 to 64 characters, starting with a lowercase letter, only lowercase letters, digits and _, no __) uses that name directly. Any other turbopuffer name, with uppercase letters, - or . and up to 128 characters, is stored under a derived collection name: tp_ followed by 26 lowercase letters and digits. The turbopuffer API keeps using the original name everywhere, in writes, queries and the namespace list. The console shows the derived collection, with the original name in its turbopuffer_namespace metadata.

The first write creates the namespace, as does create_namespace: true. Creating one needs a key with the editor or admin role. Writes need writer, queries need viewer, and deleting a namespace needs admin. See API keys, roles and rate limits. For a derived tp_ name, a key limited to certain collection names must also cover the tp_ name. Plan limits on collections, request sizes, documents per request and rates apply as on every other API; see Limits and errors.

Endpoints

Endpoint Notes
POST /v2/namespaces/{namespace} Write
POST /v2/namespaces/{namespace}/query Query and multi-query
DELETE /v2/namespaces/{namespace} Returns {"status":"OK"}. Data removal finishes in the background, and the name stays taken until then
GET /v1/namespaces prefix, cursor, page_size. Returns {"namespaces":[{"id":...}], "next_cursor":...}
GET /v2/namespaces/{namespace}/metadata approx_row_count, approx_logical_bytes, created_at, updated_at, schema. Row and byte counts are refreshed about once a minute and read 0 until first measured. Index status is always up to date, encryption is the default
GET, POST /v1/namespaces/{namespace}/schema Read, or add attributes. Changing an existing attribute's type fails with 400, as in turbopuffer; changing its other options (for example turning on full_text_search) answers 501
GET /v1/namespaces/{namespace}/hint_cache_warm Loads an idle namespace onto a search node ahead of traffic. Returns {"status":"ACCEPTED"}. See Idle collections and cold starts

Coming soon: explain_query, _debug/recall, copy_from_namespace, branch_from_namespace and the async copy operations, and PATCH on namespace metadata (pinning and read_only). They answer 501 today.

Writes

Supported: upsert_rows, upsert_columns, deletes, distance_metric (cosine_distance or euclidean_squared), schema, create_namespace and return_affected_ids. disable_backpressure and sharding are accepted and ignored.

A write is committed before the response returns, so the next query sees it. Queries accept consistency: {"level": "eventual"} as well. Every upsert replaces the whole document, as in turbopuffer. IDs can be unsigned integers, UUIDs or strings of up to 64 bytes, and come back with the type you wrote.

Coming soon: patch_rows, patch_columns, patch_by_filter, delete_by_filter, upsert_condition, patch_condition, delete_condition, encryption, copy_from_namespace and branch_from_namespace. A request that contains any of them is rejected with 501 and nothing is written, even if it also contains upserts.

501 responses carry x-should-retry: false, so the SDKs raise at once instead of retrying. Errors use the body {"status":"error","error":"<message>"}. Authentication failures (401) and rate limiting (429, with Retry-After) use WarpSearch's standard error body; the SDKs act on the status code either way.

Schema and types

Types are inferred from the first non-null value written, or declared in schema. An attribute that has only ever been null is not created yet. Supported: string, int, uint (up to 2^63-1), float, uuid, datetime, bool, their array forms ([]string, []int, []uint, []float, []uuid, []datetime, []bool), and dense vectors [N]f32 and [N]f16. f16 vectors are stored at half precision; on the wire they stay float32.

Vectors can be sent as JSON number arrays or as base64 little-endian float32. Set vector_encoding: "base64" on a query to get them back in base64. A column named vector is a vector automatically. Other vector attributes need a schema entry with an [N]f32 type or ann: true. Every vector attribute gets an approximate index, built automatically as the namespace grows.

full_text_search: true, or a configuration object, makes a string attribute searchable with BM25.

Option Support
case_sensitive, ascii_folding Honored
stemming, language English only. WarpSearch uses a dictionary stemmer (KStem), so some stems differ from turbopuffer's
remove_stopwords Accepted, no effect. Stop words are kept, so scores differ slightly
tokenizer, max_token_length, k1, b Accepted, no effect
filterable, regex, glob, fuzzy Accepted. Filters work on every non-text attribute whatever these say

On a full-text attribute, Eq matches as a phrase rather than by exact value, unless the attribute also has filterable: true.

Attribute names may use letters, digits, _, . and @, and must start with a letter or _; other names (for example with - or spaces) fail with 400. Attributes created through other WarpSearch APIs, such as the WarpSearch schema API or an Elasticsearch mapping, appear in the turbopuffer schema with the closest type.

Not supported: multi-vector []N f32 with late_interaction, sparse vectors ({}f16 and sparse_knn), bytes, [N]i8, and embed. They fail with 400 or 501.

Queries

Feature Support
rank_by vector ["vector", "ANN", [...]] and exact "kNN"
rank_by full text ["text", "BM25", "query"], with last_as_prefix. Sum, Max and Product (weights) over BM25 clauses, and filters inside rank_by (score 1 when they match)
rank_by attributes ["price", "desc"], up to 8 keys, and ordering by id
filters And, Or, Not, Eq, NotEq, In, NotIn, Lt, Lte, Gt, Gte, Contains, NotContains, ContainsAny, NotContainsAny, AnyLt, AnyLte, AnyGt, AnyGte, Glob and NotGlob (* and ? only), Regex, Fuzzy, ContainsAllTokens, ContainsAnyToken, ContainsTokenSequence
Results include_attributes (a list or true; unknown names are ignored), exclude_attributes, top_k (default 10), limit (a number or {"total": n}), offset, vector_encoding, consistency
Diversification limit.per with limit: 1 and one attribute keeps the best row per value
aggregate_by Count and Sum, with or without group_by on one or more attributes, and ["ForEachUnique", "tags"] for arrays. Documents without a value for a group key are left out of the groups
Multi-query queries (up to 16), and rerank_by: ["RRF", {"rank_constant": 60, "weights": [...]}] for one fused hybrid result, paged by a top-level limit and offset

$dist is the vector distance for ANN and kNN (cosine_distance is 1 minus cosine similarity), the BM25 score for full text, and the RRF score after rerank_by. It is absent when you order by attributes. Documents without a value for the order attribute come last in both directions (turbopuffer puts them first when ascending). Null handling follows turbopuffer: Eq null matches documents without the attribute, NotEq null those with it, and Lt and Lte also match documents without it.

Responses have rows, aggregations, aggregation_groups, results for multi-query, billing (byte counts are always zero, since WarpSearch bills by its own plans) and performance (server_total_ms, query_execution_ms; cache fields report hot and 1.0).

Coming soon: the Attribute, Saturate, Decay and Dist rank expressions, SparseKNN, Embed in rank_by, compute_attributes (including Highlight), rerank_by with a model, IGlob and NotIGlob, Glob with [...] classes or {a,b} alternatives, and limit.per with several attributes or more than one row per value. They answer 501.

Example

from turbopuffer import Turbopuffer

tpuf = Turbopuffer(
    base_url="https://api.warpsearch.io/compat/turbopuffer",
    api_key="ws_live_...",
)
ns = tpuf.namespace("docs")

ns.write(
    upsert_rows=[
        {"id": 1, "vector": [0.1, 0.9, 0.2], "text": "Fast hybrid search", "category": "guide"},
        {"id": 2, "vector": [0.8, 0.1, 0.3], "text": "Billing and plans", "category": "faq"},
    ],
    distance_metric="cosine_distance",
    schema={"text": {"type": "string", "full_text_search": True}},
)

ann = ns.query(
    rank_by=("vector", "ANN", [0.2, 0.8, 0.1]),
    filters=("category", "Eq", "guide"),
    top_k=10,
    include_attributes=["text"],
)
for row in ann.rows:
    print(row.id, row["$dist"], row["text"])

bm25 = ns.query(rank_by=("text", "BM25", "hybrid search"), top_k=10)

hybrid = ns.multi_query(
    queries=[
        {"rank_by": ("vector", "ANN", [0.2, 0.8, 0.1]), "top_k": 20},
        {"rank_by": ("text", "BM25", "hybrid search"), "top_k": 20},
    ],
    rerank_by=("RRF", {"rank_constant": 60}),
    limit=10,
)
print(hybrid.results[0].rows)

The same calls over HTTP:

curl -s -X POST https://api.warpsearch.io/compat/turbopuffer/v2/namespaces/docs \
  -H "Authorization: Bearer ws_live_..." \
  -H "Content-Type: application/json" \
  -d '{"upsert_rows": [{"id": 1, "vector": [0.1, 0.9, 0.2], "text": "Fast hybrid search"}],
       "distance_metric": "cosine_distance",
       "schema": {"text": {"type": "string", "full_text_search": true}}}'

curl -s -X POST https://api.warpsearch.io/compat/turbopuffer/v2/namespaces/docs/query \
  -H "Authorization: Bearer ws_live_..." \
  -H "Content-Type: application/json" \
  -d '{"rank_by": ["text", "BM25", "hybrid"], "top_k": 10, "include_attributes": ["text"]}'

Differences from turbopuffer

Area WarpSearch
Region None. Remove region and TURBOPUFFER_REGION
Consistency Writes are visible to the next query
BM25 scores Different tokenizer from word_v4, stop words kept, KStem stemming. Same matching rows, different $dist and occasionally a different order for close scores
Vector distances Same as turbopuffer
Multi-query without rerank_by Each subquery runs separately, not on one snapshot
Ordering by attributes Missing values sort last in both directions
Attribute names Letters, digits, _, . and @ only
Conditional writes, patches, delete by filter Not yet, 501
Sparse and multi-vector, embed Not supported
Billing fields Always zero; WarpSearch bills by its own plans

To search the same collection with other interfaces, see Searching, Query languages, Vector search and Hybrid search. For limits and error codes, see Limits and errors.

View this page as Markdown