skip to content

How do you add full-text search to a LanceDB table and combine it with vector search?

level: seniorimportance: should knowfreq 44%

answer

  1. Keyword index lives in the same table
  2. Build it before text queries match
  3. One query mode blends both legs
  4. Two scores that share no scale
  5. Rank-based fusion is the safe default

basics

~20 s

Build an inverted index over a text column with create_fts_index, then query it with search(text, query_type="fts"). Passing query_type="hybrid" instead runs the keyword and vector legs together and fuses their two ranked lists through a reranker.

solid answer

~50 s

`tbl.create_fts_index("text")` builds a full-text index over a string column; after that, `tbl.search("quarterly revenue", query_type="fts")` does keyword retrieval on the same table that holds your vectors — no second system. `query_type="hybrid"` runs both legs and merges them, which needs two things in place: a way to turn the query string into a vector (an embedding function registered on the table's schema, or supplying the vector yourself) and a reranker to combine the results. The default is reciprocal rank fusion, which merges on **rank** and so sidesteps the fact that a BM25-style keyword score and a cosine distance are not on the same scale; you can swap in another reranker with `.rerank(...)`. The operational gotcha is freshness: the FTS index covers the rows present when it was built, so newly ingested rows need the index updated before keyword search will surface them.

code

python · 13 lines
python
tbl.create_fts_index("text", replace=True)

keyword = (
    tbl.search("ERR_4021 timeout", query_type="fts")
    .limit(10)
    .to_pandas()
)

blended = (
    tbl.search("why does the upload time out", query_type="hybrid")
    .limit(10)
    .to_pandas()
)

go deeper

for a junior

Know that keyword search needs create_fts_index on a text column first, and that search accepts a query_type argument selecting vector, full-text or hybrid retrieval.

for a middle

Explain what hybrid needs to run — a way to embed the query string and a reranker to merge two result lists — and why the two legs' scores cannot simply be added together.

for a senior

Own the failure modes: stale FTS index after ingest, hybrid silently degrading to one usable leg, and reranker cost. Justify hybrid with measurements on labelled queries instead of enabling it by reflex.

for a principal

Treat retrieval quality as a measured product target with an owned evaluation set, and weigh keeping keyword search inside the same embedded table against the operational reality of a separate search engine your organization may already run.

## Why this exists on an embedded database The pitch is that vectors already live beside the rest of your columns in a Lance table, so keyword search should not require standing up a second engine and keeping two copies of the corpus in sync. `create_fts_index` builds an inverted index over one or more string columns of the same table, and the query builder gains a text mode. ## The three query modes `table.search(...)` behaves differently depending on what you pass and what you declare: - A vector, or `query_type="vector"` — nearest-neighbour search over the vector column. - A string with `query_type="fts"` — keyword retrieval against the full-text index. Matching is lexical: tokenized terms, with scoring that rewards rare terms and penalizes long documents. - A string with `query_type="hybrid"` — both legs run, and their results are fused. Everything downstream is unchanged: `.limit()`, `.where()`, `.select()` and the terminal methods work the same way, and hybrid results carry a fused relevance score rather than a raw distance. ## What hybrid needs before it works Hybrid mode has to produce a vector from your query string. That means either an embedding function is registered on the table's schema — so the client can embed the query with the same model used at ingest — or you supply the query vector yourself alongside the text. A hybrid query on a table with bring-your-own vectors and no registered embedding function has no way to build the dense leg, and that is the single most common first-attempt failure. The second requirement is a reranker: two ranked lists have to become one. ## Fusion and why rank-based fusion is the default The two legs produce numbers that are not comparable. A lexical score is unbounded and depends on corpus statistics; a cosine distance sits in a fixed range and is smaller-is-better. Adding or averaging them directly is meaningless, and normalizing them per query is fragile because the score distributions shift with the query. Reciprocal rank fusion avoids the problem by discarding the magnitudes entirely and combining positions instead: a document ranked highly by either leg scores well, and a document ranked decently by both wins over one that is first in a single leg. That robustness is why it is the default. LanceDB ships alternatives — a linear combination that does weight the two score sources, and a cross-encoder reranker that rescores the merged candidate set with a model that reads query and document together — selected via `.rerank(...)`. The cross-encoder is far more accurate and far slower, so it belongs on a small merged candidate list, not on hundreds of rows. The choice between fusion strategies as a retrieval technique is a broader retrieval-design question; what matters at the product level is knowing which knob LanceDB exposes and what the default does. ## The freshness trap A vector search on a partially indexed table still returns complete results, because the engine scans the unindexed fragments as well. Full-text search is less forgiving in practice: rows added after the FTS index was built are not matched until the index is updated. The symptom is brutal to debug — a document is plainly in the table, `where` finds it, and keyword search does not — and it is worse in hybrid mode, where the dense leg does find the row so results look merely mediocre rather than broken. The fix is maintenance: refresh the index after ingest, either by running the table's optimize step or by rebuilding the FTS index with replace enabled. Put it in the ingest pipeline rather than the runbook. ## When hybrid earns its cost Dense retrieval is strong on paraphrase and weak on exact tokens — part numbers, error codes, surnames, acronyms — because those carry little semantic signal and embedding models routinely place them near unrelated text. Keyword retrieval is the mirror image. Hybrid is worth its extra latency exactly where queries mix both, which is most real search over technical or catalogue content. For a corpus of prose answered by conceptual questions, the dense leg alone is often as good and half the cost. Decide by measuring both against labelled queries rather than by defaulting hybrid on. ## Operational notes Building the FTS index costs a pass over the text column and adds storage proportional to vocabulary and document count. Keep it scoped to the columns you actually search — indexing every string column inflates build time and footprint for no retrieval benefit.

  • Why is rank-based fusion a safer default than adding the two scores together?
    Because the two scores share no scale. A lexical score is unbounded and depends on corpus statistics like term rarity and document length; a cosine distance is bounded and smaller-is-better. Summing them lets whichever has the larger dynamic range dominate, and per-query normalization is unstable because the distributions move with the query. Fusing on rank ignores magnitudes and rewards documents both legs liked.
  • A document is visible via a where clause but never matched by an fts query. What do you check?
    Index freshness first. The full-text index covers rows present when it was built, so anything ingested afterwards will not match until the index is updated — run the table's optimize step or rebuild the FTS index with replace. Then check that the column you indexed is the one you expect and that the query terms actually appear in it, since matching is lexical rather than semantic.
  • When would you skip hybrid and stay with pure vector search?
    When queries are conceptual and the corpus is prose, dense retrieval alone usually matches hybrid quality at lower latency and without an FTS index to build and maintain. Hybrid earns its cost when queries carry exact tokens — error codes, SKUs, proper nouns — that embeddings represent poorly. Decide with a labelled query set rather than switching it on by default.

saying these in an interview costs you the question

  • Assuming full-text search works without building an index
  • Expecting hybrid to work with no way to embed the query
  • Summing keyword and vector scores as if comparable
  • Forgetting to refresh the FTS index after ingest
  • Running a cross-encoder reranker over hundreds of candidates

context