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 ignoredwhen a region is configured and the base URL has no{region}placeholder. Delete theregionargument and unsetTURBOPUFFER_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_namespaceand the async copy operations, andPATCHon namespace metadata (pinning andread_only). They answer501today.
#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_namespaceandbranch_from_namespace. A request that contains any of them is rejected with501and 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 f32withlate_interaction, sparse vectors ({}f16andsparse_knn),bytes,[N]i8, andembed. They fail with400or501.
#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,DecayandDistrank expressions,SparseKNN,Embedinrank_by,compute_attributes(includingHighlight),rerank_bywith a model,IGlobandNotIGlob,Globwith[...]classes or{a,b}alternatives, andlimit.perwith several attributes or more than one row per value. They answer501.
#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.