Skip to content

bug(python): hybrid search silently ignores .offset() #3765

Closed

Description

@Adityaj0

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 internal self._vector_query / self._fts_query sub-builders and forwards limit, columns, where, etc., but never forwards self._offset to either sub-query.
  • LanceHybridQueryBuilder._combine_hybrid_results() combines and reranks the two result sets and then does results.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._offset into the per-sub-query fetch limit (limit + offset), so each sub-query fetches enough rows to cover the skipped prefix.
  • Applies offset when 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

  1. added a commit that references this issue on Aug 17, 2026
    d742b17
  2. added a commit that references this issue on Sep 8, 2026
    577fb48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions