More type conversion for Qdrant on Edge Python bindings (#7372)

* Cleanup `edge::Shard::retrieve`

* Cleanup Python errors

* Cleanup `PointId` conversions

* Rename `ids` into `point_ids` in `PyShard::retrieve`

* WIP: Prototype native conversions for more types
This commit is contained in:
Roman Titov
2025-10-10 19:56:05 +02:00
committed by GitHub
parent da5e5cd42d
commit e7fe24bb1b
9 changed files with 315 additions and 306 deletions

View File

@@ -26,12 +26,9 @@ mod qdrant_edge {
PyVectorStorageDatatype, PyVectorStorageType,
};
#[pymodule_export]
use super::search::{
PyFilter, PyQuery, PyQueryVector, PyScoredPoint, PySearchParams, PySearchRequest,
PyWithPayload, PyWithVector,
};
use super::search::{PyScoredPoint, PySearchParams, PySearchRequest};
#[pymodule_export]
use super::types::{PyRecord, PyVector};
use super::types::{PyRecord, PySparseVector};
#[pymodule_export]
use super::update::{PyPoint, PyUpdateOperation};
}
@@ -55,29 +52,23 @@ impl PyShard {
pub fn search(&self, search: PySearchRequest) -> Result<Vec<PyScoredPoint>> {
let points = self.0.search(search.into())?;
let points = points.into_iter().map(PyScoredPoint).collect();
let points = PyScoredPoint::from_rust_vec(points);
Ok(points)
}
pub fn retrieve(
&self,
ids: Vec<PyPointId>,
point_ids: Vec<PyPointId>,
with_payload: Option<PyWithPayload>,
with_vector: Option<PyWithVector>,
) -> Result<Vec<PyRecord>, PyErr> {
let ids_res: Result<Vec<_>, _> = ids.into_iter().map(PointIdType::try_from).collect();
let ids = ids_res?;
let records = self
.0
.retrieve(
&ids,
with_payload.map(WithPayloadInterface::from),
with_vector.map(WithVector::from),
)
.map_err(PyError::from)?;
let points = records.into_iter().map(PyRecord).collect();
) -> Result<Vec<PyRecord>> {
let point_ids = PyPointId::into_rust_vec(point_ids);
let points = self.0.retrieve(
&point_ids,
with_payload.map(WithPayloadInterface::from),
with_vector.map(WithVector::from),
)?;
let points = PyRecord::from_rust_vec(points);
Ok(points)
}
}
@@ -95,6 +86,6 @@ impl From<OperationError> for PyError {
impl From<PyError> for PyErr {
fn from(err: PyError) -> Self {
PyErr::new::<PyException, _>(err.0.to_string())
PyException::new_err(err.0.to_string())
}
}

View File

@@ -1,7 +1,10 @@
use std::mem;
use derive_more::Into;
use pyo3::IntoPyObjectExt as _;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use segment::data_types::vectors::*;
use segment::json_path::JsonPath;
use segment::data_types::vectors::NamedQuery;
use segment::types::*;
use shard::query::query_enum::QueryEnum;
use shard::search::*;
@@ -39,67 +42,30 @@ impl PySearchRequest {
}
}
#[pyclass(name = "Query")]
#[derive(Clone, Debug, Into)]
pub struct PyQuery(QueryEnum);
#[pymethods]
impl PyQuery {
#[staticmethod]
pub fn nearest(query: PyQueryVector, using: Option<String>) -> Self {
Self(QueryEnum::Nearest(NamedQuery {
query: query.into(),
using,
}))
impl<'py> FromPyObject<'py> for PyQuery {
fn extract_bound(query: &Bound<'py, PyAny>) -> PyResult<Self> {
let query = if let Ok(single) = query.extract() {
QueryEnum::Nearest(NamedQuery::default_dense(single))
} else {
return Err(PyValueError::new_err(format!(
"failed to convert Python object {query} into query"
)));
};
Ok(Self(query))
}
}
impl PyQuery {
fn _variants(query: QueryEnum) {
match query {
QueryEnum::Nearest(_) => (),
QueryEnum::RecommendBestScore(_) => todo!(), // TODO!
QueryEnum::RecommendSumScores(_) => todo!(), // TODO!
QueryEnum::Discover(_) => todo!(), // TODO!
QueryEnum::Context(_) => todo!(), // TODO!
}
}
}
#[pyclass(name = "QueryVector")]
#[derive(Clone, Debug, Into)]
pub struct PyQueryVector(VectorInternal);
#[pymethods]
impl PyQueryVector {
#[staticmethod]
pub fn dense(vector: Vec<f32>) -> Self {
Self(VectorInternal::Dense(vector))
}
#[staticmethod]
pub fn sparse(vector: PySparseVector) -> Self {
Self(VectorInternal::Sparse(vector.into()))
}
}
impl PyQueryVector {
fn _variants(query: VectorInternal) {
match query {
VectorInternal::Dense(_) => (),
VectorInternal::Sparse(_) => (),
VectorInternal::MultiDense(_) => todo!(), // TODO!
}
}
}
#[pyclass(name = "Filter")]
#[derive(Clone, Debug, Into)]
pub struct PyFilter(Filter);
#[pymethods]
impl PyFilter {
// TODO!
impl<'py> FromPyObject<'py> for PyFilter {
fn extract_bound(_filter: &Bound<'py, PyAny>) -> PyResult<Self> {
todo!()
}
}
#[pyclass(name = "SearchParams")]
@@ -140,124 +106,110 @@ impl PyQuantizationSearchParams {
}
}
#[pyclass(name = "WithVector")]
#[derive(Clone, Debug, Into)]
pub struct PyWithVector(WithVector);
#[pymethods]
impl PyWithVector {
#[new]
pub fn new(with_vector: bool) -> Self {
Self(WithVector::Bool(with_vector))
}
impl<'py> FromPyObject<'py> for PyWithVector {
fn extract_bound(with_vector: &Bound<'py, PyAny>) -> PyResult<Self> {
#[derive(FromPyObject)]
enum Helper {
Bool(bool),
Selector(Vec<String>),
}
#[staticmethod]
pub fn selector(vectors: Vec<VectorNameBuf>) -> Self {
Self(WithVector::Selector(vectors))
let with_vector = match with_vector.extract()? {
Helper::Bool(bool) => WithVector::Bool(bool),
Helper::Selector(vectors) => WithVector::Selector(vectors),
};
Ok(Self(with_vector))
}
}
impl PyWithVector {
fn _variants(with_vector: WithVector) {
match with_vector {
WithVector::Bool(_) => (),
WithVector::Selector(_) => (),
impl<'py> IntoPyObject<'py> for PyWithVector {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr; // Infallible?
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
IntoPyObject::into_pyobject(&self, py)
}
}
impl<'py> IntoPyObject<'py> for &PyWithVector {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr; // Infallible?
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
match &self.0 {
WithVector::Bool(bool) => bool.into_bound_py_any(py),
WithVector::Selector(vectors) => vectors.into_bound_py_any(py),
}
}
}
#[pyclass(name = "WithPayload")]
#[derive(Clone, Debug, Into)]
pub struct PyWithPayload(WithPayloadInterface);
#[pymethods]
impl PyWithPayload {
#[new]
pub fn new(with_payload: bool) -> Self {
Self(WithPayloadInterface::Bool(with_payload))
}
#[staticmethod]
pub fn fields(fields: Vec<PyJsonPath>) -> Self {
let fields = fields.into_iter().map(Into::into).collect(); // TODO: Transmute!?
Self(WithPayloadInterface::Fields(fields))
}
#[staticmethod]
pub fn selector(selector: PyPayloadSelector) -> Self {
Self(WithPayloadInterface::Selector(selector.into()))
}
}
impl PyWithPayload {
fn _variants(with_payload: WithPayloadInterface) {
match with_payload {
WithPayloadInterface::Bool(_) => (),
WithPayloadInterface::Fields(_) => (),
WithPayloadInterface::Selector(_) => (),
impl<'py> FromPyObject<'py> for PyWithPayload {
fn extract_bound(with_payload: &Bound<'py, PyAny>) -> PyResult<Self> {
#[derive(FromPyObject)]
enum Helper {
Bool(bool),
// TODO: `Fields(Vec<JsonPath>)`!
// TODO: `Selector(PayloadSelector)`!
}
}
}
#[pyclass(name = "PayloadSelector")]
#[derive(Clone, Debug, Into)]
pub struct PyPayloadSelector(PayloadSelector);
#[pymethods]
impl PyPayloadSelector {
#[staticmethod]
pub fn include(fields: Vec<PyJsonPath>) -> Self {
let include = PayloadSelectorInclude {
include: fields.into_iter().map(Into::into).collect(), // TODO: Transmute!?
let with_payload = match with_payload.extract()? {
Helper::Bool(bool) => WithPayloadInterface::Bool(bool),
};
Self(PayloadSelector::Include(include))
}
#[staticmethod]
pub fn exclude(fields: Vec<PyJsonPath>) -> Self {
let exclude = PayloadSelectorExclude {
exclude: fields.into_iter().map(Into::into).collect(), // TODO: Transmute!?
};
Self(PayloadSelector::Exclude(exclude))
Ok(Self(with_payload))
}
}
impl PyPayloadSelector {
fn _variants(payload_selector: PayloadSelector) {
match payload_selector {
PayloadSelector::Include(_) => (),
PayloadSelector::Exclude(_) => (),
impl<'py> IntoPyObject<'py> for PyWithPayload {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr; // Infallible?
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
IntoPyObject::into_pyobject(&self, py)
}
}
impl<'py> IntoPyObject<'py> for &PyWithPayload {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr; // Infallible?
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
match &self.0 {
WithPayloadInterface::Bool(bool) => bool.into_bound_py_any(py),
WithPayloadInterface::Fields(_fields) => todo!(),
WithPayloadInterface::Selector(_selector) => todo!(),
}
}
}
#[pyclass(name = "JsonPath")]
#[derive(Clone, Debug, Into)]
pub struct PyJsonPath(JsonPath);
#[pymethods]
impl PyJsonPath {
#[new]
pub fn new(json_path: &str) -> super::Result<Self> {
let json_path = json_path.parse().map_err(|()| {
OperationError::validation_error(format!("{json_path} is not a valid JSON path"))
})?;
Ok(Self(json_path))
}
}
#[pyclass(name = "ScoredPoint")]
#[derive(Clone, Debug, Into)]
#[repr(transparent)]
pub struct PyScoredPoint(pub ScoredPoint);
impl PyScoredPoint {
pub fn from_rust_vec(points: Vec<ScoredPoint>) -> Vec<Self> {
// `PyScoredPoint` has transparent representation, so transmuting is safe
unsafe { mem::transmute(points) }
}
}
#[pymethods]
impl PyScoredPoint {
#[getter]
pub fn id(&self) -> PyPointId {
PyPointId::from(self.0.id)
PyPointId(self.0.id)
}
#[getter]
@@ -271,15 +223,8 @@ impl PyScoredPoint {
}
#[getter]
pub fn vector(&self) -> Option<DenseVector> {
let Some(vector) = &self.0.vector else {
return None;
};
match vector {
VectorStructInternal::Single(vec) => Some(vec.clone()),
_ => None, // TODO!
}
pub fn vector(&self) -> Option<&PyVectorInternal> {
self.0.vector.as_ref().map(PyVectorInternal::from_ref)
}
#[getter]

View File

@@ -2,7 +2,7 @@ use std::mem;
use derive_more::Into;
use pyo3::IntoPyObjectExt as _;
use pyo3::exceptions::PyException;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyFloat, PyInt, PyList};
use segment::types::*;
@@ -62,8 +62,8 @@ fn value_from_py(val: &Bound<PyAny>) -> PyResult<serde_json::Value> {
let obj = object_from_py(dict)?;
serde_json::Value::Object(obj)
} else {
return Err(PyErr::new::<PyException, _>(format!(
"failed to convert Python object {val} into payload value"
return Err(PyValueError::new_err(format!(
"failed to convert Python object {val} into payload value type"
)));
};
@@ -88,11 +88,13 @@ fn number_from_py(num: &Bound<PyAny>) -> PyResult<serde_json::Number> {
serde_json::Number::from(int)
} else if let Ok(float) = num.extract() {
serde_json::Number::from_f64(float).ok_or_else(|| {
PyErr::new::<PyException, _>(format!("failed to convert {float} into payload number"))
PyValueError::new_err(format!(
"failed to convert {float} into payload number type"
))
})?
} else {
return Err(PyErr::new::<PyException, _>(format!(
"failed to convert Python object {num} into payload number"
return Err(PyValueError::new_err(format!(
"failed to convert Python object {num} into payload number type"
)));
};

View File

@@ -1,38 +1,67 @@
use pyo3::exceptions::PyException;
use std::mem;
use derive_more::Into;
use pyo3::IntoPyObjectExt as _;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use segment::types::PointIdType;
use uuid::Uuid;
#[derive(Clone, Debug, IntoPyObject, FromPyObject)]
pub enum PyPointId {
NumId(u64),
UuidString(String),
Uuid(Uuid),
#[derive(Copy, Clone, Debug, Into)]
#[repr(transparent)]
pub struct PyPointId(pub PointIdType);
impl PyPointId {
pub fn into_rust_vec(point_ids: Vec<PyPointId>) -> Vec<PointIdType> {
// `PyPointId` has transparent representation, so transmuting is safe
unsafe { mem::transmute(point_ids) }
}
}
impl TryFrom<PyPointId> for PointIdType {
type Error = PyErr;
fn try_from(value: PyPointId) -> Result<Self, Self::Error> {
match value {
PyPointId::NumId(id) => Ok(PointIdType::NumId(id)),
PyPointId::UuidString(uuid_string) => {
let uuid = Uuid::parse_str(&uuid_string).map_err(|_| {
PyErr::new::<PyException, _>(format!(
"failed to parse string {uuid_string} into UUID for point ID"
))
impl<'py> FromPyObject<'py> for PyPointId {
fn extract_bound(point_id: &Bound<'py, PyAny>) -> PyResult<Self> {
#[derive(FromPyObject)]
enum Helper {
NumId(u64),
Uuid(Uuid),
UuidStr(String),
}
let point_id = match point_id.extract()? {
Helper::NumId(id) => PointIdType::NumId(id),
Helper::Uuid(uuid) => PointIdType::Uuid(uuid),
Helper::UuidStr(uuid_str) => {
let uuid = Uuid::parse_str(&uuid_str).map_err(|err| {
PyValueError::new_err(format!("failed to parse {uuid_str} as UUID: {err}"))
})?;
Ok(PointIdType::Uuid(uuid))
PointIdType::Uuid(uuid)
}
PyPointId::Uuid(uuid) => Ok(PointIdType::Uuid(uuid)),
}
};
Ok(Self(point_id))
}
}
impl From<PointIdType> for PyPointId {
fn from(value: PointIdType) -> Self {
match value {
PointIdType::NumId(id) => PyPointId::NumId(id),
PointIdType::Uuid(uuid) => PyPointId::Uuid(uuid),
impl<'py> IntoPyObject<'py> for PyPointId {
type Target = PyAny;
type Output = Bound<'py, PyAny>;
type Error = PyErr; // Infallible
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
IntoPyObject::into_pyobject(&self, py)
}
}
impl<'py> IntoPyObject<'py> for &PyPointId {
type Target = PyAny;
type Output = Bound<'py, PyAny>;
type Error = PyErr; // Infallible
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
match &self.0 {
PointIdType::NumId(id) => id.into_bound_py_any(py),
PointIdType::Uuid(uuid) => uuid.into_bound_py_any(py),
}
}
}

View File

@@ -1,31 +1,34 @@
use std::mem;
use derive_more::Into;
use pyo3::prelude::*;
use segment::data_types::order_by::OrderValue;
use shard::operations::point_ops::VectorStructPersisted;
use shard::retrieve::record_internal::RecordInternal;
use crate::*;
#[pyclass(name = "Record")]
#[derive(Clone, Debug, Into)]
#[repr(transparent)]
pub struct PyRecord(pub RecordInternal);
impl PyRecord {
pub fn from_rust_vec(records: Vec<RecordInternal>) -> Vec<Self> {
// `PyRecord` has transparent representation, so transmuting is safe
unsafe { mem::transmute(records) }
}
}
#[pymethods]
impl PyRecord {
#[getter]
pub fn id(&self) -> PyPointId {
PyPointId::from(self.0.id)
PyPointId(self.0.id)
}
#[getter]
pub fn vector(&self) -> Option<PyVector> {
let Some(vector) = &self.0.vector else {
return None;
};
let vector_persisted = VectorStructPersisted::from(vector.clone());
Some(PyVector(vector_persisted))
pub fn vector(&self) -> Option<&PyVectorInternal> {
self.0.vector.as_ref().map(PyVectorInternal::from_ref)
}
#[getter]
@@ -41,7 +44,6 @@ impl PyRecord {
#[derive(IntoPyObject)]
pub enum PyOrderValue {
// Put Int first so ints don't get parsed as floats (since f64 can extract from ints).
Int(i64),
Float(f64),
}
@@ -49,8 +51,8 @@ pub enum PyOrderValue {
impl From<OrderValue> for PyOrderValue {
fn from(value: OrderValue) -> Self {
match value {
OrderValue::Int(int) => PyOrderValue::Int(int),
OrderValue::Float(float) => PyOrderValue::Float(float),
OrderValue::Int(int) => Self::Int(int),
OrderValue::Float(float) => Self::Float(float),
}
}
}

View File

@@ -1,87 +1,85 @@
use std::collections::HashMap;
use std::mem;
use derive_more::Into;
use pyo3::{pyclass, pymethods};
use segment::data_types::vectors::DenseVector;
use pyo3::IntoPyObjectExt as _;
use pyo3::prelude::*;
use segment::data_types::vectors::{DenseVector, VectorStructInternal};
use segment::types::VectorNameBuf;
use shard::operations::point_ops::{VectorPersisted, VectorStructPersisted};
use sparse::common::sparse_vector::SparseVector;
use sparse::common::types::{DimId, DimWeight};
#[pyclass(name = "Vector")]
#[derive(Clone, Debug, Into)]
pub struct PyVector(pub VectorStructPersisted);
pub struct PyVector(VectorStructPersisted);
#[pymethods]
impl PyVector {
#[staticmethod]
pub fn single(vector: DenseVector) -> Self {
Self(VectorStructPersisted::Single(vector))
}
#[staticmethod]
pub fn multi_dense(vectors: Vec<DenseVector>) -> Self {
Self(VectorStructPersisted::MultiDense(vectors))
}
#[staticmethod]
pub fn named(vectors: HashMap<VectorNameBuf, PyNamedVector>) -> Self {
// TODO: Transmute!?
let vectors = vectors
.into_iter()
.map(|(name, vector)| (name, vector.into()))
.collect();
Self(VectorStructPersisted::Named(vectors))
}
}
impl PyVector {
fn _variants(vector: VectorStructPersisted) {
match vector {
VectorStructPersisted::Single(_) => (),
VectorStructPersisted::MultiDense(_) => (),
VectorStructPersisted::Named(_) => (),
impl<'py> FromPyObject<'py> for PyVector {
fn extract_bound(vector: &Bound<'py, PyAny>) -> PyResult<Self> {
#[derive(FromPyObject)]
enum Helper {
Single(DenseVector),
Multi(Vec<DenseVector>),
Named(HashMap<VectorNameBuf, PyNamedVector>),
}
let vector = match vector.extract()? {
Helper::Single(single) => VectorStructPersisted::Single(single),
Helper::Multi(multi) => VectorStructPersisted::MultiDense(multi),
Helper::Named(named) => {
let named = PyNamedVector::into_rust_map(named);
VectorStructPersisted::Named(named)
}
};
Ok(Self(vector))
}
}
#[pyclass(name = "NamedVector")]
#[derive(Clone, Debug, Into)]
pub struct PyNamedVector(VectorPersisted);
#[pymethods]
impl PyNamedVector {
#[staticmethod]
pub fn dense(vector: DenseVector) -> Self {
Self(VectorPersisted::Dense(vector))
}
#[staticmethod]
pub fn multi_dense(vectors: Vec<DenseVector>) -> Self {
Self(VectorPersisted::MultiDense(vectors))
}
#[staticmethod]
pub fn sparse(vector: PySparseVector) -> Self {
Self(VectorPersisted::Sparse(vector.into()))
}
}
#[repr(transparent)]
struct PyNamedVector(VectorPersisted);
impl PyNamedVector {
fn _variants(vector: VectorPersisted) {
match vector {
VectorPersisted::Dense(_) => (),
VectorPersisted::MultiDense(_) => (),
VectorPersisted::Sparse(_) => (),
pub fn into_rust_map(
vectors: HashMap<VectorNameBuf, Self>,
) -> HashMap<VectorNameBuf, VectorPersisted> {
unsafe { mem::transmute(vectors) }
}
}
impl<'py> FromPyObject<'py> for PyNamedVector {
fn extract_bound(vector: &Bound<'py, PyAny>) -> PyResult<Self> {
#[derive(FromPyObject)]
enum Helper {
Dense(DenseVector),
MultiDense(Vec<DenseVector>),
Sparse(PySparseVector),
}
let vector = match vector.extract()? {
Helper::Dense(dense) => VectorPersisted::Dense(dense),
Helper::MultiDense(multi) => VectorPersisted::MultiDense(multi),
Helper::Sparse(sparse) => {
let sparse = PySparseVector::into_rust(sparse);
VectorPersisted::Sparse(sparse)
}
};
Ok(Self(vector))
}
}
#[pyclass(name = "SparseVector")]
#[derive(Clone, Debug, Into)]
#[repr(transparent)]
pub struct PySparseVector(SparseVector);
impl PySparseVector {
pub fn into_rust(self) -> SparseVector {
unsafe { mem::transmute(self) }
}
}
#[pymethods]
impl PySparseVector {
#[new]
@@ -90,12 +88,46 @@ impl PySparseVector {
}
#[getter]
pub fn indices(&self) -> Vec<DimId> {
self.0.indices.clone()
pub fn indices(&self) -> &[DimId] {
&self.0.indices
}
#[getter]
pub fn values(&self) -> Vec<DimWeight> {
self.0.values.clone()
pub fn values(&self) -> &[DimWeight] {
&self.0.values
}
}
#[derive(Clone, Debug, Into)]
#[repr(transparent)]
pub struct PyVectorInternal(pub VectorStructInternal);
impl PyVectorInternal {
pub fn from_ref(vector: &VectorStructInternal) -> &Self {
unsafe { mem::transmute(vector) }
}
}
impl<'py> IntoPyObject<'py> for PyVectorInternal {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr; // Infallible
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
IntoPyObject::into_pyobject(&self, py)
}
}
impl<'py> IntoPyObject<'py> for &PyVectorInternal {
type Target = PyAny;
type Output = Bound<'py, Self::Target>;
type Error = PyErr; // Infallible
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
match &self.0 {
VectorStructInternal::Single(single) => single.into_bound_py_any(py),
VectorStructInternal::MultiDense(_multi) => todo!(),
VectorStructInternal::Named(_named) => todo!(),
}
}
}

View File

@@ -33,7 +33,7 @@ impl PyPoint {
#[new]
pub fn new(id: PyPointId, vector: PyVector, payload: Option<PyPayload>) -> Result<Self, PyErr> {
let point = PointStructPersisted {
id: PointIdType::try_from(id)?,
id: PointIdType::from(id),
vector: VectorStructPersisted::from(vector),
payload: payload.map(Payload::from),
};