fix(python): preserve async query search options during serialization (#4659)

Async `to_query_object()` dropped enabled `fast_search` on every query
type and `postfilter` on plain and full-text queries. Re-executing a
serialized fast search could therefore return unindexed rows.

Copy enabled `fast_search` into the Python `Query` and serialize enabled
plain/FTS postfilter from the Rust request's prefilter setting. Preserve
the existing unset defaults: `None` for these options, with the vector
query's existing `False` postfilter default.

Validation:

- Added 12 serialization cases across plain, FTS, and vector queries,
covering both enabled and unset options, plus two FTS execution
round-trip cases. Before the fix, nine cases failed, including returning
an unindexed row after serialization.
- Rebuilt the Python extension using the uv-managed environment.
- Query tests and existing FTS fast-search tests: 192 passed, 1 slow
test deselected.
- Rust formatting and Python package lint/format checks pass.
Repository-wide `ruff check .` reports 49 pre-existing errors in
unchanged `docs/web-tests` files.

Fixes #4658

<!-- lance-gatekeeper-fix:v1 agent=f828b24871a2dc928668be2de4f85078
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
This commit is contained in:
lancedb-gatefixer[bot]
2026-10-27 16:32:32 +08:00
committed by GitHub
co-authored by Xuanwo
parent 1edfa6077b
commit 7fca4d666a
3 changed files with 53 additions and 1 deletions
+2
View File
@@ -910,6 +910,8 @@ class Query(pydantic.BaseModel):
query.filter = req.filter
query.full_text_query = req.full_text_search
query.columns = req.select
# Keep the unset value for this opt-in flag, as in the sync builders.
query.fast_search = req.fast_search or None
query.with_row_id = req.with_row_id
# use_rte is a genuine tri-state (None / True / False); preserve it as-is
# so a round-tripped query keeps an explicit False.
+50
View File
@@ -1977,6 +1977,56 @@ def test_query_serialization_sync(table: lancedb.table.Table):
check_set_props(q, full_text_query=FullTextSearchQuery(columns=[], query="foo"))
@pytest.mark.asyncio
@pytest.mark.parametrize("query_type", ["plain", "fts", "vector"])
@pytest.mark.parametrize("fast_search", [False, True])
@pytest.mark.parametrize("postfilter", [False, True])
async def test_query_serialization_async_search_options(
table_async: AsyncTable, query_type, fast_search, postfilter
):
query = table_async.query().where("id = 1")
if query_type == "fts":
query = query.nearest_to_text(MatchQuery("dog", "text"))
elif query_type == "vector":
query = query.nearest_to([1.0, 2.0])
if fast_search:
query = query.fast_search()
if postfilter:
query = query.postfilter()
serialized = query.to_query_object()
assert serialized.fast_search is (True if fast_search else None)
default_postfilter = False if query_type == "vector" else None
assert serialized.postfilter is (True if postfilter else default_postfilter)
@pytest.mark.asyncio
@pytest.mark.parametrize("fast_search", [False, True])
async def test_query_serialization_async_fast_search_execution(
table_async, fast_search
):
await table_async.create_index("text", config=FTS())
await table_async.add(
[
{
"id": 3,
"vector": [5.0, 6.0],
"str_field": "c",
"float_field": 3.0,
"text": "unindexed dog",
}
]
)
query = table_async.query().nearest_to_text(MatchQuery("unindexed", "text"))
if fast_search:
query = query.fast_search()
expected_ids = [] if fast_search else [3]
assert [row["id"] for row in await query.to_list()] == expected_ids
restored = table_async._sync_query_to_async(query.to_query_object())
assert [row["id"] for row in await restored.to_list()] == expected_ids
@pytest.mark.asyncio
async def test_query_serialization_async(table_async: AsyncTable):
# Simple queries
+1 -1
View File
@@ -377,7 +377,7 @@ impl From<AnyQuery> for PyQueryRequest {
refine_factor: None,
distance_type: None,
bypass_vector_index: None,
postfilter: None,
postfilter: (!query_request.prefilter).then_some(true),
norm: None,
order_by: query_request
.order_by