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, query: Option, prefetches: Option>, with_vector: Option, with_payload: Option, filter: Option, score_threshold: Option, params: Option, ) -> 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 { 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 { 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, prefetches: Option>, params: Option, filter: Option, score_threshold: Option, ) -> 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 { self.0.query.clone().map(PyScoringQuery) } #[getter] pub fn limit(&self) -> usize { self.0.limit } #[getter] pub fn params(&self) -> Option { self.0.params.map(PySearchParams) } #[getter] pub fn filter(&self) -> Option { self.0.filter.clone().map(PyFilter) } #[getter] pub fn score_threshold(&self) -> Option { 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 { 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 { #[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 { 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 { 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>, }, 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::("Rrf", &[("k", k as &dyn Repr)]) } PyFusion::Rrf { k, weights: Some(weights), } => f.complex_enum::("Rrf", &[("k", k as &dyn Repr), ("weights", weights)]), PyFusion::Dbsf {} => f.complex_enum::("Dbsf", &[]), } } } impl From 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 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, start_from: Option, ) -> PyResult { 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 { self.0.direction.map(PyDirection::from) } #[getter] pub fn start_from(&self) -> Option { 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 for PyOrderBy { fn from(order_by: OrderByInterface) -> Self { Self(OrderBy::from(order_by)) } } impl From 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::(repr) } } impl From for PyDirection { fn from(direction: Direction) -> Self { match direction { Direction::Asc => PyDirection::Asc, Direction::Desc => PyDirection::Desc, } } } impl From 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 { #[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 { 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 { 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::(repr) } } impl From for PySample { fn from(sample: SampleInternal) -> Self { match sample { SampleInternal::Random => PySample::Random, } } } impl From 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, ) -> 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; } }