mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-29 14:56:51 +00:00
fix(python): allow phrase queries without a column (#4653)
Quoted async full-text queries without `columns` carry an unset phrase column, but converting them to Python unwrapped it and raised `PanicException`. Python also required `PhraseQuery.column`, preventing callers from constructing the equivalent query object. Make the Python column optional and preserve `Option<String>` in both Rust conversion directions, leaving column selection to query planning. Regression tests cover quoted strings and structured queries with omitted, `None`, and explicit columns, query reuse, JSON serialization, slop, and document granularity. Validation: - Reproduced both missing-column failures before the fix; explicit-column controls passed. - Rebuilt the extension with `uv run --extra tests --extra dev maturin develop --extras tests,dev`. - `cd python && uv run --no-sync --extra tests pytest python/tests/test_fts.py python/tests/test_query.py::test_query_serialization_async -q`: 85 passed. - `cargo fmt --all -- --check` and `ruff check python` pass; touched Python files pass formatting checks. - `cargo clippy --quiet -p lancedb-python --lib --features remote` passes. - Root-wide `ruff check .` reports 49 existing findings in unchanged documentation examples. Fixes #4651 <!-- lance-gatekeeper-fix:v1 agent=62d36fad70c8717236283b8e97dd6b30 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:
co-authored by
Xuanwo
parent
8887c82789
commit
1edfa6077b
@@ -591,8 +591,8 @@ class PhraseQuery(FullTextQuery):
|
||||
----------
|
||||
query : str
|
||||
The query string to match against.
|
||||
column : str
|
||||
The name of the column to match against.
|
||||
column : str, optional
|
||||
The column to match against. None leaves column selection to query planning.
|
||||
slop : int, default 0
|
||||
The maximum number of intervening positions permitted in the phrase.
|
||||
document_granularity : DocumentGranularity, optional
|
||||
@@ -602,7 +602,7 @@ class PhraseQuery(FullTextQuery):
|
||||
"""
|
||||
|
||||
query: str
|
||||
column: str
|
||||
column: Optional[str] = None
|
||||
slop: int = pydantic.Field(0, kw_only=True)
|
||||
document_granularity: Optional[DocumentGranularity] = pydantic.Field(
|
||||
None, kw_only=True
|
||||
|
||||
@@ -584,6 +584,52 @@ async def test_search_fts_phrase_query_async(async_table):
|
||||
assert len(phrase_results) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("columns", [None, ["text"]])
|
||||
async def test_phrase_query_to_query_object_async(tmp_path, columns):
|
||||
db = await ldb.connect_async(tmp_path)
|
||||
table = await db.create_table(
|
||||
"phrases", pa.table({"text": ["puppy runs fast", "cat sleeps"]})
|
||||
)
|
||||
await table.create_index("text", config=FTS(with_position=True))
|
||||
|
||||
query = table.query().nearest_to_text('"puppy runs"', columns=columns)
|
||||
phrase = query.to_query_object().full_text_query.query
|
||||
|
||||
expected = (
|
||||
PhraseQuery("puppy runs", "text") if columns else PhraseQuery("puppy runs")
|
||||
)
|
||||
assert phrase == expected
|
||||
results = await table.query().nearest_to_text(phrase).to_list()
|
||||
assert [row["text"] for row in results] == ["puppy runs fast"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("column", [None, "text"])
|
||||
async def test_phrase_query_object_roundtrip_async(tmp_path, column):
|
||||
db = await ldb.connect_async(tmp_path)
|
||||
table = await db.create_table(
|
||||
"phrases",
|
||||
pa.table({"text": ["puppy runs fast", "puppy often runs", "cat sleeps"]}),
|
||||
)
|
||||
await table.create_index("text", config=FTS(with_position=True))
|
||||
phrase = PhraseQuery(
|
||||
"puppy runs", column, slop=1, document_granularity=DocumentGranularity.ROW
|
||||
)
|
||||
query = table.query().nearest_to_text(phrase)
|
||||
|
||||
assert query.to_query_object().full_text_query.query == phrase
|
||||
serialized = json.loads(phrase.to_json())["phrase"]
|
||||
assert serialized["slop"] == 1
|
||||
assert serialized["document_granularity"] == "row"
|
||||
assert serialized.get("column") == column
|
||||
results = await query.to_list()
|
||||
assert sorted(row["text"] for row in results) == [
|
||||
"puppy often runs",
|
||||
"puppy runs fast",
|
||||
]
|
||||
|
||||
|
||||
def test_search_fts_specify_column(table):
|
||||
table.create_fts_index("text")
|
||||
table.create_fts_index("text2")
|
||||
|
||||
+3
-5
@@ -103,7 +103,7 @@ impl<'a, 'py> FromPyObject<'a, 'py> for PyLanceDB<FtsQuery> {
|
||||
}
|
||||
"PhraseQuery" => {
|
||||
let query = ob.getattr("query")?.extract()?;
|
||||
let column = ob.getattr("column")?.extract()?;
|
||||
let column = ob.getattr("column")?.extract::<Option<String>>()?;
|
||||
let slop = ob.getattr("slop")?.extract()?;
|
||||
let document_granularity = ob
|
||||
.getattr("document_granularity")?
|
||||
@@ -114,9 +114,7 @@ impl<'a, 'py> FromPyObject<'a, 'py> for PyLanceDB<FtsQuery> {
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let mut query = PhraseQuery::new(query)
|
||||
.with_column(Some(column))
|
||||
.with_slop(slop);
|
||||
let mut query = PhraseQuery::new(query).with_column(column).with_slop(slop);
|
||||
if let Some(document_granularity) = document_granularity {
|
||||
query = query.with_document_granularity(document_granularity);
|
||||
}
|
||||
@@ -213,7 +211,7 @@ impl<'py> IntoPyObject<'py> for PyLanceDB<FtsQuery> {
|
||||
}
|
||||
namespace
|
||||
.getattr(intern!(py, "PhraseQuery"))?
|
||||
.call((query.terms, query.column.unwrap()), Some(&kwargs))
|
||||
.call((query.terms, query.column), Some(&kwargs))
|
||||
}
|
||||
FtsQuery::Boost(query) => {
|
||||
let positive = Self(query.positive.as_ref().clone()).into_pyobject(py)?;
|
||||
|
||||
Reference in New Issue
Block a user