From 7fca4d666adf1d5f522ce8ac422117821da195ca Mon Sep 17 00:00:00 2001 From: "lancedb-gatefixer[bot]" <313497061+lancedb-gatefixer[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:32:32 +0800 Subject: [PATCH] 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 Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com> Co-authored-by: Xuanwo --- python/python/lancedb/query.py | 2 ++ python/python/tests/test_query.py | 50 +++++++++++++++++++++++++++++++ python/src/query.rs | 2 +- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index 66aae06a6..644b6571f 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -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. diff --git a/python/python/tests/test_query.py b/python/python/tests/test_query.py index cef985aa6..3d7c01fc5 100644 --- a/python/python/tests/test_query.py +++ b/python/python/tests/test_query.py @@ -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 diff --git a/python/src/query.rs b/python/src/query.rs index bf267443c..c2bd6afb3 100644 --- a/python/src/query.rs +++ b/python/src/query.rs @@ -377,7 +377,7 @@ impl From 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