Files
qdrant/lib/edge/python/src/query.rs
xzfc 628195fafe Remove shard dependency on api (#8284)
* Move `DenseVector`/`MultiDenseVector` from `api` to `segment`

* Move `OrderByInterface` from `api` to `segment`

Reason: it's used in `edge` which shouldn't depend on `api`.

* Make `shard` -> `api` dependency optional

* Remove `api` from the amalgamation

* Don't install protoc in edge Actions
2026-03-05 04:38:02 +00:00

652 lines
17 KiB
Rust

use std::fmt;
use bytemuck::{TransparentWrapper, TransparentWrapperAlloc as _};
use derive_more::Into;
use ordered_float::OrderedFloat;
use pyo3::IntoPyObjectExt;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use segment::data_types::order_by::{Direction, OrderBy, OrderByInterface, StartFrom};
use segment::data_types::vectors::{DEFAULT_VECTOR_NAME, VectorInternal};
use segment::index::query_optimization::rescore_formula::parsed_formula::ParsedFormula;
use segment::json_path::JsonPath;
use shard::query::query_enum::QueryEnum;
use shard::query::*;
use super::*;
use crate::repr::*;
#[pyclass(name = "QueryRequest", from_py_object)]
#[derive(Clone, Debug, Into)]
pub struct PyQueryRequest(ShardQueryRequest);
#[pyclass_repr]
#[pymethods]
impl PyQueryRequest {
#[new]
#[pyo3(signature = (
limit,
offset = None,
query = None,
prefetches = None,
with_vector = None,
with_payload = None,
filter = None,
score_threshold = None,
params = None,
))]
#[allow(clippy::too_many_arguments)]
pub fn new(
limit: usize,
offset: Option<usize>,
query: Option<PyScoringQuery>,
prefetches: Option<Vec<PyPrefetch>>,
with_vector: Option<PyWithVector>,
with_payload: Option<PyWithPayload>,
filter: Option<PyFilter>,
score_threshold: Option<f32>,
params: Option<PySearchParams>,
) -> Self {
Self(ShardQueryRequest {
prefetches: PyPrefetch::peel_vec(prefetches.unwrap_or_default()),
limit,
offset: offset.unwrap_or(0),
with_vector: with_vector.map(WithVector::from).unwrap_or_default(),
with_payload: with_payload
.map(WithPayloadInterface::from)
.unwrap_or_default(),
query: query.map(ScoringQuery::from),
filter: filter.map(Filter::from),
score_threshold: score_threshold.map(OrderedFloat),
params: params.map(SearchParams::from),
})
}
#[getter]
pub fn prefetches(&self) -> &[PyPrefetch] {
PyPrefetch::wrap_slice(&self.0.prefetches)
}
#[getter]
pub fn query(&self) -> Option<&PyScoringQuery> {
self.0.query.as_ref().map(PyScoringQuery::wrap_ref)
}
#[getter]
pub fn filter(&self) -> Option<&PyFilter> {
self.0.filter.as_ref().map(PyFilter::wrap_ref)
}
#[getter]
pub fn score_threshold(&self) -> Option<f32> {
self.0
.score_threshold
.map(|threshold| threshold.into_inner())
}
#[getter]
pub fn limit(&self) -> usize {
self.0.limit
}
#[getter]
pub fn offset(&self) -> usize {
self.0.offset
}
#[getter]
pub fn params(&self) -> Option<PySearchParams> {
self.0.params.map(PySearchParams)
}
#[getter]
pub fn with_vector(&self) -> &PyWithVector {
PyWithVector::wrap_ref(&self.0.with_vector)
}
#[getter]
pub fn with_payload(&self) -> &PyWithPayload {
PyWithPayload::wrap_ref(&self.0.with_payload)
}
pub fn __repr__(&self) -> String {
self.repr()
}
}
impl PyQueryRequest {
fn _getters(self) {
// Every field should have a getter method
let ShardQueryRequest {
prefetches: _,
query: _,
filter: _,
score_threshold: _,
limit: _,
offset: _,
params: _,
with_vector: _,
with_payload: _,
} = self.0;
}
}
#[pyclass(name = "Prefetch", from_py_object)]
#[derive(Clone, Debug, Into, TransparentWrapper)]
#[repr(transparent)]
pub struct PyPrefetch(ShardPrefetch);
#[pyclass_repr]
#[pymethods]
impl PyPrefetch {
#[new]
#[pyo3(signature = (
limit,
query = None,
prefetches = None,
params = None,
filter = None,
score_threshold = None,
))]
pub fn new(
limit: usize,
query: Option<PyScoringQuery>,
prefetches: Option<Vec<PyPrefetch>>,
params: Option<PySearchParams>,
filter: Option<PyFilter>,
score_threshold: Option<f32>,
) -> Self {
Self(ShardPrefetch {
prefetches: PyPrefetch::peel_vec(prefetches.unwrap_or_default()),
limit,
query: query.map(ScoringQuery::from),
params: params.map(SearchParams::from),
filter: filter.map(Filter::from),
score_threshold: score_threshold.map(OrderedFloat),
})
}
#[getter]
pub fn prefetches(&self) -> &[PyPrefetch] {
PyPrefetch::wrap_slice(&self.0.prefetches)
}
#[getter]
pub fn query(&self) -> Option<PyScoringQuery> {
self.0.query.clone().map(PyScoringQuery)
}
#[getter]
pub fn limit(&self) -> usize {
self.0.limit
}
#[getter]
pub fn params(&self) -> Option<PySearchParams> {
self.0.params.map(PySearchParams)
}
#[getter]
pub fn filter(&self) -> Option<PyFilter> {
self.0.filter.clone().map(PyFilter)
}
#[getter]
pub fn score_threshold(&self) -> Option<f32> {
self.0
.score_threshold
.map(|threshold| threshold.into_inner())
}
pub fn __repr__(&self) -> String {
self.repr()
}
}
impl PyPrefetch {
fn _getters(self) {
// Every field should have a getter method
let ShardPrefetch {
prefetches: _,
query: _,
limit: _,
params: _,
filter: _,
score_threshold: _,
} = self.0;
}
}
impl<'py> IntoPyObject<'py> for &PyPrefetch {
type Target = PyPrefetch;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
IntoPyObject::into_pyobject(self.clone(), py)
}
}
#[derive(Clone, Debug, Into, TransparentWrapper)]
#[repr(transparent)]
pub struct PyScoringQuery(ScoringQuery);
impl FromPyObject<'_, '_> for PyScoringQuery {
type Error = PyErr;
fn extract(query: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
#[derive(FromPyObject)]
enum Helper {
Vector(PyQuery),
Fusion(PyFusion),
OrderBy(PyOrderBy),
Formula(PyFormula),
Sample(PySample),
Mmr(PyMmr),
}
fn _variants(query: ScoringQuery) {
match query {
ScoringQuery::Vector(_) => {}
ScoringQuery::Fusion(_) => {}
ScoringQuery::OrderBy(_) => {}
ScoringQuery::Formula(_) => {}
ScoringQuery::Sample(_) => {}
ScoringQuery::Mmr(_) => {}
}
}
let query = match query.extract()? {
Helper::Vector(query) => ScoringQuery::Vector(QueryEnum::from(query)),
Helper::Fusion(fusion) => ScoringQuery::Fusion(FusionInternal::from(fusion)),
Helper::OrderBy(order_by) => ScoringQuery::OrderBy(OrderBy::from(order_by)),
Helper::Formula(formula) => ScoringQuery::Formula(ParsedFormula::from(formula)),
Helper::Sample(sample) => ScoringQuery::Sample(SampleInternal::from(sample)),
Helper::Mmr(mmr) => ScoringQuery::Mmr(MmrInternal::from(mmr)),
};
Ok(Self(query))
}
}
impl<'py> IntoPyObject<'py> for PyScoringQuery {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
match self.0 {
ScoringQuery::Vector(vector) => PyQuery(vector).into_bound_py_any(py),
ScoringQuery::Fusion(fusion) => PyFusion::from(fusion).into_bound_py_any(py),
ScoringQuery::OrderBy(order_by) => PyOrderBy(order_by).into_bound_py_any(py),
ScoringQuery::Formula(formula) => PyFormula(formula).into_bound_py_any(py),
ScoringQuery::Sample(sample) => PySample::from(sample).into_bound_py_any(py),
ScoringQuery::Mmr(mmr) => PyMmr(mmr).into_bound_py_any(py),
}
}
}
impl<'py> IntoPyObject<'py> for &PyScoringQuery {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
IntoPyObject::into_pyobject(self.clone(), py)
}
}
impl Repr for PyScoringQuery {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.0 {
ScoringQuery::Vector(vector) => PyQuery::wrap_ref(vector).fmt(f),
ScoringQuery::Fusion(fusion) => PyFusion::from(fusion.clone()).fmt(f),
ScoringQuery::OrderBy(order_by) => PyOrderBy::wrap_ref(order_by).fmt(f),
ScoringQuery::Formula(_formula) => f.unimplemented(), // TODO!
ScoringQuery::Sample(sample) => PySample::from(*sample).fmt(f),
ScoringQuery::Mmr(mmr) => PyMmr::wrap_ref(mmr).fmt(f),
}
}
}
#[pyclass(name = "Fusion", from_py_object)]
#[derive(Clone, Debug)]
pub enum PyFusion {
#[pyo3(constructor = (k, weights = None))]
Rrf {
k: usize,
weights: Option<Vec<f32>>,
},
Dbsf {},
}
#[pymethods]
impl PyFusion {
pub fn __repr__(&self) -> String {
self.repr()
}
}
impl Repr for PyFusion {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
PyFusion::Rrf { k, weights: None } => {
f.complex_enum::<Self>("Rrf", &[("k", k as &dyn Repr)])
}
PyFusion::Rrf {
k,
weights: Some(weights),
} => f.complex_enum::<Self>("Rrf", &[("k", k as &dyn Repr), ("weights", weights)]),
PyFusion::Dbsf {} => f.complex_enum::<Self>("Dbsf", &[]),
}
}
}
impl From<FusionInternal> for PyFusion {
fn from(fusion: FusionInternal) -> Self {
match fusion {
FusionInternal::Rrf { k, weights } => PyFusion::Rrf {
k,
weights: weights.map(|w| w.into_iter().map(|f| f.into_inner()).collect()),
},
FusionInternal::Dbsf => PyFusion::Dbsf {},
}
}
}
impl From<PyFusion> for FusionInternal {
fn from(fusion: PyFusion) -> Self {
match fusion {
PyFusion::Rrf { k, weights } => FusionInternal::Rrf {
k,
weights: weights.map(|w| w.into_iter().map(ordered_float::OrderedFloat).collect()),
},
PyFusion::Dbsf {} => FusionInternal::Dbsf,
}
}
}
#[pyclass(name = "OrderBy", from_py_object)]
#[derive(Clone, Debug, Into, TransparentWrapper)]
#[repr(transparent)]
pub struct PyOrderBy(OrderBy);
#[pyclass_repr]
#[pymethods]
impl PyOrderBy {
#[new]
#[pyo3(signature = (key, direction = None, start_from = None))]
pub fn new(
key: PyJsonPath,
direction: Option<PyDirection>,
start_from: Option<PyStartFrom>,
) -> PyResult<Self> {
let order_by = OrderBy {
key: JsonPath::from(key),
direction: direction.map(Direction::from),
start_from: start_from.map(StartFrom::from),
};
Ok(Self(order_by))
}
#[getter]
pub fn key(&self) -> &PyJsonPath {
PyJsonPath::wrap_ref(&self.0.key)
}
#[getter]
pub fn direction(&self) -> Option<PyDirection> {
self.0.direction.map(PyDirection::from)
}
#[getter]
pub fn start_from(&self) -> Option<PyStartFrom> {
self.0.start_from.map(PyStartFrom)
}
pub fn __repr__(&self) -> String {
self.repr()
}
}
impl PyOrderBy {
fn _getters(self) {
// Every field should have a getter method
let OrderBy {
key: _,
direction: _,
start_from: _,
} = self.0;
}
}
impl From<OrderByInterface> for PyOrderBy {
fn from(order_by: OrderByInterface) -> Self {
Self(OrderBy::from(order_by))
}
}
impl From<PyOrderBy> for OrderByInterface {
fn from(order_by: PyOrderBy) -> Self {
OrderByInterface::Struct(OrderBy::from(order_by))
}
}
#[pyclass(name = "Direction", from_py_object)]
#[derive(Copy, Clone, Debug)]
pub enum PyDirection {
Asc,
Desc,
}
#[pymethods]
impl PyDirection {
pub fn __repr__(&self) -> String {
self.repr()
}
}
impl Repr for PyDirection {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let repr = match self {
PyDirection::Asc => "Asc",
PyDirection::Desc => "Desc",
};
f.simple_enum::<Self>(repr)
}
}
impl From<Direction> for PyDirection {
fn from(direction: Direction) -> Self {
match direction {
Direction::Asc => PyDirection::Asc,
Direction::Desc => PyDirection::Desc,
}
}
}
impl From<PyDirection> for Direction {
fn from(direction: PyDirection) -> Self {
match direction {
PyDirection::Asc => Direction::Asc,
PyDirection::Desc => Direction::Desc,
}
}
}
#[derive(Copy, Clone, Debug, Into)]
pub struct PyStartFrom(StartFrom);
impl FromPyObject<'_, '_> for PyStartFrom {
type Error = PyErr;
fn extract(start_from: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
#[derive(FromPyObject)]
enum Helper {
Integer(IntPayloadType),
Float(FloatPayloadType),
DateTime(String),
}
fn _variants(start_from: StartFrom) {
match start_from {
StartFrom::Integer(_) => {}
StartFrom::Float(_) => {}
StartFrom::Datetime(_) => {}
}
}
let start_from = match start_from.extract()? {
Helper::Integer(int) => StartFrom::Integer(int),
Helper::Float(float) => StartFrom::Float(float),
Helper::DateTime(date_time) => {
let date_time = date_time.parse().map_err(|err| {
PyValueError::new_err(format!("failed to parse date-time: {err}"))
})?;
StartFrom::Datetime(date_time)
}
};
Ok(Self(start_from))
}
}
impl<'py> IntoPyObject<'py> for PyStartFrom {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
IntoPyObject::into_pyobject(&self, py)
}
}
impl<'py> IntoPyObject<'py> for &PyStartFrom {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> PyResult<Self::Output> {
match &self.0 {
StartFrom::Integer(int) => int.into_bound_py_any(py),
StartFrom::Float(float) => float.into_bound_py_any(py),
StartFrom::Datetime(date_time) => date_time.to_string().into_bound_py_any(py),
}
}
}
impl Repr for PyStartFrom {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.0 {
StartFrom::Integer(int) => int.fmt(f),
StartFrom::Float(float) => float.fmt(f),
StartFrom::Datetime(date_time) => date_time.to_string().fmt(f),
}
}
}
#[pyclass(name = "Sample", from_py_object)]
#[derive(Copy, Clone, Debug)]
pub enum PySample {
Random,
}
#[pymethods]
impl PySample {
pub fn __repr__(&self) -> String {
self.repr()
}
}
impl Repr for PySample {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let repr = match self {
PySample::Random => "Random",
};
f.simple_enum::<Self>(repr)
}
}
impl From<SampleInternal> for PySample {
fn from(sample: SampleInternal) -> Self {
match sample {
SampleInternal::Random => PySample::Random,
}
}
}
impl From<PySample> for SampleInternal {
fn from(sample: PySample) -> Self {
match sample {
PySample::Random => SampleInternal::Random,
}
}
}
#[pyclass(name = "Mmr", from_py_object)]
#[derive(Clone, Debug, Into, TransparentWrapper)]
#[repr(transparent)]
pub struct PyMmr(MmrInternal);
#[pyclass_repr]
#[pymethods]
impl PyMmr {
#[new]
#[pyo3(signature = (vector, lambda_, candidates_limit, using = None))]
pub fn new(
vector: PyNamedVectorInternal,
lambda_: f32,
candidates_limit: usize,
using: Option<String>,
) -> Self {
let mmr = MmrInternal {
vector: VectorInternal::from(vector),
using: using.unwrap_or_else(|| DEFAULT_VECTOR_NAME.to_string()),
lambda: OrderedFloat(lambda_),
candidates_limit,
};
Self(mmr)
}
#[getter]
pub fn vector(&self) -> &PyNamedVectorInternal {
PyNamedVectorInternal::wrap_ref(&self.0.vector)
}
#[getter]
pub fn using(&self) -> &str {
&self.0.using
}
#[getter]
pub fn lambda_(&self) -> f32 {
self.0.lambda.into_inner()
}
#[getter]
pub fn candidates_limit(&self) -> usize {
self.0.candidates_limit
}
pub fn __repr__(&self) -> String {
self.repr()
}
}
impl PyMmr {
fn _getters(self) {
// Every field should have a getter method
let MmrInternal {
vector: _,
using: _,
lambda: _,
candidates_limit: _,
} = self.0;
}
}