Repository navigation
Description
Bug
The synchronous LanceHybridQueryBuilder (used by Table.search(query_type="hybrid") / .search(...).vector(...).text(...)) silently ignores .offset(). Calling .offset(N) on a hybrid query has no effect on the returned rows — the result is identical to .offset(0).
Root cause
In python/python/lancedb/query.py:
LanceHybridQueryBuilder._create_query_builders()builds the internalself._vector_query/self._fts_querysub-builders and forwardslimit,columns,where, etc., but never forwardsself._offsetto either sub-query.LanceHybridQueryBuilder._combine_hybrid_results()combines and reranks the two result sets and then doesresults.slice(length=limit)— always starting from position 0, regardless of any configured offset.
So self._offset (set via the inherited LanceQueryBuilder.offset()) is set but never read anywhere in the hybrid code path.
Note: the async path (AsyncHybridQuery, backed by the Rust HybridQuery::offset) does forward offset, but applies it independently to each sub-query before combining/reranking, which is a different (and arguably also incorrect) semantics than "skip the first N rows of the final reranked result." That's a separate, Rust-level concern and is out of scope for this issue, which is specifically about the sync Python path silently doing nothing.
Repro
import lancedb
db = lancedb.connect("memory://")
table = db.create_table(
"t",
[
{"text": "a", "vector": [0.1, 0.1]},
{"text": "b", "vector": [2, 2]},
{"text": "cat", "vector": [-0.1, -0.1]},
{"text": "dog", "vector": [0.5, -0.5]},
],
)
table.create_fts_index("text", with_position=False)
full = (
table.search(query_type="hybrid")
.vector([0.0, 0.4])
.text("dog")
.limit(4)
.with_row_id()
.to_arrow()
)
offset_result = (
table.search(query_type="hybrid")
.vector([0.0, 0.4])
.text("dog")
.offset(2)
.limit(2)
.with_row_id()
.to_arrow()
)
# Expected: offset_result == full[2:4]
# Actual: offset_result == full[0:2] (offset is ignored)
print(full["_rowid"].to_pylist())
print(offset_result["_rowid"].to_pylist())Environment
Reproduces on current main (confirmed by diffing the affected file against upstream — the bug is not fork-specific).
Fix
I've opened a PR that:
- Forwards
self._offsetinto the per-sub-query fetch limit (limit + offset), so each sub-query fetches enough rows to cover the skipped prefix. - Applies
offsetwhen slicing the final combined/reranked table (results.slice(offset=offset, length=limit)). - Adds a regression test in
python/python/tests/test_hybrid_query.py.
Activity
fix: hybrid search silently ignores .offset() (#3769)
fix(python): apply offset when combining async hybrid results (#4028)