Skip to main content
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. 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