To combine SQL analytics with vector and full-text search, run the search inside the SQL
query instead of beside it. In Infino, vector_search, bm25_search, and hybrid_search
are table functions: each returns its ranked hits as a relation, so the same statement can
join them to other tables, filter them, count them with GROUP BY, and rank within groups
with window functions. There is no second system to query and no stitching of result sets
in application code. The data stays as Parquet files, on local disk or in object storage.
This guide builds two tables from 10,003 real customer questions to an online bank and a
lookup table of which support team owns each topic, then runs five queries that no single
search call can answer. Run the blocks in order in one Python session. Nothing needs an
account, a key, or a server.
The hybrid search on Parquet guide covers the search
calls themselves on the same dataset. This guide is about what SQL adds on top of them.
Do you need a separate vector database for this?
The usual stack keeps vectors in one system, full-text search in a second, and analytics
in a warehouse. A question like “which teams own the requests that sound like this
complaint” then takes three calls and a join in application code, over three copies of the
data kept in sync.
A dedicated vector database is still the right call when the workload is vector serving
alone at a very high sustained query rate, with no keyword relevance and no aggregation.
When the questions mix meaning, exact terms, and counting, running all three in one SQL
engine removes the sync and the stitching. Infino runs search and SQL in your own process
over one copy of the data, with the indexes stored inside the table’s Parquet files.
Build the tables
questions holds the text, its topic, and an embedding, with full-text indexes on both
text columns and a vector index on the embedding. teams is an ordinary table with no
search index, the kind of lookup table an application already has.
Two small helpers keep the queries readable. SQL takes the query embedding as a
comma-separated string literal:
Search as a table: join, count, and group the hits
Each search function returns the table’s _id, its text and scalar columns, and a score,
so a hit can be joined like any row. Which teams own the 200 questions nearest in meaning
to a complaint?
The same shape works on keywords. How many questions mention fees, by team?
The last argument, k, is how many hits the search returns before SQL sees them. An
aggregate should use a k large enough to hold every row it means to count.
Rank within groups, and filter by another table
Window functions work over search results too. The best hybrid match for one question,
per team:
A join can also restrict the ranking to one slice, here the transfers team:
A WHERE clause on a joined table filters after the top k is chosen, so fetch more than
you keep when the filter is selective. For a filter pushed into the vector search itself,
use filter_column on vector_search in the search guide.
Score directions differ by function. BM25 and hybrid scores are higher for a better match,
while a vector score is a distance, lower for a closer match. The
SQL reference lists each one.
Measure where keyword and meaning disagree
Running each retriever on its own and joining the two relations on _id shows what hybrid
fusion hides: how many rows only one retriever finds.
Of 157 distinct questions the two retrievers returned, only 43 came back from both. That
gap is the case for fusing them, and a query like this one is how to watch it per query
class as the data changes.
On latency, the engine’s continuous benchmark measures hybrid_search through SQL at
3.47 ms p50 for 10 rows, on 1M-document tables in object storage with a warm cache. The
numbers and how to reproduce them are on the benchmarks page.
Vector search is approximate nearest-neighbor search, so check recall on your own data as
well as latency. The first query to touch a file on object storage pays a round trip before
its bytes are cached, covered in tradeoffs.
Common questions
Can I run vector search in SQL?
Yes. vector_search('table', 'column', vector, k) is a table function you call in the
FROM clause, with the query embedding passed as a comma-separated string or an array
literal. The result is a relation of the nearest rows with a distance score, which the
rest of the statement can join, filter, and aggregate.
How do I combine full-text and vector search in one SQL query?
Call hybrid_search, which runs BM25 and vector search and fuses the two rankings with
reciprocal rank fusion, or call bm25_search and vector_search separately and join them
on _id to see where they agree.
Does a WHERE clause filter before or after the search?
After. The search returns its top k hits and SQL filters those, so a selective filter
needs a larger k. A text predicate can instead be pushed into the vector search itself
with filter_column, which returns the nearest rows that match rather than a thinned
slice of the global top k.
Do I need to move data out of Parquet to query it this way?
No. Each Infino table is stored as Parquet files with the indexes inside them, on local
disk or in object storage, and SQL runs over those files in your process. Any Parquet
reader can still open the same files for other analytics.
Can I join search results with tables that have no search index?
Yes. A table created with an empty IndexSpec() is queryable by SQL like any other, so
lookup tables, metadata, and ownership tables can sit next to the searchable ones and
join to their hits. Last modified on September 28, 2026