mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-12 23:02:28 +00:00
`nprobes(0)` bypassed the validation used by the minimum and maximum probe setters, allowing zero into query requests and producing inconsistent local and remote results. Sync Python could additionally interpret the zero maximum as an unbounded search. Reject zero in the Rust vector-query builder and propagate its input error through the Python vector/hybrid and Node bindings. Validate nonpositive counts in the sync Python vector and hybrid builders so every SDK and backend reports `nprobes must be greater than 0` before execution. Update the Rust examples and document the positive-count requirement. Regression tests cover local and remote vector/hybrid builders, rejection before remote query requests, promised TypeScript query vectors, and positive counts replacing both probe bounds. Validation: - Rust probe regression tests: 2 passed. - Focused Python query, remote, hybrid, and serialization tests: 22 passed. The four sync regression cases fail with the original setters. - Focused TypeScript indexed local and mocked remote tests: 2 passed. - Workspace `cargo check --features remote --tests --examples` and Clippy passed. - Rust formatting, Python formatting/lint, and TypeScript build/lint/docs passed. Repository-wide `ruff check .` reports 29 existing errors in unchanged CI scripts; `ruff check python` passes. Fixes #4388 BREAKING CHANGE: Rust `VectorQuery::nprobes` now returns `Result<Self>` instead of `Self`; callers must handle the result, as with the existing minimum/maximum probe setters. <!-- lance-gatekeeper-fix:v1 agent=e92fda5a212c7f9e80fa55e7b4e4634d generation=1 --> Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
1259 lines
44 KiB
Rust
1259 lines
44 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()?;
|
|
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(Some(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.unwrap()), 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 minimum_nprobes: Option<usize>,
|
|
// None means user did not set it and default should be used (currently 20)
|
|
// Some(0) means user set it to None and there is no limit
|
|
pub maximum_nprobes: Option<usize>,
|
|
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,
|
|
minimum_nprobes: None,
|
|
maximum_nprobes: 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)),
|
|
minimum_nprobes: Some(vector_query.minimum_nprobes),
|
|
maximum_nprobes: match vector_query.maximum_nprobes {
|
|
None => Some(0),
|
|
Some(value) => Some(value),
|
|
},
|
|
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 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 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.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
|
|
))
|
|
})
|
|
}
|