Files
qdrant/lib/edge/python/src/lib.rs
qdrant-cloud-bot 2ccbd9ed82 refactor(edge): split EdgeShard load into new() and load() (#8326)
* refactor(edge): split EdgeShard load into new() and load()

- new(path, config): create edge shard only when path has no existing segments
- load(path, config?): load from existing files; config optional (load from
  edge_config.json or infer from segments)
- Helpers: has_existing_segments, ensure_dirs_and_open_wal, resolve_initial_config,
  load_segments, ensure_appendable_segment
- Python: EdgeShard.create_new(path, config); __init__ still calls load()
- Examples use new() for creation and load(path, None) for reopen
- Error instead of panic on config mismatch; explicit load/create in Python
- fix: use OptimizerThresholds.deferred_internal_id for dev compatibility

Made-with: Cursor

* rollback threshold changes

* fix(edge): address dancixx review comments

- Persist inferred config in load() when edge_config.json does not exist
  (SaveOnDisk::new only saves when file exists)
- Python: add #[new] delegating to load() for backward compatibility
- qdrant_edge.pyi: Optional return types for deleted_threshold,
  vacuum_min_vector_number, default_segment_number; add __init__ doc

Made-with: Cursor

* Revert "fix(edge): address dancixx review comments"

This reverts commit 370e21a6c74c41210e1658f89814395cb86ed81c.

* fix: optional returns

* fix: save config it is not exist

* fix: save data on disk always

* fix: python examples

* chore: remove edge.close()

* fix: wal lock

* fix: rename of EdgeShardConfig -> EdgeConfig

---------

Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com>
Co-authored-by: Daniel Boros <dancixx@gmail.com>
2026-03-26 18:23:05 +01:00

226 lines
7.2 KiB
Rust

pub mod config;
pub mod count;
pub mod facet;
pub mod info;
pub mod query;
pub mod repr;
pub mod scroll;
pub mod search;
pub mod snapshots;
pub mod types;
pub mod update;
pub mod utils;
use std::path::PathBuf;
use bytemuck::TransparentWrapperAlloc as _;
use edge::EdgeConfig;
use pyo3::exceptions::PyException;
use pyo3::prelude::*;
use segment::common::operation_error::OperationError;
use segment::types::*;
use self::config::*;
use self::count::*;
use self::facet::*;
use self::info::*;
use self::query::*;
use self::scroll::*;
use self::search::*;
use self::types::*;
use self::update::*;
#[pymodule]
mod qdrant_edge {
#[pymodule_export]
use super::PyEdgeShard;
#[pymodule_export]
use super::config::quantization::{
PyBinaryQuantizationConfig, PyBinaryQuantizationEncoding,
PyBinaryQuantizationQueryEncoding, PyCompressionRatio, PyProductQuantizationConfig,
PyScalarQuantizationConfig, PyScalarType,
};
#[pymodule_export]
use super::config::sparse_vector_data::{PyEdgeSparseVectorParams, PyModifier};
#[pymodule_export]
use super::config::vector_data::{
PyDistance, PyEdgeVectorParams, PyHnswIndexConfig, PyMultiVectorComparator,
PyMultiVectorConfig, PyPlainIndexConfig, PyVectorStorageDatatype,
};
#[pymodule_export]
use super::config::{PyEdgeConfig, PyEdgeOptimizersConfig};
#[pymodule_export]
use super::count::PyCountRequest;
#[pymodule_export]
use super::facet::{PyFacetHit, PyFacetRequest, PyFacetResponse};
#[pymodule_export]
use super::query::{
PyDirection, PyFusion, PyMmr, PyOrderBy, PyPrefetch, PyQueryRequest, PySample,
};
#[pymodule_export]
use super::scroll::PyScrollRequest;
#[pymodule_export]
use super::search::{
PyAcornSearchParams, PyQuantizationSearchParams, PySearchParams, PySearchRequest,
};
#[pymodule_export]
use super::types::filter::{
PyFieldCondition, PyFilter, PyGeoBoundingBox, PyGeoPoint, PyGeoPolygon, PyGeoRadius,
PyHasIdCondition, PyHasVectorCondition, PyIsEmptyCondition, PyIsNullCondition, PyMatchAny,
PyMatchExcept, PyMatchPhrase, PyMatchText, PyMatchTextAny, PyMatchValue, PyMinShould,
PyNestedCondition, PyRangeDateTime, PyRangeFloat, PyValuesCount,
};
#[pymodule_export]
use super::types::formula::{PyDecayKind, PyExpressionInterface, PyFormula};
#[pymodule_export]
use super::types::payload_schema::{
PyBoolIndexParams, PyDatetimeIndexParams, PyFloatIndexParams, PyGeoIndexParams,
PyIntegerIndexParams, PyKeywordIndexParams, PyLanguage, PyPayloadSchemaType,
PySnowballLanguage, PySnowballParams, PyStopwordsSet, PyTextIndexParams, PyTokenizerType,
PyUuidIndexParams,
};
#[pymodule_export]
use super::types::query::{
PyContextPair, PyContextQuery, PyDiscoverQuery, PyFeedbackItem, PyFeedbackNaiveQuery,
PyNaiveFeedbackCoefficients, PyPayloadSelectorInterface, PyQueryInterface,
PyRecommendQuery,
};
#[pymodule_export]
use super::types::{PyPoint, PyPointVectors, PyRecord, PyScoredPoint, PySparseVector};
#[pymodule_export]
use super::update::{PyUpdateMode, PyUpdateOperation};
}
#[pyclass(name = "EdgeShard")]
#[derive(Debug)]
pub struct PyEdgeShard(Option<edge::EdgeShard>);
#[pymethods]
impl PyEdgeShard {
/// Load an edge shard from existing files at `path`.
/// Optional `config`: if provided, compatibility is checked and config is overwritten on disk.
#[staticmethod]
#[pyo3(signature = (path, config = None))]
pub fn load(path: PathBuf, config: Option<PyEdgeConfig>) -> Result<Self> {
let shard = edge::EdgeShard::load(&path, config.map(EdgeConfig::from))?;
Ok(Self(Some(shard)))
}
/// Create a new edge shard at `path` with the given configuration.
/// Fails if the path already contains segment data.
#[staticmethod]
pub fn create(path: PathBuf, config: PyEdgeConfig) -> Result<Self> {
let shard = edge::EdgeShard::new(&path, config.0)?;
Ok(Self(Some(shard)))
}
pub fn flush(&self) -> Result<()> {
self.get_shard()?.flush();
Ok(())
}
pub fn optimize(&self) -> Result<bool> {
let optimized = self.get_shard()?.optimize()?;
Ok(optimized)
}
pub fn close(&mut self) {
self.0.take(); // `edge::Shard` is automatically flushed on drop
}
pub fn update(&self, operation: PyUpdateOperation) -> Result<()> {
self.get_shard()?.update(operation.into())?;
Ok(())
}
pub fn query(&self, query: PyQueryRequest) -> Result<Vec<PyScoredPoint>> {
let points = self.get_shard()?.query(query.into())?;
let points = PyScoredPoint::wrap_vec(points);
Ok(points)
}
pub fn search(&self, search: PySearchRequest) -> Result<Vec<PyScoredPoint>> {
let points = self.get_shard()?.search(search.into())?;
let points = PyScoredPoint::wrap_vec(points);
Ok(points)
}
pub fn scroll(&self, scroll: PyScrollRequest) -> Result<(Vec<PyRecord>, Option<PyPointId>)> {
let (points, next_offset) = self.get_shard()?.scroll(scroll.into())?;
let points = PyRecord::wrap_vec(points);
Ok((points, next_offset.map(PyPointId)))
}
pub fn count(&self, count: PyCountRequest) -> Result<usize> {
let points_count = self.get_shard()?.count(count.into())?;
Ok(points_count)
}
pub fn facet(&self, facet: PyFacetRequest) -> Result<PyFacetResponse> {
let response = self.get_shard()?.facet(facet.into())?;
Ok(PyFacetResponse::new(response))
}
pub fn retrieve(
&self,
point_ids: Vec<PyPointId>,
with_payload: Option<PyWithPayload>,
with_vector: Option<PyWithVector>,
) -> Result<Vec<PyRecord>> {
let point_ids = PyPointId::peel_vec(point_ids);
let points = self.get_shard()?.retrieve(
&point_ids,
with_payload.map(WithPayloadInterface::from),
with_vector.map(WithVector::from),
)?;
let points = PyRecord::wrap_vec(points);
Ok(points)
}
pub fn info(&self) -> Result<PyShardInfo> {
let info = self.get_shard()?.info();
let info = PyShardInfo(info);
Ok(info)
}
// ------- Snapshot related methods -------
#[staticmethod]
pub fn unpack_snapshot(snapshot_path: PathBuf, target_path: PathBuf) -> Result<()> {
edge::EdgeShard::unpack_snapshot(&snapshot_path, &target_path)?;
Ok(())
}
pub fn snapshot_manifest(&self) -> Result<PyValue> {
let manifest = self.get_shard()?.snapshot_manifest()?;
Ok(PyValue::new(serde_json::to_value(&manifest).unwrap()))
}
#[pyo3(signature = (snapshot_path, tmp_dir=None))]
pub fn update_from_snapshot(
&mut self,
snapshot_path: PathBuf,
tmp_dir: Option<PathBuf>,
) -> Result<()> {
self._update_from_snapshot(snapshot_path, tmp_dir)?;
Ok(())
}
}
pub type Result<T, E = PyError> = std::result::Result<T, E>;
#[derive(Debug)]
pub struct PyError(OperationError);
impl From<OperationError> for PyError {
fn from(err: OperationError) -> Self {
Self(err)
}
}
impl From<PyError> for PyErr {
fn from(err: PyError) -> Self {
PyException::new_err(err.0.to_string())
}
}