From b4609897530c4190e2a98b10f74e249b5a4e16db Mon Sep 17 00:00:00 2001 From: "lancedb-gatefixer[bot]" <313497061+lancedb-gatefixer[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:39:56 -0700 Subject: [PATCH] fix(python): accept automatic fuzziness in MatchQuery (#4467) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MatchQuery(..., fuzziness=None)` raised a Pydantic validation error before a search could run because the field accepted only integers. Allow `Optional[int]`, retain the exact-match default of `0`, and make the documented parameter type explicit. The existing Rust binding already accepts and serializes the optional value. Regression coverage checks construction and JSON serialization for `None`, `0`, `1`, and `2`, plus native indexed searches with the default, explicit exact matching, automatic mode, and one- and two-edit controls. The two `None` regressions reproduced the original constructor error before the fix. Validation against the source-built extension in the locked uv environment: - `uv run --extra tests --extra dev maturin develop --extras tests,dev` - `uv run --no-sync --extra tests --extra dev pytest python/tests/test_fts.py -k 'match_query_fuzziness or fts_query_to_json' -q` — 10 passed. - `uv run --no-sync --extra tests --extra dev pytest python/tests/test_fts.py -q --disable-warnings` — 61 passed. - Locked-environment `ruff check .`, `ruff format .`, and `git diff --check` passed. The reported engine limitation remains observable with pinned Lance `14.0.0-beta.1`: its dataset planner compatibility gate rewrites `None` to zero, so a five-character AUTO query still omits one-edit matches. Automatic expansion for longer terms requires upstream Lance activation. The native acceptance regression uses a two-character term, whose documented automatic distance is zero, while the JSON regression verifies that Python forwards `None` as `null`. This keeps the repair within the issue's requested Python constructor scope. Fixes #4466 Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com> --- python/python/lancedb/query.py | 4 ++-- python/python/tests/test_fts.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/python/python/lancedb/query.py b/python/python/lancedb/query.py index b210c6497..d9755afa1 100644 --- a/python/python/lancedb/query.py +++ b/python/python/lancedb/query.py @@ -479,7 +479,7 @@ class MatchQuery(FullTextQuery): boost : float, default 1.0 The boost factor for the query. The score of each matching document is multiplied by this value. - fuzziness : int, optional + fuzziness : int or None, default 0 The maximum edit distance for each term in the match query. Defaults to 0 (exact match). If None, fuzziness is applied automatically by the rules: @@ -506,7 +506,7 @@ class MatchQuery(FullTextQuery): query: str column: str boost: float = pydantic.Field(1.0, kw_only=True) - fuzziness: int = pydantic.Field(0, kw_only=True) + fuzziness: Optional[int] = pydantic.Field(0, kw_only=True) max_expansions: int = pydantic.Field(50, kw_only=True) operator: FullTextOperator = pydantic.Field(FullTextOperator.OR, kw_only=True) prefix_length: int = pydantic.Field(0, kw_only=True) diff --git a/python/python/tests/test_fts.py b/python/python/tests/test_fts.py index 5107ed445..a867d8405 100644 --- a/python/python/tests/test_fts.py +++ b/python/python/tests/test_fts.py @@ -13,6 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import os import random import shutil @@ -1137,6 +1138,34 @@ def test_fts_lindera_tokenizer( assert [row["text"] for row in results] == ["成田国際空港"] +@pytest.mark.parametrize("fuzziness", [None, 0, 1, 2]) +def test_match_query_fuzziness_to_json(fuzziness): + query = MatchQuery("puppy", "text", fuzziness=fuzziness) + + assert query.fuzziness == fuzziness + assert json.loads(query.to_json())["match"]["fuzziness"] == fuzziness + + +@pytest.mark.parametrize( + "options, expected", + [ + pytest.param({}, ["ab"], id="default-exact"), + pytest.param({"fuzziness": 0}, ["ab"], id="explicit-exact"), + pytest.param({"fuzziness": None}, ["ab"], id="automatic"), + pytest.param({"fuzziness": 1}, ["ab", "ac"], id="one-edit"), + pytest.param({"fuzziness": 2}, ["ab", "ac", "zz"], id="two-edits"), + ], +) +def test_match_query_fuzziness_search(tmp_db, options, expected): + # Automatic fuzziness is zero for two-character terms. + table = tmp_db.create_table("fuzzy", pa.table({"text": ["ab", "ac", "zz"]})) + table.create_index("text", config=FTS(stem=False, remove_stop_words=False)) + + results = table.search(MatchQuery("ab", "text", **options)).to_list() + + assert sorted(row["text"] for row in results) == sorted(expected) + + def test_fts_query_to_json(): """Test that FTS query to_json() produces valid JSON strings with exact format."""