mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
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:
@@ -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()
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user