Search in Turbopuffer.
Join in SQL.
This demo shows one SQL query searching your vector index. Chalk pushes the ranking, the filters and the limit into Turbopuffer as a single search, then joins the few rows that come back with everything else your query touches.
See which SQL work moves into the search request
These are recorded queries against the same helpdesk search namespace.
Try this experiment · expected results
1Compare Retrieve and Filter
Inspect the SQL, outgoing request, and optimized plan for the first two tabs.
Expected: Supported ranking, filtering, and limits appear in the Turbopuffer request.
2Compare filtering after retrieval
Open Retrieve, then filter and Federated join, then read the remaining Chalk plan and results.
Expected: Work left above the scan runs in Chalk; moving a filter across retrieval can change the query’s meaning.
The ranking, sort and limit all fold into the scan. Turbopuffer runs one nearest-neighbor search and returns five rows.
| id | title | product | distance |
|---|---|---|---|
| 1 | Reset a forgotten password | auth | 0.240 |
| 2 | Password reset email never arrived | auth | 0.314 |
| 5 | Locked out after too many sign-in attempts | auth | 0.425 |
| 6 | Recover an account without access to two-factor | auth | 0.553 |
| 23 | Mobile app keeps logging me out | auth | 0.586 |
SELECT id, title, product,
tpuf_ann(vector, CAST([<embedding of the question>] AS FLOAT[256])) AS __score__
FROM helpdesk.articles
ORDER BY __score__ DESC
LIMIT 5{
"rank_by": ["vector", "ANN", "<256 floats>"],
"top_k": 5,
"include_attributes": ["audience", "body", "product", "title", "updated_at", "views"]
}- Limit 5pushed down
- └Project id, title, product, __score__
- └Sort __score__ DESCpushed down
- └Project tpuf_ann(vector, q) AS __score__pushed down
- └Turbopuffer scan · helpdesk.articles
- Project id, title, product, __score__
- └Turbopuffer scan · helpdesk.articles
Built on Chalk
ChalkSQL reads the source through its connector, pushes supported work to that source, and runs the remaining SQL over the returned rows.
A namespace is a table
Register a Turbopuffer namespace with ChalkSQL and it becomes helpdesk.articles: its id, its vector, a __score__ column, and every attribute as a typed SQL column.
The optimizer pushes work down
Before the query runs, the planner folds the ranking, the filters Turbopuffer understands, the sort and the LIMIT into the scan, and turns them into one Turbopuffer query.
SQL does the rest
Whatever Turbopuffer cannot do, ChalkSQL does on the handful of rows that come back: predicates it has no operator for, joins against other sources, aggregates.
Join retrieval with other sources
ChalkSQL combines the retrieved articles with tables from the rest of the catalog. The recorded federated-join query shows the work left in Chalk after retrieval.
A namespace, a query, one request
Register the namespace once, with the attributes you want as SQL columns. From then on it is a table like any other: query it, filter it, and join it with your features, your warehouse or a list of support queues.
The third tab is the whole of what crosses the network: one search, three filters, and a top_k of 3.
from libchalk.chalksql import (
RustTurbopufferQueryClient, TurbopufferNamespaceRegistration, TurbopufferSchemaProvider,
)
registry.register_turbopuffer_query_client(
RustTurbopufferQueryClient.create(api_key=TURBOPUFFER_API_KEY, region="gcp-us-central1")
)
articles = TurbopufferNamespaceRegistration(
name="articles",
namespace_name="chalk-example-helpdesk",
backend=RustTurbopufferQueryClientBindInfo.create(),
executor_bind_info=executor,
key_dtype=pa.int64(),
vector_dtype=pa.list_(pa.float32(), 256),
attribute_schema={"title": pa.large_utf8(), "product": pa.large_utf8(), ...},
fts_attributes=["title", "body"],
)
catalog.register_schema_provider(
"helpdesk", TurbopufferSchemaProvider(registrations=[articles], executor_bind_info=executor)
)Implementation details
How the example works
A namespace is a table
Register a Turbopuffer namespace with ChalkSQL and it becomes helpdesk.articles: its id, its vector, a __score__ column, and every attribute as a typed SQL column.
The optimizer pushes work down
Before the query runs, the planner folds the ranking, the filters Turbopuffer understands, the sort and the LIMIT into the scan, and turns them into one Turbopuffer query.
SQL does the rest
Whatever Turbopuffer cannot do, ChalkSQL does on the handful of rows that come back: predicates it has no operator for, joins against other sources, aggregates.
SQL behavior and pushdown rules
Rank, then order and limit
Select the ranking as __score__, order by __score__ DESC, and add a LIMIT. That becomes rank_by and top_k. For vector search the score is a cosine distance, so the best match has the smallest value.
Filters that push down
=, <>, <, <=, >, >= against a typed literal, NOT, LIKE and starts_with (as Turbopuffer glob filters), combined with AND. They join the same request as the ranking.
Everything else, after retrieval
Retrieve the top candidates in a subquery and filter or join them in the outer query. Turbopuffer still does the search; ChalkSQL only touches the rows it returned.
Query every source as one
Vector indexes, warehouses, databases and features, in one SQL query that sends each source only the work it does best. Talk to an engineer about your retrieval stack.