qdrant-edge 0.8.0

A lightweight, in-process vector search engine designed for embedded devices, autonomous systems, and mobile agents.
Documentation
use std::path::Path;
use std::sync::Arc;

use crate::common::universal_io::{MmapFile, MmapFs};
use parking_lot::RwLock;
use crate::segment::common::operation_error::OperationResult;
use crate::segment::data_types::load_profile::LoadProfile;
use crate::segment::index::UniversalReadExt;

use crate::edge::EdgeConfig;
use crate::edge::read_only::ReadOnlyEdgeShard;
use crate::edge::read_only::enumerate::{ManifestSegmentEnumerator, SegmentEnumerator};
use crate::edge::read_only::holder::ReadOnlySegmentHolder;

impl ReadOnlyEdgeShard<MmapFile> {
    /// Open a read-only follower over local memory-mapped files, discovering segments from the
    /// leader's segment manifest. Requires the leader to write a manifest (the
    /// `write_segment_manifest` feature flag).
    pub fn open_mmap(path: &Path) -> OperationResult<Self> {
        Self::open(MmapFs, path, None, None)
    }
}

/// Effective follower config: tunables prefer the caller-`provided` config and fall back to the
/// segment-`derived` one (via [`EdgeConfig::fill_unspecified_from`]), while `vectors` and
/// `sparse_vectors` always come from `derived` — the segments are the follower's source of truth
/// for the stored data.
pub(super) fn merge_follower_config(provided: EdgeConfig, mut derived: EdgeConfig) -> EdgeConfig {
    // Take the vector params out of `derived` up front, so caller-provided ones cannot win in
    // the fill below (a non-empty map would count as "specified").
    let vectors = std::mem::take(&mut derived.vectors);
    let sparse_vectors = std::mem::take(&mut derived.sparse_vectors);

    let mut config = provided.fill_unspecified_from(&derived);
    config.vectors = vectors;
    config.sparse_vectors = sparse_vectors;
    config
}

impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
    /// Open a read-only follower over the edge-shard directory at `path` using read backend `fs`.
    ///
    /// Segments are discovered through the leader's segment manifest (read via `fs`) — a read-only
    /// follower always uses the manifest, so there is no discovery to configure.
    ///
    /// A follower has no `edge_config.json` — the segments are the source of truth, so the config is
    /// derived from the segments themselves (see [`EdgeConfig::from_segment_config`]), mirroring the
    /// read-write [`EdgeShard`](crate::EdgeShard)'s fallback. A provided `config` overrides tunable
    /// parameters at open (its `Some` values win over the derived ones; `vectors`/`sparse_vectors`
    /// are ignored); a [`refresh`](Self::refresh) re-derives the config from the segments alone.
    /// An empty shard (no segments yet) starts from the provided config (or a default one).
    ///
    /// A `load_profile` — derived from the request this shard is being opened to serve (see
    /// [`LoadProfile`]) — parks the segment components that request won't touch cold instead of
    /// warming them per the persisted segment configs, cutting the cold-start cost. Without one,
    /// loading follows the segment configs alone. The profile also applies to segments a later
    /// [`refresh`](Self::refresh) discovers: the shard was opened for that one request, so new
    /// segments shouldn't load any warmer.
    pub fn open(
        fs: S::Fs,
        path: &Path,
        config: Option<EdgeConfig>,
        load_profile: Option<LoadProfile>,
    ) -> OperationResult<Self>
    where
        S::Fs: Send + Sync + Clone + 'static,
    {
        let enumerator = ManifestSegmentEnumerator::new(fs.clone(), path);
        Self::open_with_enumerator(fs, path, enumerator, config, load_profile)
    }

    /// Open with an explicit segment [`enumerator`](SegmentEnumerator).
    ///
    /// Internal seam: [`open`](Self::open) always discovers via the manifest, but tests inject other
    /// discovery strategies (e.g. a directory scan) here.
    ///
    /// The initial load is a [`refresh`](Self::refresh) over an empty shard: same discovery, same
    /// parallel load, same handling of segments that change while being loaded. The manifest is
    /// superset-biased, so segments that cannot be loaded (not yet finalized, already deleted, or
    /// appendable) are skipped rather than failing the open.
    pub(crate) fn open_with_enumerator(
        fs: S::Fs,
        path: &Path,
        enumerator: impl SegmentEnumerator + 'static,
        config: Option<EdgeConfig>,
        load_profile: Option<LoadProfile>,
    ) -> OperationResult<Self>
    where
        S::Fs: Send + Sync + Clone + 'static,
    {
        let provided_config = config.unwrap_or_default();

        // Segments never carry `max_search_threads` / `search_pool_core`, so the pool is sized and
        // pinned from the caller-provided config alone: the CPU-derived default unless set.
        let search_pool = crate::edge::read_view::build_segment_pool(
            "edge-search",
            provided_config.search_thread_count(),
            provided_config.search_pool_core,
        )?;

        let shard = Self {
            path: path.to_path_buf(),
            fs,
            config: RwLock::new(Arc::new(provided_config.clone())),
            segments: RwLock::new(ReadOnlySegmentHolder::default()),
            enumerator: Box::new(enumerator),
            search_pool,
            load_profile,
        };
        shard.refresh()?;

        // Open-only config overlay: caller-provided tunables win over the segment-derived ones
        // (`refresh` re-derives from the segments alone — see the `config` field docs). For an
        // empty shard the refresh left the provided config in place, and the overlay is a no-op.
        let derived = shard.config.read().clone();
        *shard.config.write() = Arc::new(merge_follower_config(
            provided_config,
            EdgeConfig::clone(&derived),
        ));

        Ok(shard)
    }
}