fix(python)!: use null for missing linear combination scores

When `LinearCombinationReranker` uses `return_score="all"` and either the
vector or full-text side has no rows, the unavailable score column is now a
typed Arrow `float32` null column instead of a column of NaNs. Existing scores
from the non-empty side and their descending `_relevance_score` ordering are
preserved.

Regression coverage exercises both missing sides through the hybrid query path.
It checks the missing column's `float32` type, null count and row count, exact
preservation of the original present-side values, and descending relevance
ordering. The existing empty-result, score-column override, and `return_all`
cases remain covered.

The exact head `db33a068263cd69ee578f95a375f0060dcbb4e21` passed all 15
applicable check runs and all 4 applicable workflow runs, with no commit
statuses. The hosted macOS Python run completed with 3019 passed, 126 skipped,
and 70 deselected; the two new cases and nine adjacent regression cases passed.
The retained CI collector reached the all-success snapshot but exited 1 while
writing its final metadata because of a harness `TypeError`; it did not rerun
or invalidate the product or CI results.

BREAKING CHANGE: For `return_score="all"` with an empty vector or full-text
side, the missing side's score column now contains typed nulls instead of NaN.
Consumers that distinguish these values will observe different results.
This commit is contained in:
SHAIK SHIVAJI
2026-11-02 18:30:42 +08:00
committed by GitHub
parent 9af3bac6e4
commit 1a037c0315
2 changed files with 48 additions and 3 deletions
@@ -3,7 +3,6 @@
from collections import defaultdict
from numpy import nan
import pyarrow as pa
from .base import Reranker
@@ -66,7 +65,7 @@ class LinearCombinationReranker(Reranker):
elif self.score == "all":
results = results.append_column(
"_distance",
pa.array([nan] * len(fts_results), type=pa.float32()),
pa.nulls(len(fts_results), type=pa.float32()),
)
return results
@@ -87,7 +86,7 @@ class LinearCombinationReranker(Reranker):
elif self.score == "all":
results = results.append_column(
"_score",
pa.array([nan] * len(vector_results), type=pa.float32()),
pa.nulls(len(vector_results), type=pa.float32()),
)
return results
results = defaultdict()
+46
View File
@@ -1150,6 +1150,52 @@ def test_linear_combination_missing_fts_is_penalised():
)
@pytest.mark.parametrize("empty_side", ["vector", "fts"])
def test_linear_combination_empty_side_scores_are_null(empty_side):
"""With return_score="all", the missing side's score column is null, not NaN."""
from lancedb.query import LanceHybridQueryBuilder
vector_results = pa.table(
{
"_rowid": pa.array([1, 2], pa.uint64()),
"_distance": pa.array([0.1, 0.2], pa.float32()),
}
)
fts_results = pa.table(
{
"_rowid": pa.array([1, 2], pa.uint64()),
"_score": pa.array([5.0, 4.0], pa.float32()),
}
)
if empty_side == "vector":
expected_present = fts_results["_score"].to_pylist()
vector_results = vector_results.slice(0, 0)
missing = "_distance"
present = "_score"
else:
expected_present = vector_results["_distance"].to_pylist()
fts_results = fts_results.slice(0, 0)
missing = "_score"
present = "_distance"
result = LanceHybridQueryBuilder._combine_hybrid_results(
fts_results=fts_results,
vector_results=vector_results,
norm="score",
fts_query="query",
reranker=LinearCombinationReranker(return_score="all"),
limit=10,
with_row_ids=True,
)
assert result[missing].type == pa.float32()
assert result[missing].null_count == len(result) == 2
assert result[present].to_pylist() == expected_present
assert result["_relevance_score"].to_pylist() == sorted(
result["_relevance_score"].to_pylist(), reverse=True
)
@pytest.mark.skipif(
os.environ.get("WATSONX_API_KEY") is None
or (