Previous "Docstrings:" commits concatenated original class docstrings
with original __init__ docstrings. The reason was technical: CPython
won't let us easily add a docstring for the __new__ method. But this
resulted in weird-looking docstrings. This commit un-weirds them.
See also: https://www.github.com/PyO3/pyo3/issues/4326
This commit replaces hand-written stubs with auto-generated stubs.
Temporary quality drop (no docstrings, `Incomplete` annotations, etc);
subsequent commits will restore them.
This commit is made early in the stack so changes in stub generation
would be visible in subsequent commits.
One TrackerRead trait, in lib/blobstore/src/tracker/read.rs, now covers
the Gridstore Tracker and ReadOnlyTracker and the Logstore
AppendOnlyTracker. LogstoreView, LogstoreReader, and validate_consistency
are generic over it, as GridstoreView already was over the Gridstore-only
trait.
The trait gains get_range and an access pattern on get, and its iter
returns impl Iterator instead of the concrete Gridstore Iter, which drops
the storage type parameter from the trait. The Gridstore trackers
implement get_range through a new read_slots helper; nothing in Gridstore
calls it yet. The lifecycle methods of LogstoreReader (open, preopen,
files, live preload, live reload, clear cache) stay in an impl block bound
to AppendOnlyTracker, so a future immutable in-RAM tracker plugs into the
read path without inheriting reload semantics.
No runtime change: Gridstore slot reads keep the Random access pattern,
and PointerItem stays the iterator item type.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Add a BM25-over-sparse baseline benchmark
The text payload index is meant to score about as fast as BM25 over sparse
vectors, so the number it has to match needs to exist before the scorer
does. Measures a local shard end to end, no HTTP.
- embeds the corpus through `lib/bm25` with its defaults, so the baseline
is the route a user migrates from rather than a reimplementation
- Zipf-like vocabulary. On a uniform one every term is equally selective,
IDF is flat and pruning has nothing to prune, which would flatter any
scorer measured against it
- two shards rather than one shard before and after optimization: a shard
that will optimize starts as soon as the upsert lands, so the first cut
timed a half-converted index and called it fresh. Both states assert
what they hold before anything is timed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Measure the sparse BM25 baseline where the routes separate
- 200k documents by default (BM25_SPARSE_DOCS overrides): at 20k every shape
of both routes measures the same and half of a shard-level query is the
shard; reachable through the shard since #10682
- a third shard with the sparse index on disk, which the optimized state never
exercised
- recall at 10 against BM25 by definition, printed per state: the default
avg_len of 256 on a corpus averaging 110 tokens misses a quarter of the true
top 10, so the optimized state is also timed with the corpus average
- corpus, queries, reference and recall move to segment::fixtures::bm25_corpus,
to be shared with the text-index bench and the comparison harness
- module doc: the sparse shapes measure within a few percent of each other; the
states exist for the text comparison
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Refuse an empty corpus in the sparse BM25 baseline
BM25_SPARSE_DOCS=0 built empty shards, made the average length NaN and
scored every empty truth as recall 1.0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Measure what the sparse BM25 baseline states claim
The fresh shard kept the default 10 MB indexing threshold and optimized
itself in the background, so it was timed as a second optimized state.
Disable indexing and re-check it after timing. Keep the shard storage
under CARGO_TARGET_TMPDIR so the on-disk index is not read from a tmpfs.
Fix doc comments that named missing files, a no-op IDF clamp and the
wrong reason for the empty-corpus guard.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* AI: Share reads across different diskcache pipelines
* AI: Unify scheduled reads into single ScheduledRead struct
* AI: Implement follower promotion on fetch abandonment
* AI: self-promote if waiting for an abandoned placeholder
* AI: simplify
* AI: use Weak<Placeholder> instead of manually counting strong refs
* AI: track abandoned waiters
* AI + manual: park with timeout
* AI: fix barrier in test
* fmt with latest nightly
* expect remote pipeline
* Carry document length into the immutable and on-disk text indexes
The mutable index records `doc_len`. The immutable and on-disk backends dropped
it on conversion. They now carry it and persist it as a sidecar, so a length
survives an optimizer run and a restart.
- `ImmutableInvertedIndex` gains `point_to_doc_len`, parallel to
`point_to_tokens_count` and zeroed wherever that vector is, so summing it
never counts a deleted document.
- `OnDiskInvertedIndex` writes `point_to_doc_len.dat`, only when the index
records lengths. `files()` lists it only when it exists, so a snapshot carries
it exactly when there is one.
- Deletions are masked on load, not at build time. The file is written once and
a point deleted later through the id-tracker is zeroed in
`TryFrom<&OnDiskInvertedIndex>`. No segment total is stored, since it could
only be summed after that masking.
- A sidecar shorter than the counts is treated as absent. It can only come from
a partially copied file set, and padding it would give every point past the
truncation a zero that reads like a real length.
- A missing sidecar on an index that should have one makes `new_mmap` report the
index absent, which routes into the existing rebuild from payload. The check
lives there rather than in `OnDiskInvertedIndex::open` because the read-only
stack never builds and would drop the field instead.
- `FullTextMmapIndexBuilder::add_many` reached below `index_str_tokens` and so
declared no length at all. It now tokenizes through the shared helper and
measures like the gridstore path.
Recording is still gated behind `TextIndexParams::scoring()`, so none of this is
written today.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Wipe the text index directory instead of its listed files
`files()` lists the `doc_len` sidecar only when the index loaded it, so a
truncated sidecar is omitted. `wipe` then deleted the listed files, failed to
remove the now non-empty directory, discarded that error and returned success,
leaving the directory and a stale sidecar behind.
Remove the directory itself. Each field owns its own `{field}-text` directory,
so this no longer depends on `files()` being an accurate inventory, and real
removal errors propagate rather than being swallowed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Drop the fully qualified visibility paths in the text index
`pub(in crate::index::field_index::full_text_index)` is tedious to read and to
keep correct, and it grants nothing here: `inverted_index` and
`mutable_text_index` are private modules of `full_text_index`, so their types
are not nameable from outside it whatever the field visibility says. Plain
`pub` is no wider in practice.
Widening `Storage` surfaced two `private_interfaces` warnings, since
`ZerocopyPostingValue` and `PostingListHeader` in `types.rs` were still behind
the long path; those move too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Harden the document length sidecar after review
- check for the sidecar before opening the index rather than after. The
open populates the whole file set, so the first start after scoring is
enabled would fault in every segment's postings only to discard them
- treat a sidecar that covers more points than the index as untrustworthy,
not just one that covers fewer, and warn with both counts the way
`SortedBlockIndex::open` does. A longer one used to be accepted and then
silently truncated when materialized
- unlink a stale sidecar when a build records no lengths. It was the only
file here that could outlive the build that wrote it, and `open` would
have read it as this build's
- say why an index is being rebuilt from payload instead of leaving a
silent full re-index announced at debug level
- read `phrase_matching` from the config in the mmap builder, like the
other two callers of the same helper pair, so the two halves of the
sentinel rule cannot drift apart
- assert the length invariant the writers actually maintain, and correct
what `files()` and the field comment claim
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Address review on the document length sidecar
- shrink the document length vector when it is handed over to the immutable index, it arrives with the mutable index's doubling capacity
- narrow the immutable index fields to pub(super), with a test-only accessor for the one reader outside the module
- let wipe propagate a missing index directory instead of treating it as success, nothing reaches it with the directory already gone
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Configurable key prefix for object storage snapshots
Snapshot object keys were always derived from the local snapshots path,
so every deployment sharing a bucket wrote under the same `snapshots/`
root. Each cloud config block gains an optional `prefix`, and objects
become `<prefix>/snapshots/...`.
The prefix is applied by wrapping the client in `PrefixStore`, so the
snapshot operations and the names returned by the API are unchanged.
Leading, trailing and repeated slashes are dropped, and an empty prefix
is a no-op.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Test that snapshot operations cannot escape the storage prefix
Hostile targets are handed to the cloud manager directly, past
`validate_snapshot_name`: parent references, absolute paths, encoded
slashes, backslashes and empty paths. Writes must stay under the prefix,
and objects planted outside the prefix must be invisible to list,
download, stream and delete.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Normalize empty prefix components
Refactor prefix handling to remove empty components.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Tim Visée <tim+github@visee.me>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Add `match: { substring }` filter condition
Unindexed `text` and `text_any` matching became token-aware in #10341 and
#10593. Users who relied on the old raw substring behaviour get it back as
an explicit condition: `match: { "substring": "..." }` selects points with a
string value containing the given string, byte-wise and case-sensitive,
consistent with exact keyword and prefix matching.
Execution: a keyword index (with or without the `prefix` option) serves the
condition by scanning its value dictionary and uniting the postings of the
matching keys; cardinality reuses the prefix estimator, generalised into
`keys_union_cardinality`. The per-point checker goes through the forward
index. Without a keyword index the condition falls back to reading the
payload. Text, bool, integer and uuid indexes decline it.
Strict mode: the condition requires the `KeywordMatch` capability, so with
`unindexed_filtering_retrieve: false` it is rejected on unindexed and on
text-indexed fields and allowed on any keyword index.
API: `MatchSubstring` in the REST `Match` union with regenerated OpenAPI,
gRPC `Match.substring = 12`, edge python `MatchSubstring`, edge ffi
`Match::Substring`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Test substring fallback on a text-indexed field, document estimator params
A text index cannot serve `substring`, so on a field that has only a text
index the condition runs through the payload fallback; only strict mode may
reject it. Pin that in the OpenAPI suite and reword the strict-mode unit
test comment, which read as if the text index itself blocked the query.
Also spell out what `keys` and `postings` mean in `keys_union_cardinality`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Serve `match: { substring }` from the keyword key dictionary
The condition used to enumerate keys through `MapIndexRead::for_each_value`,
which on the on-disk variant drags the whole `value_to_points` file through
`for_each_entry`, plus one random read per matching key for its postings
count. Query planning paid that scan in full, before deciding whether to use
the clause at all.
Route it through the `prefix_index.bin` key dictionary instead: front-coded
keys with their postings counts inline, no postings. Estimation now reads
keys only and never touches `value_to_points`; filtering takes the matched
key list and resolves postings in one batched read, as prefix matching
already does.
This makes the `prefix` option a requirement: a keyword index without it has
no key dictionary, so it declines the condition and falls back to the payload
scan, the same as a text index. Strict mode follows — substring now infers
`KeywordPrefix`, so `unindexed_filtering_retrieve: false` names
`keyword (with prefix: true)` as the index to create.
`PrefixIndex::for_each_key` reads blocks in ~1 MiB chunks rather than the
whole key section at once: a substring cannot be pruned by the block index,
so the one-shot read would grow with the dictionary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Sync MatchSubstring OpenAPI description with Rust docs
After routing substring matching through the prefix key dictionary,
the schema docstring required regenerating so OpenAPI stays consistent.
* Scan the map index keys for substring match without a dictionary
Without the keyword dictionary a substring condition was declined by the
field index and left to the per-point condition checker, which reads the
forward index for every candidate point. Enumerate the distinct keys of
`values_to_points` instead: the same one-pass-over-distinct-values shape as
the dictionary scan, only over a structure that interleaves keys with their
postings. Filtering and cardinality estimation are then always served, so
the condition can act as a primary clause on a plain keyword index.
Prefix matching keeps its per-point fallback: an ordered dictionary is what
makes a prefix a bounded range, and enumerating every key to answer one is
not a trade worth making implicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Reject substring matching in strict mode
A substring condition is answered by looking at every distinct value of the
field: no index gives it a bounded access path, so there is no index a user
could create to make it affordable. Reject it under strict mode instead,
wherever a filter reaches verification — read and write filters, nested
sub-filters, and prefetch filters.
Filter limits are now checked before the unindexed-field check, so the
rejection is not reported as "create an index for this key", advice that
would lead to the same rejection afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Update the strict mode substring test to the new rejection
The test asserted that substring filtering under strict mode asks for a
keyword index with the `prefix` option. It is now rejected whatever index
the field carries, so every case in the test gets the same answer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Estimate a substring condition without scanning
Counting the keys a substring matches costs the same scan as answering the
condition, and `filter` then repeats it to collect those keys. Report the
uninformed estimate instead — the one an unindexed condition has always
reported — and keep the primary clause, so the scan happens once, in
`filter`, and only when the planner picks the condition to drive iteration.
With no counts to collect, `substring_scan` collapses into `substring_keys`:
the in-RAM variants no longer look up a posting count per matched key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Don't to parse everything as UTF-8
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: qdrant-cloud-bot <111755117+qdrant-cloud-bot@users.noreply.github.com>
Co-authored-by: timvisee <tim@visee.me>
Snapshot storage was limited to S3 although the object_store dependency
already ships the GCS and Azure backends. `snapshots_storage` now accepts
`gcs` (alias `gcp`) and `azure`, configured through `gcs_config` and
`azure_config` blocks next to the existing `s3_config`. The legacy S3
shape is unchanged.
Client construction is split into one builder per backend, all sharing
the Qdrant user agent and the plain-HTTP rule for `http://` endpoints.
Startup warns when a config block for an unselected backend is present.
The e2e snapshot recovery test is parameterized over the cloud backends.
The GCS case is skipped because fake-gcs-server does not implement the
XML multipart upload API that object_store uses for GCS uploads.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
A value that tokenizes to nothing is indexed and matches nothing. The mmap
build already treats it as no document, folding it into the "no tokens"
mask, while the mutable index counted it, so `points_count` for the same
data depended on whether the segment had been optimized yet.
- `index_tokens` counts a transition rather than incrementing, so a point
becomes a document when it gains its first token and stops being one when
rewritten to nothing. Re-indexing the same point no longer counts it twice
- `remove` decrements only when the removed token set had tokens
- the builder counts the same way
`points_count` feeds `count_indexed_points` for cardinality estimation, and
is `N` for the IDF of a text score.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(segment): replace timing-based building cancellation test
`test_building_cancellation` compared wall-clock times of independent
builds cancelled after fractions of a measured baseline. Its tolerances
had to be widened repeatedly (#2039, #8243, #10346) and it still failed on
Windows CI, e.g. when an early stop landed in a slow non-cancellable setup
step (time_early: 969, time_later: 631, baseline 2488).
Replace it with tests that target build phases explicitly and measure
work instead of time:
- test_building_cancelled_before_start: a build cancelled up front returns
`Cancelled` without starting any vector index work.
- test_building_cancelled_during_main_graph: a watcher cancels the build
once the main HNSW graph (observed via the progress tracker) reaches
1000 of 10000 points. The build must return `Cancelled`, leave the graph
unfinished, and insert at most 2 points per build thread after the flag
is set (only in-flight insertions may complete).
Both also check that a cancelled build leaves nothing behind in the
segments or temp directory. Use random vectors with a fixed seed instead
of identical zero vectors.
Verified the main-graph test fails when the per-point stop check is
removed, or only done every 64 points (102 points after cancel, limit 16).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(segment): cover cancellation during HNSW graph healing
it: an uncancellable heal phase is what blocked consensus when removing a
replica during an optimization.
Track progress of the `migrate` phase (healed `(point, level)` pairs out of
the total), like `main_graph` already does. Besides showing healing
progress in optimization telemetry, this lets a test tell "stopped right
away" apart from "healed everything, then noticed the flag"; both return
`Cancelled`, because the flag is checked again right after healing.
Generalize the main-graph cancellation test into a helper that cancels
any phase at 10% of its work and checks that at most 2 items per build
thread complete afterwards, and add test_building_cancelled_during_heal:
it builds an HNSW segment, deletes a quarter of its points (below the
default healing_threshold of 0.3), and cancels the rebuild while healing.
Verified the test fails with the heal stop check removed ("migrate was
completed despite cancellation"). Measured: 8 items after cancel with 8
build threads, out of 3000-5700 to heal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Warn when distributed mode has API key but enforce_internal_auth is off
Make the insecure internal (p2p) gRPC configuration visible at startup so
operators enable enforcement after rolling upgrades complete.
* Shorten enforce_internal_auth warning message
Drop the rolling-upgrade guidance from the log line.
* test(consensus): prove leader removal can stall
A departing leader can clear peer addresses before its queued commit
notification reaches the surviving voter. Control the removal append
and its acknowledgement to reproduce that ordering without fixed sleeps
or stopping the leader process.
Check the actual notification failure and the survivor's commit index.
Keep follower-removal and three-voter controls, and cover both the default
removal wait and timeout=60.
* test: timeout=60s with follower or leader
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: fail when leader removal loses its commit
Require the surviving voter to receive the committed removal and remain
operational. Fail explicitly when the commit notification never reaches
its gate instead of treating the stuck cluster as success.
Both two-node leader-removal cases now fail against the existing bug.
The follower-removal and three-node controls pass.
---------
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: add a gated proxy for peer RPCs
Pause one selected internal request while other peer traffic continues.
Preserve payloads, metadata, deadlines, and cancellation so consensus
tests can control transfer timing without blocking unrelated requests.
Cover forwarding, independent gates, and cleanup with socket tests.
* test: connect peer proxies to consensus clusters
Let consensus tests route internal RPCs through request gates. Keep each
proxy alive across peer restarts so advertised addresses remain stable,
and close all proxies during test cleanup.
Wait for the upstream gRPC connection before returning from proxied
startup. Verify consensus progress during a held WAL-delta request,
recovery data, and restart behavior with both URI configuration modes.
* test: fix potentially misleading peer proxy method names
Explicitly state the guarantees, or lack of.
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: add support for hold_snapshot_download
Removes flakiness from snapshot-related consensus tests too
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: improve asserts when force deleting peer
Actually verify survivors recover and retain the expected data.
Making sure no data loss happens.
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: add OsError socket handling + explicit wal_delta tests
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: reject zero as a defined consensus leader
* test: recheck leader agreement on each poll
After a restart, the leader can change during election. Let the cluster
wait resample the leader on each poll and require agreement on a nonzero
leader before the snapshot test starts its transfer.
Keep explicit leader checks for existing callers, membership-size checks,
and the existing timeout. Cover election changes and offline peers.
* test: verify independent snapshot download gates
* test: use a positive peer connection deadline
* test: cover recovery after the removed source exits
* chore: add clarifying comment on timeout=0 usage
It's not obvious at first why it's like so.
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: share consensus response gates
Move response gates, their tests, and Raft decoding from the leader
removal proof into the base test infrastructure. Both removal scenarios
can then use the same successful-response check.
* test: support selective RPC blocking
Keep a removed source unaware of membership changes while its transfer
continues. Block its Raft traffic in both directions so election
attempts cannot disrupt survivor recovery.
* test: make source removal scenarios deterministic
Separate recovery after source exit from late data sent by a removed
source. Require a successful receiver response in the late scenario,
and retain complete data and replica-state checks in both cases.
* test: refactor timeouts and deadlines
* Cancellation happens after observing the intended phase, without an RPC deadline.
* Separate deadline tests cover held requests, upstream work, and held responses.
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: bound peer probes and removal requests
Give cluster probes and peer removal finite client timeouts so a stalled
HTTP request cannot leave the test waiting indefinitely.
* test: separate RPC release from termination
Keep the upstream handler blocked until the test releases it or the RPC
terminates. Use a separate termination event for cancellation assertions,
and release the handler during teardown instead of racing a fixture timer.
* test: use monotonic polling deadlines
Measure elapsed polling time with a monotonic clock so system clock
adjustments cannot shorten or extend the wait.
* test: allow more time to observe proxy events
Allow ten seconds for proxy observations and ordinary test requests.
Event and future waits still return as soon as they complete. Keep the
one-second expiry tests and document the HTTP deadline setup race.
* test: bound leader and replication requests
Limit how long leader lookup and transfer submission wait for an HTTP
response. A stalled submission must fail so the test can release its
transfer gates and clean up the peers.
* test: preserve readiness failures in diagnostics
Catch request failures while collecting cluster diagnostics, including
read timeouts. Report the original readiness failure instead of replacing
it with a diagnostic error.
* test: assert points calls for the correct collection
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: make sure check_cluster_size and check_leader cannot stall
Have an explicit timeout.
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* test: retry timeouts during initial leader lookup
Treat request timeouts as retryable while discovering the expected leader,
matching the subsequent leader and membership checks. Keep polling after
a transient timeout instead of aborting the cluster-status wait.
* test: ensure batch data is different
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
---------
Signed-off-by: Anton Antonov <anton.synd.antonov@gmail.com>
* Split additional links phase out of HNSWIndex::build
Move the per-payload-block subgraph phase, together with its existing
helpers condition_points and build_filtered_graph, from HNSWIndex::build
into hnsw/build/additional_links.rs. The phase body is moved verbatim;
the borrowed locals become explicit parameters, and the function returns
the number of vectors indexed through the subgraphs.
GPU insert context creation moves to gpu_build.rs next to the other GPU
setup helpers, so build.rs no longer needs feature-gated GPU imports.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Split main graph CPU phase out of HNSWIndex::build
Move old-index healing, the single-threaded warm-up and the parallel
point insertion into hnsw/build/main_graph.rs as build_main_graph_on_cpu,
mirroring build_main_graph_on_gpu. Level assignment and the GPU attempt
stay in the orchestrator, since the GPU result decides whether the CPU
path runs at all.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Extract config derivation, field subtasks and thread pool from HNSWIndex::build
Share the kilobytes-to-vectors full scan threshold conversion between
load_or_derive_config and build as derive_config, with the vector count
as an explicit argument so both call sites keep their existing divisor.
Move the per-field progress subtasks into additional_links_fields next
to the phase that consumes them, and the rayon pool with its low-priority
spawn handler into build_thread_pool.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Extract GPU attempt, graph saving and the deleted-points check from build
upload_and_build_main_graph in gpu_build.rs owns the "any graph will be
built" gate, the upload, the main graph attempt and its timing log, so
build.rs keeps one cfg pair and adopts the GPU graph with an if-let.
save_graph builds the links format param and calls into_graph_layers in
one place, since the param borrows the inline vectors.
The debug-only walk over deleted points becomes a predicate over
links_empty under debug_assert!.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Cancellable open and live_reload for read-only edge shards
Add cooperative cancellation to ReadOnlyEdgeShard::open and
ReadOnlyEdgeShard::live_reload, following the contract of
EdgeShardReadWithCancellation: a shared stop flag is checked between
stages and between segments, a set flag yields OperationError::Cancelled
and never a partial result, and the flag is never set or reset by the
callee.
New entry points: ReadOnlyEdgeShard::open_with_cancellation and
ReadOnlyEdgeShard::live_reload_with_cancellation. The existing open,
live_reload and live_reload_with keep their signatures and delegate with
a fresh flag.
The flag is propagated into ReadOnlySegment::schedule_open. The staged
handle carries it, so both the prefetch staging and finish check it
between components. The edge loader propagates a Cancelled error instead
of logging it as an unloadable segment.
The holder swap and the config re-derivation that follows it are one
indivisible step, so a cancellation never leaves a config lagging behind
the segment set. Segment reloads stay atomic under their write lock, so a
cancelled live_reload leaves the shard consistent and the next one
continues from there.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Check cancellation between vector component preopens
A preopen polls its open future once, so real work starts right away.
Checking the flag between the storage, quantized vectors and index
preopens of a dense vector, and between the storage and index preopens
of a sparse vector, keeps the staging contract that the flag is observed
between components.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The test compared raft commit indices across peers before issuing shard
cleanup. A follower advances its commit index before it applies the entry,
so cleanup could still race the follower applying CommitRead, which
invalidates running clean tasks and makes the endpoint return 500.
Wait for every peer to report the applied `read_hash_ring_committed`
resharding stage via telemetry instead. Reading it takes the same shard
holder lock as the consensus handler, so an observed stage is fully applied.
Repurpose the unused stage check helper to read the stage from telemetry,
since the `comment` field it read no longer exists.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* test: pin persisted proxy segments follow-up findings
* fix: tests
* Respect `up_to` in flushing pending proxy changes
* Fix unproxy divergence, centralize logic in single shared function
* Don't pass locked segments we don't use
* Close the post-swap window losing acknowledged proxy changes
`finish_optimization` left a window between `swap_new` and the end of the
function where the propagated proxy changes were durable in no reachable
place. The proxies had left the holder, so a flush pass no longer saw them
as unsaved work and acknowledged the WAL past what their pending changes
logs persisted, while their source files were still on disk contradicting
the newer state. Their `ack_pin` was only registered at the very end, and
the optimized segment, the only durable home of those changes, had no
version file yet, so `normalize_segment_dir` deleted it on the next load.
A failure or crash in that window lost every change past the proxies' logs.
Close it from both sides, reordering only:
- Save the optimized segment's version file right after the flush that made
the propagated changes durable, before the swap. That flush is what
`SegmentBuilder::build` postponed the version file for, so the segment is
loadable from the swap onwards.
- Register the proxies' deferred destruction (and with it their `ack_pin`)
under the same write lock that evicted them, so no flush pass can observe
the holder without either the proxies or their pin.
Collecting the deferred point ids moves up with the registration, as it
borrows the swapped-out proxies. The deferred destruction still cannot run
before the manifest is synced: `locked_proxies` holds the segments alive
until the end of the function, so `try_drop_data` retries until then.
* Rename the optimization test hook after the window it guards
The hook no longer sits before the version save, it marks a failure
anywhere in the window after the optimized segment was swapped in. Rename
it and the test accordingly, and restate the test doc as the invariant that
window must uphold rather than the bug it used to describe.
* Pin WAL ack while creating snapshot
* Patch test that was stuck
* Initialize necessary feature flags in tests
* Correctly propagate changes in two stages, lock updates on second stage
* Use existing WAL ack pinning infrastructure
* test: pin unproxy phase 2 propagation failure losing acknowledged changes
* test: pin WAL ack pin at zero suppressing clock persistence
* Fix propagate and unproxy data consistency error on failure
* Store clocks before checking WAL ack pin
* fix: wait for the flush worker when stopping it in tests
* fix: linter
---------
Co-authored-by: timvisee <tim@visee.me>