Hybrid Query Builder#
LanceHybridQueryBuilder (defined at query.py:2093) is the sync Python query builder that executes vector and full-text search (FTS) sub-queries in parallel, then combines and reranks their results. It subclasses LanceQueryBuilder and stores all configuration on the instance until execution time.
Key entry points:
__init__β stores query text/vector, column names, and initializes all optional numeric parameters toNoneto_arrowβ executes the query; calls_create_query_builders(), dispatches both sub-queries via aThreadPoolExecutor, then calls_combine_hybrid_resultsand_finish_hybrid_results_create_query_buildersβ the central method that instantiatesLanceFtsQueryBuilderandLanceVectorQueryBuilder, then forwards all stored parameters to them
Default reranker is RRFReranker (reciprocal rank fusion), set lazily inside _create_query_builders if none has been provided .
Parameter Forwarding in _create_query_builders#
_create_query_builders forwards parameters to both sub-builders after instantiating them. The forwarding block (lines 2610β2640) handles:
| Parameter | Setter(s) | Forwarded to |
|---|---|---|
_limit | .limit() | vector + FTS |
_columns | .select() | vector + FTS |
_where | .where() | vector + FTS |
_with_row_id | .with_row_id() | vector + FTS |
_phrase_query | .phrase_query() | FTS only |
_distance_type | .metric() | vector only |
_minimum_nprobes | .minimum_nprobes() / .nprobes() | vector only |
_maximum_nprobes | .maximum_nprobes() / .nprobes() | vector only |
_refine_factor | .refine_factor() | vector only |
_ef | .ef() | vector only |
_lower_bound / _upper_bound | .distance_range() | vector only |
The .nprobes(n) setter is a shorthand that sets both _minimum_nprobes and _maximum_nprobes to the same value .
Known Pitfall: Truthiness Checks on Numeric Parameters#
Several guards in _create_query_builders use bare truthiness checks (if self._param:) instead of explicit None checks. This causes any parameter legitimately set to 0 or 0.0 to be silently dropped β treated as if it were never set.
Affected lines :
| Line | Check | Problem |
|---|---|---|
| 2627 | if self._minimum_nprobes: | 0 is dropped |
| 2631 | if self._refine_factor: | 0 is dropped |
| 2633 | if self._ef: | 0 is dropped |
| 2637 | if self._lower_bound or self._upper_bound: | 0.0 for either bound is dropped |
Correctly guarded :
| Line | Check |
|---|---|
| 2629 | if self._maximum_nprobes is not None: β |
The distance_range / upper_bound=0.0 Bug#
The most impactful manifestation is GitHub Issue #3651: calling .distance_range(upper_bound=0.0) on a hybrid search silently drops the constraint. With L2 distance, an upper bound of 0.0 means "only return vectors with zero distance" β an intentional, restrictive filter. Because the guard at line 2637 evaluates 0.0 as falsy, the range is never forwarded to the vector sub-query, and the full result set is returned instead .
Workaround (until fixed): There is no clean workaround via the public API. For the distance_range case specifically, consider running separate vector and FTS queries and merging manually if a zero bound is required.
Fix Pattern#
All affected guards should use is not None:
# Before (buggy)
if self._lower_bound or self._upper_bound:
# After (correct)
if self._lower_bound is not None or self._upper_bound is not None:
This is the same pattern already used correctly for _maximum_nprobes .
Async path is unaffected. The async
LanceHybridQueryBuilderfollows a different forwarding path and does not share this bug .
Related History & References#
- PR #2356 introduced distance-range forwarding to the hybrid builder, including the original
if self._lower_bound or self._upper_bound:guard that contains the zero-value bug . - PR #2360 refactored all parameter forwarding into the centralized
_create_query_builders()method , which is the current shape of the code. - PR #3096 fixed an unrelated but structurally similar issue where
self._postfilterwas passed without negation, showing that silent logic errors in this forwarding block have recurred . - Issue #3651 (open as of 2026-07-13) is the active bug report for the
upper_bound=0.0case .
Primary source: python/python/lancedb/query.py