Make EdgeConfig tunables optional with a layered fallback chain (#9714)

Tunable EdgeConfig parameters (on_disk_payload, hnsw_config, optimizers)
are now Option, and every tunable resolves through the fallback chain
provided -> persisted -> derived from segments -> default when loading
an existing shard. Leaving a parameter unspecified keeps the shard as it
is; an explicit value overwrites it and existing segments converge to it
through the optimizers.

vectors/sparse_vectors are excluded from overwrite semantics: an empty
map inherits the persisted/segment-derived definitions, a non-empty map
is validated for compatibility against the loaded segments (size,
distance, multivector, datatype, sparse modifier) and fails the load on
mismatch.

The derived layer folds over all segments in UUID order instead of
taking an arbitrary first segment, so a plain appendable segment (which
carries no HNSW parameters) can never mask an indexed segment's actual
build parameters. Previously a lost edge_config.json could resolve
unspecified HNSW params to compiled-in defaults and silently trigger a
full re-index via ConfigMismatchOptimizer.

The read-only follower accepts an optional config on open: provided
tunables are applied once over the segment-derived config (vectors
always come from the segments), and refresh re-derives from segments
alone.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Andrey Vasnetsov
2026-07-07 11:45:02 +02:00
committed by generall
parent 500c68ecbb
commit 2bb287d693
13 changed files with 615 additions and 151 deletions

View File

@@ -254,7 +254,7 @@ class EdgeConfig:
Union["EdgeVectorParams", Dict[str, "EdgeVectorParams"]]
] = None,
sparse_vectors: Optional[Dict[str, "EdgeSparseVectorParams"]] = None,
on_disk_payload: bool = True,
on_disk_payload: Optional[bool] = None,
hnsw_config: Optional["HnswIndexConfig"] = None,
quantization_config: Optional[QuantizationConfigType] = None,
optimizers: Optional["EdgeOptimizersConfig"] = None,
@@ -263,12 +263,19 @@ class EdgeConfig:
"""
Create an EdgeConfig.
Parameters left as None are "not specified": when loading an existing shard each
one resolves through provided -> persisted -> derived from segments -> default,
so an unspecified parameter keeps the shard as it is. vectors and sparse_vectors
define the stored data: if provided they are validated for compatibility against
the existing segments, if omitted they are inherited from the shard.
Args:
vectors: Dense vector configuration. Can be a single EdgeVectorParams for
the default vector (name "") or a dict of name -> EdgeVectorParams.
Optional if sparse_vectors is provided (sparse-only config).
sparse_vectors: Optional sparse vector configurations.
on_disk_payload: If True, store payload on disk (mmap); otherwise in RAM.
None keeps the shard's current value (defaults to on-disk).
hnsw_config: Optional global HNSW config (used when building HNSW index).
quantization_config: Optional global quantization config.
optimizers: Optional optimizer settings.
@@ -290,13 +297,13 @@ class EdgeConfig:
...
@property
def on_disk_payload(self) -> bool:
"""Whether payload is stored on disk."""
def on_disk_payload(self) -> Optional[bool]:
"""Whether payload is stored on disk, or None if not specified."""
...
@property
def hnsw_config(self) -> "HnswIndexConfig":
"""Global HNSW config."""
def hnsw_config(self) -> Optional["HnswIndexConfig"]:
"""Global HNSW config, or None if not specified."""
...
@property
@@ -305,8 +312,8 @@ class EdgeConfig:
...
@property
def optimizers(self) -> "EdgeOptimizersConfig":
"""Optimizer settings."""
def optimizers(self) -> Optional["EdgeOptimizersConfig"]:
"""Optimizer settings, or None if not specified."""
...
@property

View File

@@ -25,13 +25,13 @@ pub struct PyEdgeConfig(pub EdgeConfig);
#[pymethods]
impl PyEdgeConfig {
#[new]
#[pyo3(signature = (vectors=None, sparse_vectors=None, on_disk_payload=true, hnsw_config=None, quantization_config=None, optimizers=None, max_search_threads=None))]
#[pyo3(signature = (vectors=None, sparse_vectors=None, on_disk_payload=None, hnsw_config=None, quantization_config=None, optimizers=None, max_search_threads=None))]
pub fn new(
#[pyo3(from_py_with = option_edge_vectors_helper)] vectors: Option<
HashMap<String, PyEdgeVectorParams>,
>,
sparse_vectors: Option<HashMap<String, PyEdgeSparseVectorParams>>,
on_disk_payload: bool,
on_disk_payload: Option<bool>,
hnsw_config: Option<PyHnswIndexConfig>,
quantization_config: Option<PyQuantizationConfig>,
optimizers: Option<PyEdgeOptimizersConfig>,
@@ -52,9 +52,9 @@ impl PyEdgeConfig {
on_disk_payload,
vectors,
sparse_vectors,
hnsw_config: hnsw_config.map(|h| h.0).unwrap_or_default(),
hnsw_config: hnsw_config.map(|h| h.0),
quantization_config: quantization_config.map(QuantizationConfig::from),
optimizers: optimizers.map(|o| o.0).unwrap_or_default(),
optimizers: optimizers.map(|o| o.0),
wal_options: None,
max_search_threads,
}))
@@ -71,13 +71,13 @@ impl PyEdgeConfig {
}
#[getter]
pub fn on_disk_payload(&self) -> bool {
pub fn on_disk_payload(&self) -> Option<bool> {
self.0.on_disk_payload
}
#[getter]
pub fn hnsw_config(&self) -> PyHnswIndexConfig {
PyHnswIndexConfig(self.0.hnsw_config)
pub fn hnsw_config(&self) -> Option<PyHnswIndexConfig> {
self.0.hnsw_config.map(PyHnswIndexConfig)
}
#[getter]
@@ -86,8 +86,8 @@ impl PyEdgeConfig {
}
#[getter]
pub fn optimizers(&self) -> PyEdgeOptimizersConfig {
PyEdgeOptimizersConfig(self.0.optimizers.clone())
pub fn optimizers(&self) -> Option<PyEdgeOptimizersConfig> {
self.0.optimizers.clone().map(PyEdgeOptimizersConfig)
}
#[getter]