diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index c42344ad3..db213ac71 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -2296,7 +2296,7 @@ class LanceHybridQueryBuilder(LanceQueryBuilder): norm=self._norm, fts_query=self._fts_query._query, reranker=self._reranker, - limit=self._limit, + limit=self._limit or DEFAULT_HYBRID_LIMIT, with_row_ids=True, offset=self._offset, ) @@ -2748,13 +2748,13 @@ class LanceHybridQueryBuilder(LanceQueryBuilder): ) # Apply common configurations - if self._limit: - # The final offset/limit window is sliced out of the combined, - # reranked results, so each sub-query must fetch enough rows to - # cover the skipped prefix as well as the window itself. - sub_query_limit = self._limit + (self._offset or 0) - self._vector_query.limit(sub_query_limit) - self._fts_query.limit(sub_query_limit) + # The final offset/limit window is sliced out of the combined, + # reranked results, so each sub-query must fetch enough rows to + # cover the skipped prefix as well as the window itself. + limit = self._limit or DEFAULT_HYBRID_LIMIT + sub_query_limit = limit + (self._offset or 0) + self._vector_query.limit(sub_query_limit) + self._fts_query.limit(sub_query_limit) # WAL-PK-FUSION: without the fallback, select `self._columns` as is. self._pk_fusion = None columns = self._columns diff --git a/python/python/tests/test_hybrid_query.py b/python/python/tests/test_hybrid_query.py index 61b8001cf..c5f58308d 100644 --- a/python/python/tests/test_hybrid_query.py +++ b/python/python/tests/test_hybrid_query.py @@ -315,6 +315,33 @@ def test_hybrid_query_offset(sync_table: Table): assert offset_result["_rowid"].to_pylist() == full["_rowid"].to_pylist()[2:] +def test_hybrid_query_default_limit(sync_table: Table): + # The vector and FTS legs match disjoint rows, so fusing their results + # yields more rows than the default limit. + new_rows = [] + for i in range(20): + new_rows.append({"text": "close_vec", "vector": [0.1, 0.1]}) + new_rows.append({"text": "dog", "vector": [50.0 + i, 50.0 + i]}) + sync_table.add(new_rows) + + def query(): + return ( + sync_table.search(query_type="hybrid") + .vector([0.1, 0.1]) + .text("dog") + .with_row_id(True) + ) + + # Like the async hybrid query, the default limit is 10. + result = query().to_arrow() + assert len(result) == 10 + + # The offset window is taken after the default limit is applied. + full = query().limit(15).to_arrow() + offset_result = query().offset(5).to_arrow() + assert offset_result["_rowid"].to_pylist() == full["_rowid"].to_pylist()[5:] + + def test_hybrid_query_minimum_nprobes_zero_raises(sync_table: Table): # minimum_nprobes(0) must raise the same validation error a plain vector # query raises, not silently no-op because 0 is falsy.