mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-13 23:32:27 +00:00
Lance supports `search_effort`, but LanceDB cannot pass it through its query APIs. Expose the parameter in Rust, Python sync/async and TypeScript, including remote requests and hybrid query conversion, using the [Lance v14.0.0-beta.13](https://github.com/lance-format/lance/releases/tag/v14.0.0-beta.13) dependency already on main. The default is `None` (Auto, internally 0.5). Any explicit effort, including 0.5, overrides fixed `nprobes` in either setter order; explicit minimum/maximum bounds remain effective. Invalid values are rejected in the Rust core. Hybrid conversion now retains the vector probe parameters and handles an inferred FTS column without panicking. Namespace queries with explicit effort execute locally because the namespace wire model cannot carry it. MemWAL LSM queries reject it explicitly because Lance's LSM scanner does not yet expose this option. Maturin is included in the dev extra so the documented build command works in the project environment. BREAKING CHANGE: The public Rust `VectorQueryRequest` struct now includes `search_effort: Option<f64>`. Downstream code that initializes every field explicitly must add `search_effort: None` or use `..Default::default()`. Exhaustive destructuring patterns must include the new field or `..`. Query-builder calls and struct literals that already use `..Default::default()` require no source changes. The new effort setters in Python and TypeScript are additive. This PR does not change the persisted file format. Using explicit effort with remote tables requires server support for `search_effort` and its precedence over `nprobes`; the client wire tests do not establish that a deployed server supports it. Server-side support is tracked in lancedb/sophon#8607. Validation on AWS after merging current main (Lance beta.13 / SDK beta.5): - 14 Rust regressions and the API doctest pass. - 60 parameter regressions, the full 47-test namespace suite, and three API doctests pass. The parameter selection includes some namespace cases. - 40 TypeScript query tests and 54 remote tests pass; build, lint and generated docs pass. - Rust Clippy, Python package lint/format, Pyright and lockfile checks pass. Repository-wide Ruff reports 49 lint errors and 11 formatting failures in unchanged `docs/web-tests` files; those files are byte-identical to main and outside this change. All 1,103 Git-tracked files match the AWS validation checkout. No benchmark scripts or result artifacts are included, and no performance improvement is claimed. Uses lance-format/lance#9798. Server-side support is in lancedb/sophon#8607.
1285 lines
45 KiB
Rust
1285 lines
45 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use crate::expr::PyExpr;
|
|
use crate::runtime::future_into_py;
|
|
use crate::util::parse_distance_type;
|
|
use crate::{arrow::RecordBatchStream, util::PyLanceDB};
|
|
use crate::{error::PythonErrorExt, index::class_name};
|
|
use arrow::array::Array;
|
|
use arrow::array::ArrayData;
|
|
use arrow::array::make_array;
|
|
use arrow::pyarrow::FromPyArrow;
|
|
use arrow::pyarrow::IntoPyArrow;
|
|
use arrow::pyarrow::ToPyArrow;
|
|
use lancedb::index::scalar::{
|
|
BooleanQuery, BoostQuery, DocumentGranularity, FtsQuery, FullTextSearchQuery, MatchQuery,
|
|
MultiMatchQuery, Occur, Operator, PhraseQuery,
|
|
};
|
|
use lancedb::query::AnalyzePlanDistributedMetrics;
|
|
use lancedb::query::QueryBase;
|
|
use lancedb::query::QueryExecutionOptions;
|
|
use lancedb::query::QueryFilter;
|
|
use lancedb::query::{
|
|
ColumnOrdering, ExecutableQuery, Query as LanceDbQuery, Select, TakeQuery as LanceDbTakeQuery,
|
|
VectorQuery as LanceDbVectorQuery,
|
|
};
|
|
use lancedb::table::AnyQuery;
|
|
use pyo3::Bound;
|
|
use pyo3::IntoPyObject;
|
|
use pyo3::PyAny;
|
|
use pyo3::PyRef;
|
|
use pyo3::PyResult;
|
|
use pyo3::Python;
|
|
use pyo3::prelude::{PyAnyMethods, PyDictMethods};
|
|
use pyo3::pyfunction;
|
|
use pyo3::pymethods;
|
|
use pyo3::types::PyList;
|
|
use pyo3::types::{PyDict, PyString};
|
|
use pyo3::{Borrowed, FromPyObject, exceptions::PyRuntimeError};
|
|
use pyo3::{PyErr, pyclass};
|
|
use pyo3::{exceptions::PyValueError, intern};
|
|
|
|
fn analyze_plan_options(distributed_metrics: Option<&str>) -> PyResult<QueryExecutionOptions> {
|
|
let analyze_plan_distributed_metrics = match distributed_metrics.unwrap_or("aggregate") {
|
|
"aggregate" => AnalyzePlanDistributedMetrics::Aggregate,
|
|
"per_worker" => AnalyzePlanDistributedMetrics::PerWorker,
|
|
"full" => AnalyzePlanDistributedMetrics::Full,
|
|
mode => {
|
|
return Err(PyValueError::new_err(format!(
|
|
"Invalid distributed_metrics value '{}'. Expected one of: \
|
|
'aggregate', 'per_worker', 'full'",
|
|
mode
|
|
)));
|
|
}
|
|
};
|
|
|
|
let mut options = QueryExecutionOptions::default();
|
|
options.analyze_plan_distributed_metrics = analyze_plan_distributed_metrics;
|
|
Ok(options)
|
|
}
|
|
|
|
impl<'a, 'py> FromPyObject<'a, 'py> for PyLanceDB<FtsQuery> {
|
|
type Error = PyErr;
|
|
|
|
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
|
|
let ob = ob.to_owned();
|
|
match class_name(&ob)?.as_str() {
|
|
"MatchQuery" => {
|
|
let query = ob.getattr("query")?.extract()?;
|
|
let column = ob.getattr("column")?.extract::<Option<String>>()?;
|
|
let boost = ob.getattr("boost")?.extract()?;
|
|
let fuzziness = ob.getattr("fuzziness")?.extract()?;
|
|
let max_expansions = ob.getattr("max_expansions")?.extract()?;
|
|
let operator = ob.getattr("operator")?.extract::<String>()?;
|
|
let prefix_length = ob.getattr("prefix_length")?.extract()?;
|
|
let document_granularity = ob
|
|
.getattr("document_granularity")?
|
|
.extract::<Option<String>>()?
|
|
.map(|value| {
|
|
DocumentGranularity::try_from(value.as_str())
|
|
.map_err(|err| PyValueError::new_err(err.to_string()))
|
|
})
|
|
.transpose()?;
|
|
|
|
let mut query =
|
|
MatchQuery::new(query)
|
|
.with_column(column)
|
|
.with_boost(boost)
|
|
.with_fuzziness(fuzziness)
|
|
.with_max_expansions(max_expansions)
|
|
.with_operator(Operator::try_from(operator.as_str()).map_err(|e| {
|
|
PyValueError::new_err(format!("Invalid operator: {}", e))
|
|
})?)
|
|
.with_prefix_length(prefix_length);
|
|
if let Some(document_granularity) = document_granularity {
|
|
query = query.with_document_granularity(document_granularity);
|
|
}
|
|
Ok(Self(query.into()))
|
|
}
|
|
"PhraseQuery" => {
|
|
let query = ob.getattr("query")?.extract()?;
|
|
let column = ob.getattr("column")?.extract()?;
|
|
let slop = ob.getattr("slop")?.extract()?;
|
|
let document_granularity = ob
|
|
.getattr("document_granularity")?
|
|
.extract::<Option<String>>()?
|
|
.map(|value| {
|
|
DocumentGranularity::try_from(value.as_str())
|
|
.map_err(|err| PyValueError::new_err(err.to_string()))
|
|
})
|
|
.transpose()?;
|
|
|
|
let mut query = PhraseQuery::new(query)
|
|
.with_column(Some(column))
|
|
.with_slop(slop);
|
|
if let Some(document_granularity) = document_granularity {
|
|
query = query.with_document_granularity(document_granularity);
|
|
}
|
|
Ok(Self(query.into()))
|
|
}
|
|
"BoostQuery" => {
|
|
let positive: Self = ob.getattr("positive")?.extract()?;
|
|
let negative: Self = ob.getattr("negative")?.extract()?;
|
|
let negative_boost = ob.getattr("negative_boost")?.extract()?;
|
|
Ok(Self(
|
|
BoostQuery::new(positive.0, negative.0, negative_boost).into(),
|
|
))
|
|
}
|
|
"MultiMatchQuery" => {
|
|
let query = ob.getattr("query")?.extract()?;
|
|
let columns = ob.getattr("columns")?.extract()?;
|
|
let boosts: Option<Vec<f32>> = ob.getattr("boosts")?.extract()?;
|
|
let operator: String = ob.getattr("operator")?.extract()?;
|
|
|
|
let q = MultiMatchQuery::try_new(query, columns)
|
|
.map_err(|e| PyValueError::new_err(format!("Invalid query: {}", e)))?;
|
|
let q = if let Some(boosts) = boosts {
|
|
q.try_with_boosts(boosts)
|
|
.map_err(|e| PyValueError::new_err(format!("Invalid boosts: {}", e)))?
|
|
} else {
|
|
q
|
|
};
|
|
|
|
let op = Operator::try_from(operator.as_str())
|
|
.map_err(|e| PyValueError::new_err(format!("Invalid operator: {}", e)))?;
|
|
|
|
Ok(Self(q.with_operator(op).into()))
|
|
}
|
|
"BooleanQuery" => {
|
|
let queries: Vec<(String, Self)> = ob.getattr("queries")?.extract()?;
|
|
let mut sub_queries = Vec::with_capacity(queries.len());
|
|
for (occur, q) in queries {
|
|
let occur = Occur::try_from(occur.as_str())
|
|
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
sub_queries.push((occur, q.0));
|
|
}
|
|
Ok(Self(BooleanQuery::new(sub_queries).into()))
|
|
}
|
|
name => Err(PyValueError::new_err(format!(
|
|
"Unsupported FTS query type: {}",
|
|
name
|
|
))),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'py> IntoPyObject<'py> for PyLanceDB<FtsQuery> {
|
|
type Target = PyAny;
|
|
type Output = Bound<'py, Self::Target>;
|
|
type Error = PyErr;
|
|
|
|
fn into_pyobject(self, py: pyo3::Python<'py>) -> PyResult<Self::Output> {
|
|
let namespace = py
|
|
.import(intern!(py, "lancedb"))
|
|
.and_then(|m| m.getattr(intern!(py, "query")))
|
|
.expect("Failed to import namespace");
|
|
|
|
match self.0 {
|
|
FtsQuery::CombinedFields(_) => Err(PyValueError::new_err(
|
|
"Combined-fields FTS queries are not yet supported in Python",
|
|
)),
|
|
FtsQuery::Match(query) => {
|
|
let kwargs = PyDict::new(py);
|
|
kwargs.set_item("boost", query.boost)?;
|
|
kwargs.set_item("fuzziness", query.fuzziness)?;
|
|
kwargs.set_item("max_expansions", query.max_expansions)?;
|
|
kwargs.set_item::<_, &str>("operator", query.operator.into())?;
|
|
kwargs.set_item("prefix_length", query.prefix_length)?;
|
|
if let Some(document_granularity) = query.document_granularity {
|
|
let value = match document_granularity {
|
|
DocumentGranularity::Row => "row",
|
|
DocumentGranularity::ListElement => "list_element",
|
|
};
|
|
kwargs.set_item("document_granularity", value)?;
|
|
}
|
|
namespace
|
|
.getattr(intern!(py, "MatchQuery"))?
|
|
.call((query.terms, query.column), Some(&kwargs))
|
|
}
|
|
FtsQuery::Phrase(query) => {
|
|
let kwargs = PyDict::new(py);
|
|
kwargs.set_item("slop", query.slop)?;
|
|
if let Some(document_granularity) = query.document_granularity {
|
|
let value = match document_granularity {
|
|
DocumentGranularity::Row => "row",
|
|
DocumentGranularity::ListElement => "list_element",
|
|
};
|
|
kwargs.set_item("document_granularity", value)?;
|
|
}
|
|
namespace
|
|
.getattr(intern!(py, "PhraseQuery"))?
|
|
.call((query.terms, query.column.unwrap()), Some(&kwargs))
|
|
}
|
|
FtsQuery::Boost(query) => {
|
|
let positive = Self(query.positive.as_ref().clone()).into_pyobject(py)?;
|
|
let negative = Self(query.negative.as_ref().clone()).into_pyobject(py)?;
|
|
let kwargs = PyDict::new(py);
|
|
kwargs.set_item("negative_boost", query.negative_boost)?;
|
|
namespace
|
|
.getattr(intern!(py, "BoostQuery"))?
|
|
.call((positive, negative), Some(&kwargs))
|
|
}
|
|
FtsQuery::MultiMatch(query) => {
|
|
let first = &query.match_queries[0];
|
|
let (columns, boosts): (Vec<_>, Vec<_>) = query
|
|
.match_queries
|
|
.iter()
|
|
.map(|q| (q.column.as_ref().unwrap().clone(), q.boost))
|
|
.unzip();
|
|
let kwargs = PyDict::new(py);
|
|
kwargs.set_item("boosts", boosts)?;
|
|
kwargs.set_item::<_, &str>("operator", first.operator.into())?;
|
|
namespace
|
|
.getattr(intern!(py, "MultiMatchQuery"))?
|
|
.call((first.terms.clone(), columns), Some(&kwargs))
|
|
}
|
|
FtsQuery::Boolean(query) => {
|
|
let mut queries: Vec<(&str, Bound<'py, PyAny>)> = Vec::with_capacity(
|
|
query.should.len() + query.must.len() + query.must_not.len(),
|
|
);
|
|
for q in query.should {
|
|
queries.push((Occur::Should.into(), Self(q).into_pyobject(py)?));
|
|
}
|
|
for q in query.must {
|
|
queries.push((Occur::Must.into(), Self(q).into_pyobject(py)?));
|
|
}
|
|
for q in query.must_not {
|
|
queries.push((Occur::MustNot.into(), Self(q).into_pyobject(py)?));
|
|
}
|
|
|
|
namespace
|
|
.getattr(intern!(py, "BooleanQuery"))?
|
|
.call1((queries,))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Python representation of query vector(s)
|
|
#[derive(Clone)]
|
|
pub struct PyQueryVectors(Vec<Arc<dyn Array>>);
|
|
|
|
#[derive(Clone, FromPyObject)]
|
|
#[pyo3(from_item_all)]
|
|
pub struct PyColumnOrdering {
|
|
pub column_name: String,
|
|
pub ascending: bool,
|
|
pub nulls_first: bool,
|
|
}
|
|
|
|
impl From<ColumnOrdering> for PyColumnOrdering {
|
|
fn from(ordering: ColumnOrdering) -> Self {
|
|
Self {
|
|
column_name: ordering.column_name,
|
|
ascending: ordering.ascending,
|
|
nulls_first: ordering.nulls_first,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<PyColumnOrdering> for ColumnOrdering {
|
|
fn from(ordering: PyColumnOrdering) -> Self {
|
|
Self {
|
|
column_name: ordering.column_name,
|
|
ascending: ordering.ascending,
|
|
nulls_first: ordering.nulls_first,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'py> IntoPyObject<'py> for PyColumnOrdering {
|
|
type Target = PyDict;
|
|
type Output = Bound<'py, Self::Target>;
|
|
type Error = PyErr;
|
|
|
|
fn into_pyobject(self, py: pyo3::Python<'py>) -> PyResult<Self::Output> {
|
|
let dict = PyDict::new(py);
|
|
dict.set_item("column_name", self.column_name)?;
|
|
dict.set_item("ascending", self.ascending)?;
|
|
dict.set_item("nulls_first", self.nulls_first)?;
|
|
Ok(dict)
|
|
}
|
|
}
|
|
|
|
impl<'py> IntoPyObject<'py> for PyQueryVectors {
|
|
type Target = PyList;
|
|
type Output = Bound<'py, Self::Target>;
|
|
type Error = PyErr;
|
|
|
|
fn into_pyobject(self, py: pyo3::Python<'py>) -> PyResult<Self::Output> {
|
|
let py_objs = self
|
|
.0
|
|
.into_iter()
|
|
.map(|v| v.to_data().into_pyarrow(py))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
PyList::new(py, py_objs)
|
|
}
|
|
}
|
|
|
|
// Python representation of a query
|
|
#[pyclass(get_all)]
|
|
pub struct PyQueryRequest {
|
|
pub limit: Option<usize>,
|
|
pub offset: Option<usize>,
|
|
pub take_offsets: Option<Vec<u64>>,
|
|
pub filter: Option<PyQueryFilter>,
|
|
pub full_text_search: Option<PyLanceDB<FtsQuery>>,
|
|
pub select: PySelect,
|
|
pub select_source_columns: Option<HashMap<String, String>>,
|
|
pub fast_search: Option<bool>,
|
|
pub with_row_id: Option<bool>,
|
|
pub use_lsm: Option<bool>,
|
|
pub column: Option<String>,
|
|
pub query_vector: Option<PyQueryVectors>,
|
|
pub nprobes: Option<usize>,
|
|
pub minimum_nprobes: Option<usize>,
|
|
pub maximum_nprobes: Option<usize>,
|
|
pub search_effort: Option<f64>,
|
|
pub lower_bound: Option<f32>,
|
|
pub upper_bound: Option<f32>,
|
|
pub ef: Option<usize>,
|
|
pub refine_factor: Option<u32>,
|
|
pub distance_type: Option<String>,
|
|
pub bypass_vector_index: Option<bool>,
|
|
pub postfilter: Option<bool>,
|
|
pub norm: Option<String>,
|
|
pub order_by: Option<Vec<PyColumnOrdering>>,
|
|
}
|
|
|
|
impl From<AnyQuery> for PyQueryRequest {
|
|
fn from(query: AnyQuery) -> Self {
|
|
match query {
|
|
AnyQuery::Query(query_request) => Self {
|
|
limit: query_request.limit,
|
|
offset: query_request.offset,
|
|
take_offsets: query_request.take_offsets,
|
|
filter: query_request.filter.map(PyQueryFilter),
|
|
full_text_search: query_request
|
|
.full_text_search
|
|
.map(|fts| PyLanceDB(fts.query)),
|
|
select_source_columns: PySelect::source_columns(&query_request.select),
|
|
select: PySelect(query_request.select),
|
|
fast_search: Some(query_request.fast_search),
|
|
with_row_id: Some(query_request.with_row_id),
|
|
use_lsm: query_request.use_lsm,
|
|
column: None,
|
|
query_vector: None,
|
|
nprobes: None,
|
|
minimum_nprobes: None,
|
|
maximum_nprobes: None,
|
|
search_effort: None,
|
|
lower_bound: None,
|
|
upper_bound: None,
|
|
ef: None,
|
|
refine_factor: None,
|
|
distance_type: None,
|
|
bypass_vector_index: None,
|
|
postfilter: None,
|
|
norm: None,
|
|
order_by: query_request
|
|
.order_by
|
|
.map(|order_by| order_by.into_iter().map(PyColumnOrdering::from).collect()),
|
|
},
|
|
AnyQuery::VectorQuery(vector_query) => Self {
|
|
limit: vector_query.base.limit,
|
|
offset: vector_query.base.offset,
|
|
take_offsets: vector_query.base.take_offsets,
|
|
filter: vector_query.base.filter.map(PyQueryFilter),
|
|
full_text_search: None,
|
|
select_source_columns: PySelect::source_columns(&vector_query.base.select),
|
|
select: PySelect(vector_query.base.select),
|
|
fast_search: Some(vector_query.base.fast_search),
|
|
with_row_id: Some(vector_query.base.with_row_id),
|
|
use_lsm: vector_query.base.use_lsm,
|
|
column: vector_query.column,
|
|
query_vector: Some(PyQueryVectors(vector_query.query_vector)),
|
|
nprobes: vector_query.nprobes,
|
|
minimum_nprobes: vector_query.minimum_nprobes,
|
|
maximum_nprobes: vector_query.maximum_nprobes,
|
|
search_effort: vector_query.search_effort,
|
|
lower_bound: vector_query.lower_bound,
|
|
upper_bound: vector_query.upper_bound,
|
|
ef: vector_query.ef,
|
|
refine_factor: vector_query.refine_factor,
|
|
distance_type: vector_query.distance_type.map(|d| d.to_string()),
|
|
bypass_vector_index: Some(!vector_query.use_index),
|
|
postfilter: Some(!vector_query.base.prefilter),
|
|
norm: vector_query.base.norm.map(|n| n.to_string()),
|
|
order_by: vector_query
|
|
.base
|
|
.order_by
|
|
.map(|order_by| order_by.into_iter().map(PyColumnOrdering::from).collect()),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Python representation of query selection
|
|
#[derive(Clone)]
|
|
pub struct PySelect(Select);
|
|
|
|
impl PySelect {
|
|
fn source_columns(select: &Select) -> Option<HashMap<String, String>> {
|
|
match select {
|
|
Select::Expr(pairs) => Some(
|
|
pairs
|
|
.iter()
|
|
.filter_map(|(output, expr)| match expr {
|
|
lancedb::expr::DfExpr::Column(column) if column.relation.is_none() => {
|
|
Some((output.clone(), column.name.clone()))
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect(),
|
|
),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'py> IntoPyObject<'py> for PySelect {
|
|
type Target = PyAny;
|
|
type Output = Bound<'py, Self::Target>;
|
|
type Error = PyErr;
|
|
|
|
fn into_pyobject(self, py: pyo3::Python<'py>) -> PyResult<Self::Output> {
|
|
match self.0 {
|
|
Select::All => Ok(py.None().into_bound(py).into_any()),
|
|
Select::Columns(columns) => Ok(columns.into_pyobject(py)?.into_any()),
|
|
Select::Dynamic(columns) => Ok(columns.into_pyobject(py)?.into_any()),
|
|
Select::Expr(pairs) => {
|
|
// Serialize DataFusion Expr -> SQL string so Python sees the same
|
|
// format as Select::Dynamic: a list of (name, sql_string) tuples.
|
|
let sql_pairs: PyResult<Vec<(String, String)>> = pairs
|
|
.into_iter()
|
|
.map(|(name, expr)| {
|
|
lancedb::expr::expr_to_sql_string(&expr)
|
|
.map(|sql| (name, sql))
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
|
|
})
|
|
.collect();
|
|
Ok(sql_pairs?.into_pyobject(py)?.into_any())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Python representation of query filter
|
|
#[derive(Clone)]
|
|
pub struct PyQueryFilter(QueryFilter);
|
|
|
|
impl<'py> IntoPyObject<'py> for PyQueryFilter {
|
|
type Target = PyAny;
|
|
type Output = Bound<'py, Self::Target>;
|
|
type Error = PyErr;
|
|
|
|
fn into_pyobject(self, py: pyo3::Python<'py>) -> PyResult<Self::Output> {
|
|
match self.0 {
|
|
QueryFilter::Datafusion(expr) => {
|
|
// Serialize the DataFusion expression to a SQL string so that
|
|
// callers (e.g. remote tables) see the same format as Sql.
|
|
let sql = lancedb::expr::expr_to_sql_string(&expr)
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
|
|
Ok(sql.into_pyobject(py)?.into_any())
|
|
}
|
|
QueryFilter::Sql(sql) => Ok(sql.into_pyobject(py)?.into_any()),
|
|
QueryFilter::Substrait(substrait) => Ok(substrait.into_pyobject(py)?.into_any()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[pyclass]
|
|
pub struct Query {
|
|
inner: LanceDbQuery,
|
|
}
|
|
|
|
impl Query {
|
|
pub fn new(query: LanceDbQuery) -> Self {
|
|
Self { inner: query }
|
|
}
|
|
}
|
|
|
|
#[pymethods]
|
|
impl Query {
|
|
pub fn r#where(&mut self, predicate: String) {
|
|
self.inner = self.inner.clone().only_if(predicate);
|
|
}
|
|
|
|
pub fn where_expr(&mut self, expr: PyExpr) {
|
|
self.inner = self.inner.clone().only_if_expr(expr.0);
|
|
}
|
|
|
|
pub fn select(&mut self, columns: Vec<(String, String)>) {
|
|
self.inner = self.inner.clone().select(Select::dynamic(&columns));
|
|
}
|
|
|
|
pub fn select_expr(&mut self, columns: Vec<(String, PyExpr)>) {
|
|
let pairs: Vec<(String, lancedb::expr::DfExpr)> =
|
|
columns.into_iter().map(|(name, e)| (name, e.0)).collect();
|
|
self.inner = self.inner.clone().select(Select::Expr(pairs));
|
|
}
|
|
|
|
pub fn select_columns(&mut self, columns: Vec<String>) {
|
|
self.inner = self.inner.clone().select(Select::columns(&columns));
|
|
}
|
|
|
|
pub fn limit(&mut self, limit: u32) {
|
|
self.inner = self.inner.clone().limit(limit as usize);
|
|
}
|
|
|
|
pub fn offset(&mut self, offset: u32) {
|
|
self.inner = self.inner.clone().offset(offset as usize);
|
|
}
|
|
|
|
pub fn fast_search(&mut self) {
|
|
self.inner = self.inner.clone().fast_search();
|
|
}
|
|
|
|
pub fn use_lsm(&mut self, enable: bool) {
|
|
self.inner = self.inner.clone().use_lsm(enable);
|
|
}
|
|
|
|
pub fn with_row_id(&mut self) {
|
|
self.inner = self.inner.clone().with_row_id();
|
|
}
|
|
|
|
pub fn postfilter(&mut self) {
|
|
self.inner = self.inner.clone().postfilter();
|
|
}
|
|
|
|
pub fn nearest_to(&mut self, vector: Bound<'_, PyAny>) -> PyResult<VectorQuery> {
|
|
let data: ArrayData = ArrayData::from_pyarrow_bound(&vector)?;
|
|
let array = make_array(data);
|
|
let inner = self.inner.clone().nearest_to(array).infer_error()?;
|
|
Ok(VectorQuery { inner })
|
|
}
|
|
|
|
pub fn nearest_to_text(&mut self, query: Bound<'_, PyDict>) -> PyResult<FTSQuery> {
|
|
let fts_query = query
|
|
.get_item("query")?
|
|
.ok_or(PyErr::new::<PyRuntimeError, _>(
|
|
"Query text is required for nearest_to_text",
|
|
))?;
|
|
|
|
let query = if let Ok(query_text) = fts_query.cast::<PyString>() {
|
|
let mut query_text = query_text.to_string();
|
|
let columns = query
|
|
.get_item("columns")?
|
|
.map(|columns| columns.extract::<Vec<String>>())
|
|
.transpose()?;
|
|
|
|
let is_phrase =
|
|
query_text.len() >= 2 && query_text.starts_with('"') && query_text.ends_with('"');
|
|
let is_multi_match = columns.as_ref().map(|cols| cols.len() > 1).unwrap_or(false);
|
|
|
|
if is_phrase {
|
|
// Remove the surrounding quotes for phrase queries
|
|
query_text = query_text[1..query_text.len() - 1].to_string();
|
|
}
|
|
|
|
let query: FtsQuery = match (is_phrase, is_multi_match) {
|
|
(false, _) => MatchQuery::new(query_text).into(),
|
|
(true, false) => PhraseQuery::new(query_text).into(),
|
|
(true, true) => {
|
|
return Err(PyValueError::new_err(
|
|
"Phrase queries cannot be used with multiple columns.",
|
|
));
|
|
}
|
|
};
|
|
let mut query = FullTextSearchQuery::new_query(query);
|
|
match columns {
|
|
Some(cols) if !cols.is_empty() => {
|
|
query = query.with_columns(&cols).map_err(|e| {
|
|
PyValueError::new_err(format!(
|
|
"Failed to set full text search columns: {}",
|
|
e
|
|
))
|
|
})?;
|
|
}
|
|
_ => {}
|
|
}
|
|
query
|
|
} else {
|
|
let query = fts_query.extract::<PyLanceDB<FtsQuery>>()?;
|
|
FullTextSearchQuery::new_query(query.0)
|
|
};
|
|
|
|
Ok(FTSQuery {
|
|
inner: self.inner.clone(),
|
|
fts_query: query,
|
|
})
|
|
}
|
|
|
|
pub fn order_by(&mut self, ordering: Option<Vec<PyColumnOrdering>>) -> PyResult<()> {
|
|
let ordering =
|
|
ordering.map(|ordering| ordering.into_iter().map(ColumnOrdering::from).collect());
|
|
self.inner = self.inner.clone().order_by(ordering);
|
|
Ok(())
|
|
}
|
|
|
|
#[pyo3(signature = ())]
|
|
pub fn output_schema(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
let schema = inner.output_schema().await.infer_error()?;
|
|
Python::attach(|py| schema.to_pyarrow(py).map(|obj| obj.unbind()))
|
|
})
|
|
}
|
|
|
|
#[pyo3(signature = (max_batch_length=None, timeout=None))]
|
|
pub fn execute(
|
|
self_: PyRef<'_, Self>,
|
|
max_batch_length: Option<u32>,
|
|
timeout: Option<Duration>,
|
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
let mut opts = QueryExecutionOptions::default();
|
|
if let Some(max_batch_length) = max_batch_length {
|
|
opts.max_batch_length = max_batch_length;
|
|
}
|
|
if let Some(timeout) = timeout {
|
|
opts.timeout = Some(timeout);
|
|
}
|
|
let inner_stream = inner.execute_with_options(opts).await.infer_error()?;
|
|
Ok(RecordBatchStream::new(inner_stream))
|
|
})
|
|
}
|
|
|
|
pub fn explain_plan(self_: PyRef<'_, Self>, verbose: bool) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
inner
|
|
.explain_plan(verbose)
|
|
.await
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
|
|
})
|
|
}
|
|
|
|
#[pyo3(signature = (distributed_metrics=None))]
|
|
pub fn analyze_plan(
|
|
self_: PyRef<'_, Self>,
|
|
distributed_metrics: Option<String>,
|
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
let options = analyze_plan_options(distributed_metrics.as_deref())?;
|
|
future_into_py(self_.py(), async move {
|
|
inner
|
|
.analyze_plan_with_options(options)
|
|
.await
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
|
|
})
|
|
}
|
|
|
|
pub fn to_query_request(&self) -> PyQueryRequest {
|
|
PyQueryRequest::from(AnyQuery::Query(self.inner.clone().into_request()))
|
|
}
|
|
}
|
|
|
|
#[pyclass]
|
|
pub struct TakeQuery {
|
|
inner: LanceDbTakeQuery,
|
|
}
|
|
|
|
impl TakeQuery {
|
|
pub fn new(query: LanceDbTakeQuery) -> Self {
|
|
Self { inner: query }
|
|
}
|
|
}
|
|
|
|
#[pymethods]
|
|
impl TakeQuery {
|
|
pub fn select(&mut self, columns: Vec<(String, String)>) {
|
|
self.inner = self.inner.clone().select(Select::dynamic(&columns));
|
|
}
|
|
|
|
pub fn select_columns(&mut self, columns: Vec<String>) {
|
|
self.inner = self.inner.clone().select(Select::columns(&columns));
|
|
}
|
|
|
|
pub fn with_row_id(&mut self) {
|
|
self.inner = self.inner.clone().with_row_id();
|
|
}
|
|
|
|
pub fn use_lsm(&mut self, enable: bool) {
|
|
self.inner = self.inner.clone().use_lsm(enable);
|
|
}
|
|
|
|
#[pyo3(signature = ())]
|
|
pub fn output_schema(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
let schema = inner.output_schema().await.infer_error()?;
|
|
Python::attach(|py| schema.to_pyarrow(py).map(|obj| obj.unbind()))
|
|
})
|
|
}
|
|
|
|
#[pyo3(signature = (max_batch_length=None, timeout=None))]
|
|
pub fn execute(
|
|
self_: PyRef<'_, Self>,
|
|
max_batch_length: Option<u32>,
|
|
timeout: Option<Duration>,
|
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
let mut opts = QueryExecutionOptions::default();
|
|
if let Some(max_batch_length) = max_batch_length {
|
|
opts.max_batch_length = max_batch_length;
|
|
}
|
|
if let Some(timeout) = timeout {
|
|
opts.timeout = Some(timeout);
|
|
}
|
|
let inner_stream = inner.execute_with_options(opts).await.infer_error()?;
|
|
Ok(RecordBatchStream::new(inner_stream))
|
|
})
|
|
}
|
|
|
|
pub fn explain_plan(self_: PyRef<'_, Self>, verbose: bool) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
inner
|
|
.explain_plan(verbose)
|
|
.await
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
|
|
})
|
|
}
|
|
|
|
#[pyo3(signature = (distributed_metrics=None))]
|
|
pub fn analyze_plan(
|
|
self_: PyRef<'_, Self>,
|
|
distributed_metrics: Option<String>,
|
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
let options = analyze_plan_options(distributed_metrics.as_deref())?;
|
|
future_into_py(self_.py(), async move {
|
|
inner
|
|
.analyze_plan_with_options(options)
|
|
.await
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
|
|
})
|
|
}
|
|
|
|
pub fn to_query_request(&self) -> PyQueryRequest {
|
|
PyQueryRequest::from(AnyQuery::Query(self.inner.clone().into_request()))
|
|
}
|
|
}
|
|
|
|
#[pyclass(from_py_object)]
|
|
#[derive(Clone)]
|
|
pub struct FTSQuery {
|
|
inner: LanceDbQuery,
|
|
fts_query: FullTextSearchQuery,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl FTSQuery {
|
|
pub fn r#where(&mut self, predicate: String) {
|
|
self.inner = self.inner.clone().only_if(predicate);
|
|
}
|
|
|
|
pub fn where_expr(&mut self, expr: PyExpr) {
|
|
self.inner = self.inner.clone().only_if_expr(expr.0);
|
|
}
|
|
|
|
pub fn select(&mut self, columns: Vec<(String, String)>) {
|
|
self.inner = self.inner.clone().select(Select::dynamic(&columns));
|
|
}
|
|
|
|
pub fn select_expr(&mut self, columns: Vec<(String, PyExpr)>) {
|
|
let pairs: Vec<(String, lancedb::expr::DfExpr)> =
|
|
columns.into_iter().map(|(name, e)| (name, e.0)).collect();
|
|
self.inner = self.inner.clone().select(Select::Expr(pairs));
|
|
}
|
|
|
|
pub fn select_columns(&mut self, columns: Vec<String>) {
|
|
self.inner = self.inner.clone().select(Select::columns(&columns));
|
|
}
|
|
|
|
pub fn limit(&mut self, limit: u32) {
|
|
self.inner = self.inner.clone().limit(limit as usize);
|
|
}
|
|
|
|
pub fn offset(&mut self, offset: u32) {
|
|
self.inner = self.inner.clone().offset(offset as usize);
|
|
}
|
|
|
|
pub fn order_by(&mut self, ordering: Option<Vec<PyColumnOrdering>>) -> PyResult<()> {
|
|
let ordering =
|
|
ordering.map(|ordering| ordering.into_iter().map(ColumnOrdering::from).collect());
|
|
self.inner = self.inner.clone().order_by(ordering);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn fast_search(&mut self) {
|
|
self.inner = self.inner.clone().fast_search();
|
|
}
|
|
|
|
pub fn use_lsm(&mut self, enable: bool) {
|
|
self.inner = self.inner.clone().use_lsm(enable);
|
|
}
|
|
|
|
pub fn with_row_id(&mut self) {
|
|
self.inner = self.inner.clone().with_row_id();
|
|
}
|
|
|
|
pub fn postfilter(&mut self) {
|
|
self.inner = self.inner.clone().postfilter();
|
|
}
|
|
|
|
#[pyo3(signature = ())]
|
|
pub fn output_schema(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
let schema = inner.output_schema().await.infer_error()?;
|
|
Python::attach(|py| schema.to_pyarrow(py).map(|obj| obj.unbind()))
|
|
})
|
|
}
|
|
|
|
#[pyo3(signature = (max_batch_length=None, timeout=None))]
|
|
pub fn execute(
|
|
self_: PyRef<'_, Self>,
|
|
max_batch_length: Option<u32>,
|
|
timeout: Option<Duration>,
|
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_
|
|
.inner
|
|
.clone()
|
|
.full_text_search(self_.fts_query.clone());
|
|
|
|
future_into_py(self_.py(), async move {
|
|
let mut opts = QueryExecutionOptions::default();
|
|
if let Some(max_batch_length) = max_batch_length {
|
|
opts.max_batch_length = max_batch_length;
|
|
}
|
|
if let Some(timeout) = timeout {
|
|
opts.timeout = Some(timeout);
|
|
}
|
|
let inner_stream = inner.execute_with_options(opts).await.infer_error()?;
|
|
Ok(RecordBatchStream::new(inner_stream))
|
|
})
|
|
}
|
|
|
|
pub fn nearest_to(&mut self, vector: Bound<'_, PyAny>) -> PyResult<HybridQuery> {
|
|
let vector_query = Query::new(self.inner.clone()).nearest_to(vector)?;
|
|
Ok(HybridQuery {
|
|
inner_fts: self.clone(),
|
|
inner_vec: vector_query,
|
|
})
|
|
}
|
|
|
|
pub fn explain_plan(self_: PyRef<'_, Self>, verbose: bool) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_
|
|
.inner
|
|
.clone()
|
|
.full_text_search(self_.fts_query.clone());
|
|
future_into_py(self_.py(), async move {
|
|
inner
|
|
.explain_plan(verbose)
|
|
.await
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
|
|
})
|
|
}
|
|
|
|
#[pyo3(signature = (distributed_metrics=None))]
|
|
pub fn analyze_plan(
|
|
self_: PyRef<'_, Self>,
|
|
distributed_metrics: Option<String>,
|
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_
|
|
.inner
|
|
.clone()
|
|
.full_text_search(self_.fts_query.clone());
|
|
let options = analyze_plan_options(distributed_metrics.as_deref())?;
|
|
future_into_py(self_.py(), async move {
|
|
inner
|
|
.analyze_plan_with_options(options)
|
|
.await
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
|
|
})
|
|
}
|
|
|
|
pub fn get_query(&self) -> String {
|
|
self.fts_query.query.query().clone()
|
|
}
|
|
|
|
pub fn to_query_request(&self) -> PyQueryRequest {
|
|
let mut req = self.inner.clone().into_request();
|
|
req.full_text_search = Some(self.fts_query.clone());
|
|
PyQueryRequest::from(AnyQuery::Query(req))
|
|
}
|
|
}
|
|
|
|
#[pyclass(from_py_object)]
|
|
#[derive(Clone)]
|
|
pub struct VectorQuery {
|
|
inner: LanceDbVectorQuery,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl VectorQuery {
|
|
pub fn r#where(&mut self, predicate: String) {
|
|
self.inner = self.inner.clone().only_if(predicate);
|
|
}
|
|
|
|
pub fn where_expr(&mut self, expr: PyExpr) {
|
|
self.inner = self.inner.clone().only_if_expr(expr.0);
|
|
}
|
|
|
|
pub fn add_query_vector(&mut self, vector: Bound<'_, PyAny>) -> PyResult<()> {
|
|
let data: ArrayData = ArrayData::from_pyarrow_bound(&vector)?;
|
|
let array = make_array(data);
|
|
self.inner = self.inner.clone().add_query_vector(array).infer_error()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn select(&mut self, columns: Vec<(String, String)>) {
|
|
self.inner = self.inner.clone().select(Select::dynamic(&columns));
|
|
}
|
|
|
|
pub fn select_expr(&mut self, columns: Vec<(String, PyExpr)>) {
|
|
let pairs: Vec<(String, lancedb::expr::DfExpr)> =
|
|
columns.into_iter().map(|(name, e)| (name, e.0)).collect();
|
|
self.inner = self.inner.clone().select(Select::Expr(pairs));
|
|
}
|
|
|
|
pub fn select_columns(&mut self, columns: Vec<String>) {
|
|
self.inner = self.inner.clone().select(Select::columns(&columns));
|
|
}
|
|
|
|
pub fn limit(&mut self, limit: u32) {
|
|
self.inner = self.inner.clone().limit(limit as usize);
|
|
}
|
|
|
|
pub fn offset(&mut self, offset: u32) {
|
|
self.inner = self.inner.clone().offset(offset as usize);
|
|
}
|
|
|
|
pub fn order_by(&mut self, ordering: Option<Vec<PyColumnOrdering>>) -> PyResult<()> {
|
|
let ordering =
|
|
ordering.map(|ordering| ordering.into_iter().map(ColumnOrdering::from).collect());
|
|
self.inner = self.inner.clone().order_by(ordering);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn fast_search(&mut self) {
|
|
self.inner = self.inner.clone().fast_search();
|
|
}
|
|
|
|
pub fn use_lsm(&mut self, enable: bool) {
|
|
self.inner = self.inner.clone().use_lsm(enable);
|
|
}
|
|
|
|
pub fn with_row_id(&mut self) {
|
|
self.inner = self.inner.clone().with_row_id();
|
|
}
|
|
|
|
pub fn column(&mut self, column: String) {
|
|
self.inner = self.inner.clone().column(&column);
|
|
}
|
|
|
|
pub fn distance_type(&mut self, distance_type: String) -> PyResult<()> {
|
|
let distance_type = parse_distance_type(distance_type)?;
|
|
self.inner = self.inner.clone().distance_type(distance_type);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn postfilter(&mut self) {
|
|
self.inner = self.inner.clone().postfilter();
|
|
}
|
|
|
|
pub fn refine_factor(&mut self, refine_factor: u32) {
|
|
self.inner = self.inner.clone().refine_factor(refine_factor);
|
|
}
|
|
|
|
pub fn nprobes(&mut self, nprobe: u32) -> PyResult<()> {
|
|
self.inner = self.inner.clone().nprobes(nprobe as usize).infer_error()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn search_effort(&mut self, search_effort: Option<f64>) -> PyResult<()> {
|
|
self.inner = self
|
|
.inner
|
|
.clone()
|
|
.search_effort(search_effort)
|
|
.infer_error()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn minimum_nprobes(&mut self, minimum_nprobes: u32) -> PyResult<()> {
|
|
self.inner = self
|
|
.inner
|
|
.clone()
|
|
.minimum_nprobes(minimum_nprobes as usize)
|
|
.infer_error()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn maximum_nprobes(&mut self, maximum_nprobes: u32) -> PyResult<()> {
|
|
let maximum_nprobes = if maximum_nprobes == 0 {
|
|
None
|
|
} else {
|
|
Some(maximum_nprobes as usize)
|
|
};
|
|
self.inner = self
|
|
.inner
|
|
.clone()
|
|
.maximum_nprobes(maximum_nprobes)
|
|
.infer_error()?;
|
|
Ok(())
|
|
}
|
|
|
|
#[pyo3(signature = (lower_bound=None, upper_bound=None))]
|
|
pub fn distance_range(&mut self, lower_bound: Option<f32>, upper_bound: Option<f32>) {
|
|
self.inner = self.inner.clone().distance_range(lower_bound, upper_bound);
|
|
}
|
|
|
|
pub fn ef(&mut self, ef: u32) {
|
|
self.inner = self.inner.clone().ef(ef as usize);
|
|
}
|
|
|
|
pub fn bypass_vector_index(&mut self) {
|
|
self.inner = self.inner.clone().bypass_vector_index()
|
|
}
|
|
|
|
#[pyo3(signature = ())]
|
|
pub fn output_schema(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
let schema = inner.output_schema().await.infer_error()?;
|
|
Python::attach(|py| schema.to_pyarrow(py).map(|obj| obj.unbind()))
|
|
})
|
|
}
|
|
|
|
#[pyo3(signature = (max_batch_length=None, timeout=None))]
|
|
pub fn execute(
|
|
self_: PyRef<'_, Self>,
|
|
max_batch_length: Option<u32>,
|
|
timeout: Option<Duration>,
|
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
let mut opts = QueryExecutionOptions::default();
|
|
if let Some(max_batch_length) = max_batch_length {
|
|
opts.max_batch_length = max_batch_length;
|
|
}
|
|
if let Some(timeout) = timeout {
|
|
opts.timeout = Some(timeout);
|
|
}
|
|
let inner_stream = inner.execute_with_options(opts).await.infer_error()?;
|
|
Ok(RecordBatchStream::new(inner_stream))
|
|
})
|
|
}
|
|
|
|
pub fn explain_plan(self_: PyRef<'_, Self>, verbose: bool) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
future_into_py(self_.py(), async move {
|
|
inner
|
|
.explain_plan(verbose)
|
|
.await
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
|
|
})
|
|
}
|
|
|
|
#[pyo3(signature = (distributed_metrics=None))]
|
|
pub fn analyze_plan(
|
|
self_: PyRef<'_, Self>,
|
|
distributed_metrics: Option<String>,
|
|
) -> PyResult<Bound<'_, PyAny>> {
|
|
let inner = self_.inner.clone();
|
|
let options = analyze_plan_options(distributed_metrics.as_deref())?;
|
|
future_into_py(self_.py(), async move {
|
|
inner
|
|
.analyze_plan_with_options(options)
|
|
.await
|
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))
|
|
})
|
|
}
|
|
|
|
pub fn nearest_to_text(&mut self, query: Bound<'_, PyDict>) -> PyResult<HybridQuery> {
|
|
let base_query = self.inner.clone().into_plain();
|
|
let fts_query = Query::new(base_query).nearest_to_text(query)?;
|
|
Ok(HybridQuery {
|
|
inner_vec: self.clone(),
|
|
inner_fts: fts_query,
|
|
})
|
|
}
|
|
|
|
pub fn to_query_request(&self) -> PyQueryRequest {
|
|
PyQueryRequest::from(AnyQuery::VectorQuery(self.inner.clone().into_request()))
|
|
}
|
|
}
|
|
|
|
#[pyclass]
|
|
pub struct HybridQuery {
|
|
inner_vec: VectorQuery,
|
|
inner_fts: FTSQuery,
|
|
}
|
|
|
|
#[pymethods]
|
|
impl HybridQuery {
|
|
pub fn r#where(&mut self, predicate: String) {
|
|
self.inner_vec.r#where(predicate.clone());
|
|
self.inner_fts.r#where(predicate);
|
|
}
|
|
|
|
pub fn where_expr(&mut self, expr: PyExpr) {
|
|
self.inner_vec.where_expr(expr.clone());
|
|
self.inner_fts.where_expr(expr);
|
|
}
|
|
|
|
pub fn select(&mut self, columns: Vec<(String, String)>) {
|
|
self.inner_vec.select(columns.clone());
|
|
self.inner_fts.select(columns);
|
|
}
|
|
|
|
pub fn select_expr(&mut self, columns: Vec<(String, PyExpr)>) {
|
|
self.inner_vec.select_expr(columns.clone());
|
|
self.inner_fts.select_expr(columns);
|
|
}
|
|
|
|
pub fn select_columns(&mut self, columns: Vec<String>) {
|
|
self.inner_vec.select_columns(columns.clone());
|
|
self.inner_fts.select_columns(columns);
|
|
}
|
|
|
|
pub fn limit(&mut self, limit: u32) {
|
|
self.inner_vec.limit(limit);
|
|
self.inner_fts.limit(limit);
|
|
}
|
|
|
|
pub fn offset(&mut self, offset: u32) {
|
|
self.inner_vec.offset(offset);
|
|
self.inner_fts.offset(offset);
|
|
}
|
|
|
|
pub fn order_by(&mut self, ordering: Option<Vec<PyColumnOrdering>>) -> PyResult<()> {
|
|
self.inner_vec.order_by(ordering.clone())?;
|
|
self.inner_fts.order_by(ordering)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn fast_search(&mut self) {
|
|
self.inner_vec.fast_search();
|
|
self.inner_fts.fast_search();
|
|
}
|
|
|
|
pub fn with_row_id(&mut self) {
|
|
self.inner_fts.with_row_id();
|
|
self.inner_vec.with_row_id();
|
|
}
|
|
|
|
pub fn postfilter(&mut self) {
|
|
self.inner_vec.postfilter();
|
|
self.inner_fts.postfilter();
|
|
}
|
|
|
|
pub fn use_lsm(&mut self, enable: bool) {
|
|
self.inner_vec.use_lsm(enable);
|
|
self.inner_fts.use_lsm(enable);
|
|
}
|
|
|
|
pub fn add_query_vector(&mut self, vector: Bound<'_, PyAny>) -> PyResult<()> {
|
|
self.inner_vec.add_query_vector(vector)
|
|
}
|
|
|
|
pub fn column(&mut self, column: String) {
|
|
self.inner_vec.column(column);
|
|
}
|
|
|
|
pub fn distance_type(&mut self, distance_type: String) -> PyResult<()> {
|
|
self.inner_vec.distance_type(distance_type)
|
|
}
|
|
|
|
pub fn refine_factor(&mut self, refine_factor: u32) {
|
|
self.inner_vec.refine_factor(refine_factor);
|
|
}
|
|
|
|
pub fn nprobes(&mut self, nprobe: u32) -> PyResult<()> {
|
|
self.inner_vec.nprobes(nprobe)
|
|
}
|
|
|
|
pub fn search_effort(&mut self, search_effort: Option<f64>) -> PyResult<()> {
|
|
self.inner_vec.search_effort(search_effort)
|
|
}
|
|
|
|
pub fn minimum_nprobes(&mut self, minimum_nprobes: u32) -> PyResult<()> {
|
|
self.inner_vec.minimum_nprobes(minimum_nprobes)
|
|
}
|
|
|
|
pub fn maximum_nprobes(&mut self, maximum_nprobes: u32) -> PyResult<()> {
|
|
self.inner_vec.maximum_nprobes(maximum_nprobes)
|
|
}
|
|
|
|
pub fn ef(&mut self, ef: u32) {
|
|
self.inner_vec.ef(ef);
|
|
}
|
|
|
|
pub fn bypass_vector_index(&mut self) {
|
|
self.inner_vec.bypass_vector_index();
|
|
}
|
|
|
|
#[pyo3(signature = (lower_bound=None, upper_bound=None))]
|
|
pub fn distance_range(&mut self, lower_bound: Option<f32>, upper_bound: Option<f32>) {
|
|
self.inner_vec.distance_range(lower_bound, upper_bound);
|
|
}
|
|
|
|
pub fn to_vector_query(&mut self) -> PyResult<VectorQuery> {
|
|
Ok(VectorQuery {
|
|
inner: self.inner_vec.inner.clone(),
|
|
})
|
|
}
|
|
|
|
pub fn to_fts_query(&mut self) -> PyResult<FTSQuery> {
|
|
Ok(FTSQuery {
|
|
inner: self.inner_fts.inner.clone(),
|
|
fts_query: self.inner_fts.fts_query.clone(),
|
|
})
|
|
}
|
|
|
|
pub fn get_limit(&mut self) -> Option<u32> {
|
|
self.inner_fts
|
|
.inner
|
|
.current_request()
|
|
.limit
|
|
.map(|i| i as u32)
|
|
}
|
|
|
|
pub fn get_with_row_id(&mut self) -> bool {
|
|
self.inner_fts.inner.current_request().with_row_id
|
|
}
|
|
|
|
pub fn to_query_request(&self) -> PyQueryRequest {
|
|
let mut req = self.inner_fts.to_query_request();
|
|
let vec_req = self.inner_vec.to_query_request();
|
|
req.query_vector = vec_req.query_vector;
|
|
req.column = vec_req.column;
|
|
req.distance_type = vec_req.distance_type;
|
|
req.nprobes = vec_req.nprobes;
|
|
req.minimum_nprobes = vec_req.minimum_nprobes;
|
|
req.maximum_nprobes = vec_req.maximum_nprobes;
|
|
req.search_effort = vec_req.search_effort;
|
|
req.ef = vec_req.ef;
|
|
req.refine_factor = vec_req.refine_factor;
|
|
req.lower_bound = vec_req.lower_bound;
|
|
req.upper_bound = vec_req.upper_bound;
|
|
req
|
|
}
|
|
}
|
|
|
|
/// Convert a Python FTS query to JSON string
|
|
#[pyfunction]
|
|
pub fn fts_query_to_json(query_obj: &Bound<'_, PyAny>) -> PyResult<String> {
|
|
let wrapped: PyLanceDB<FtsQuery> = query_obj.extract()?;
|
|
lancedb::table::datafusion::udtf::fts::to_json(&wrapped.0).map_err(|e| {
|
|
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
|
|
"Failed to serialize FTS query to JSON: {}",
|
|
e
|
|
))
|
|
})
|
|
}
|