Search in Turbopuffer.
Join in SQL.
Write one SQL query over 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.
One question, five queries
A customer writes in: “I can't log in, the password reset email never shows up.” Each query below finds the helpdesk articles that answer it. Everything shown is recorded from a real run against a live Turbopuffer namespace: the plan before and after optimization, the request Turbopuffer received, and the rows it returned.
The ranking, sort and limit all fold into the scan. Turbopuffer runs one nearest-neighbor search and returns five rows.
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
| 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 |
How pushdown works
A vector index is fast at one thing: finding the few best matches under a filter. Pushdown hands it exactly that job and keeps everything else in SQL, so a query never pulls the whole index across the network.
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.
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.
-- The ranking is selected as __score__ and ordered DESC with a LIMIT:
-- that is the shape the optimizer folds into one Turbopuffer search.
SELECT id, title, product,
tpuf_ann(vector, CAST(:question_embedding AS FLOAT[256])) AS __score__
FROM helpdesk.articles
WHERE product = 'auth' AND audience = 'customer'
ORDER BY __score__ DESC
LIMIT 3Writing queries that push down
Three rules cover every query on this page.
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.