mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-07 02:20:55 -05:00
segment-overflow-cap
4382 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8f82806318 |
Provision fresh appendable segments when all reach max_segment_size
Fixes appendable segment overflow (#9158): CoW-moving operations (e.g. set_payload by filter over indexed segments) and plain inserts used to pick a destination appendable segment without any size check, so a single large operation could pile every moved point into one segment far past the configured max_segment_size. The update path now respects the resolved optimizer threshold, mirrored into the segment holder as a soft size cap: - CoW-move destinations are re-checked per moved point; candidates at the cap are skipped, and the insert loops refuse destinations that reached it. - When no appendable segment has capacity left, the operation fails with the new recoverable `OperationError::OutOfAppendableCapacity`. - `LockedSegmentHolder::update_with_segment_provisioning` catches that error, provisions a fresh appendable segment (same machinery the optimization worker already used between operations), and re-applies the operation; per-point version checks make the re-run resume where it left off, exactly like WAL replay. The moving `process_*` entry points now take the segment holder lock plus an optional `SegmentProvisioning`, wired from the update worker, WAL replay, and failed-operation recovery. The edge shard passes no provisioning and no cap, keeping its behavior unchanged. The regression test from #9158 is included (adapted to configure the cap) and now passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6b3f8e876e |
Use AHashMap for JsonPath-keyed field index lookups (#9854)
Profiling showed SipHash over JsonPath keys (RandomState::hash_one) dominating field index lookups in StructPayloadIndexReadView::estimate_field_condition: the map holds only a handful of indexed fields, but it is queried per condition per filter evaluation per query, and hashing the path is the entire lookup cost. Switch the field index lookup maps (IndexesMap, ReadOnlyIndexesMap, the read view borrow, and the query_checker / condition_converter / value_retriever signatures that receive them) from std HashMap to AHashMap, already used throughout the crate. Serialized and API-level schema maps keep std HashMap: they are cold and their type leaks into conversions. conditional_search bench (criterion, 100 samples, vs saved baseline): - struct-conditional-search-query-points: 583.7 us -> 443.5 us (-23.1% median, p ~ 0) - struct-conditional-search-context-check: 73.2 us -> 64.8 us (-12.6% median, p ~ 0) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2a92bcf3a3 |
[UIO] Increase PHF prefetch (#9842)
* increase phf prefetch * no duplicate comment Co-authored-by: xzfc <5121426+xzfc@users.noreply.github.com> --------- Co-authored-by: xzfc <5121426+xzfc@users.noreply.github.com> |
||
|
|
e147d2f8e4 |
[edge] Add readonly ram sparse index (#9841)
* Add Ram variant, loads from vector storage * impl live reload * preopen sparse storage when using ram index * Batch storage reads when building sparse RAM index Rebuild `build_ram_index` around a single `read_vectors::<Sequential>` pass instead of a per-point `get_vector_opt` loop, so Gridstore coalesces the IO into block reads instead of a round-trip per point. Benefits both the read-only mutable-RAM rebuild and the writable `SparseVectorIndex::plan` build path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: generall <andrey@vasnetsov.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f8a62ed423 | Fix flaky test_join_all_completes_sibling_restart_after_workers_stop (#9848) | ||
|
|
9a20fc49e4 |
[TQDT] TQ roundtrip: raw vectors in grpc, WAL and apply internal operation (#9813)
* raw vector grpc apply are you happy fmt clean up are you happy clippy review remarks review remarks * fix after rebase |
||
|
|
94cc5eeec9 |
Ignore slow Windows CI tests for turbo multi model and quantization (#9850)
Skip the three slowest Windows rust-tests (>60s each): turbo multi random-ops model tests and the turbo Manhattan HNSW quantization case. Use rstest test_attr for the parametrized case since a top-level ignore does not apply to all generated tests. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
71e6c70458 |
Universal IO: append to file (#9720)
* universal_io: add UniversalAppend for atomic single-operation appends Growing a file previously took a separate set_len + reopen + write dance (bypassing universal_io and leaving a zero-filled window on crash), and there was no way to express appends for backends without random-offset writes. UniversalAppend::append grows the file by writing at the current end of file in one atomic grow+write operation and returns the offset at which the data landed; append_batch lands multiple buffers contiguously in as few operations as the backend allows. flusher() moves from UniversalWrite into a new UniversalFlush supertrait so append-only handles can require it without duplicating the method. Local backends, both single-syscall: - MmapFile appends through a dedicated O_APPEND fd (every write(2) / writev(2) is an atomic grow+write at EOF), then remaps via reopen(). Its flusher also fdatasyncs after appends, since msync alone does not persist file-size metadata. - IoUringFile appends via pwritev2(RWF_APPEND). O_APPEND is not an option there: on Linux, pwrite on an O_APPEND fd appends regardless of the given offset, which would break positioned writes on clones sharing the fd. Concurrent appenders are out of contract (single logical writer); object-store backends surface the new AppendOffsetConflict error and recover via reopen() + retry. * io_bridge: add AsyncWrite/AsyncAppend and appendable BlobFile Add the write-side backend traits reserved next to AsyncRead: AsyncWrite (create/remove/save) powers a UniversalWriteFileOps impl on BlobFs (create-or-truncate put, delete, atomic whole-object save; directory ops are no-ops), and AsyncAppend — a single-request append where the offset must equal the current object size, acting as a compare-and-swap token — powers UniversalAppend on BlobFile. BlobFile caches the object size across appends (one HEAD for N appends; a missing object counts as empty so the first append creates it), concatenates batches into a single request, and drops the cache on reopen() — the documented recovery path after AppendOffsetConflict. Its flusher is a no-op: appends are durable once the backend acknowledges them. * io_bridge_object_store: native single-request S3 append object_store has no append support, so issue the PutObject + x-amz-write-offset-bytes request ourselves, reusing the store's credential chain (AmazonS3::credentials) and object_store's SigV4 AwsAuthorizer, which signs every header present on the request — no hand-rolled signing and no direct reqwest dependency. The offset doubles as a compare-and-swap token: a mismatch (400 InvalidWriteOffset, or 412 on some S3-compatibles) maps to AppendOffsetConflict. The write-offset append API exists on AWS S3 Express One Zone directory buckets and compatible stores (e.g. MinIO AiStor) — plain S3 Standard buckets reject it, and real Express zonal endpoints / session auth are not verified yet; MinIO-AiStor-compatible stores are the primary target for now. GCS and Azure sources simply do not implement AsyncAppend. ObjectStoreSource carries an AppendContext (HTTP client + object URL base + signing region) built per backend from its config, and gains a generic AsyncWrite impl (single-put create/save, delete). A test-only multi-request CAS emulation over InMemory exercises the BlobFile append stack hermetically; an end-to-end flow against a real append-capable store is gated behind S3_APPEND_INTEGRATION_TEST=1. * simple_disk_cache: write-through UniversalAppend for DiskCache Append to the remote (the single grow+write operation), then write the same bytes through into the local mirror so tail reads do not re-fetch what was just uploaded. LocalState::append_local keeps the fetched bitmap accurate: blocks fully covered by the appended range are marked fetched, and the pre-append partial tail block — which resize() drops because set_len zero-fills its gap — is re-marked only when its prefix was already fetched. If the mirror turns out stale (the remote grew behind our back), append falls back to resize-only and lazy fetches heal the gap on the next read. Writeable opens are now allowed on DiskCacheFs solely to enable append; DiskCache still never implements UniversalWrite. The writeable flag propagates to the remote handle, which is opened buffered instead of O_DIRECT: appends write through the page cache, which O_DIRECT reads on the same fd would fight (and IoUringFile rejects appends on prevent_caching handles). The remote-immutability docs are relaxed to append-only with an immutable prefix, matching what reopen() already assumed. Includes a full-stack composition test: DiskCache write-through over BlobFile offset tracking over an in-memory object store. * io_bridge_object_store: build the append HTTP client lazily Opening a source from an AwsConfig eagerly built the reqwest client (TLS setup, connection pool) even when append was never used. Keep the AppendContext construction to pure config (allow_http flag, object URL base, signing region) and build the client on first append instead, cached in an Arc<OnceLock> shared across clones of the source — and thus across the file handles opened from it. Sources that never append now pay nothing; client-construction errors surface on the first append instead of at open. * universal_io: test that append grows the regular file on disk The conformance suite reads appended bytes back through universal-io handles; also assert the underlying regular file itself — created outside universal_io, verified with plain fs reads — for both local backends. * Mention why we use custom HTTP client, object_store crate has no support * io_bridge_object_store: reject appends unconfirmed by the size header A store without write-offset support may accept the signed PutObject as a plain put — replacing the object with just the appended bytes — and return 2xx (community MinIO did exactly this before 2025-05, commit minio/minio@6d18dba9). The old success path fabricated the new length when x-amz-object-size was missing, so the destruction stayed invisible while every subsequent append repeated it. Require the x-amz-object-size response header (returned by AWS and MinIO AiStor appends) for any append at offset > 0 and fail loudly without it. Offset-0 appends are equivalent to a whole-object write, so they remain valid either way — a misconfigured store now fails on the second append instead of never. * io_bridge_object_store: honor endpoint/region env vars for appends With AwsCredentials::Default the store is built via AmazonS3Builder::from_env, which honors AWS_ENDPOINT_URL_S3, AWS_ENDPOINT_URL, AWS_ENDPOINT, AWS_REGION and AWS_DEFAULT_REGION — but append_context derived the append URL and SigV4 region only from the typed config fields. An env-configured deployment would read from one host while signing and sending appends to https://{bucket}.s3.us-east-1.amazonaws.com. Resolve the append endpoint and region the same way build_store does: explicit config first, then (default credential chain only) the same environment variables, with AWS_ENDPOINT_URL_S3 taking precedence as in from_env. The resolution is a pure function over an injected env lookup so the test does not touch process-global environment state. * simple_disk_cache: delegate the append flusher to the remote DiskCache's UniversalFlush impl was an unconditional no-op, justified by object-store appends being durable on acknowledgement — but the impl is generic over any appendable remote, and for local remotes (MmapFile, IoUringFile, exactly the compositions the tests instantiate) that silently dropped the fdatasync the UniversalAppend contract requires: append, flush Ok, power loss, appended bytes gone. Delegate to the remote's flusher once the cache is materialized: local remotes get their sync, object-store flushers remain no-ops, and a never-materialized cache has made no appends so a no-op stays correct. * io_bridge_object_store: retry transient append failures The append RPC was a single unretried HTTP attempt, while every other request in this stack goes through object_store's retry layer — a routine transient 503 SlowDown or connection reset failed the append hard where a concurrent read would have silently recovered. Retry connection errors, 5xx and 429 up to three attempts with a short linear backoff, re-signing per attempt (the SigV4 signature embeds the request date). Retrying is safe because the write offset is a compare-and-swap; the one ambiguity — an attempt that landed but whose acknowledgement was lost — surfaces as a write-offset conflict on the retry, which is reconciled with a HEAD: under the single-writer contract, an object size of exactly offset + data_len proves the tail is ours, so the append reports success instead of a spurious conflict (whose reopen-and-retry recovery would duplicate the record). * universal_io: forward TypedStorage::flusher for any UniversalFlush The flusher forwarding lived in TypedStorage's S: UniversalWrite impl block, so append-only storages (DiskCache, BlobFile — UniversalAppend + UniversalFlush but not UniversalWrite) offered append through the wrapper while the durability flusher the append contract mandates was unreachable without going through .inner. Move it to an S: UniversalFlush block: UniversalWrite implies UniversalFlush, so existing callers resolve unchanged, and duplicating the method instead would have hit E0592 on backends implementing both — the very ambiguity UniversalFlush was extracted to avoid. * io_bridge_object_store: surface unbuildable append requests as errors Request building could panic on two reachable paths: url accepts URIs the http crate rejects (IPv6 zone identifiers, URIs beyond u16::MAX bytes), and AppendContext::new is public so the object URL base is not guaranteed to be a base URL. Both expects become S3Config errors, so a configuration edge case fails the append instead of panicking the thread driving the bridge runtime. * simple_disk_cache: don't fail appends the remote already committed append_impl committed to the remote first and returned Err when the subsequent local-mirror update failed (e.g. ENOSPC on the cache volume) — indistinguishable from "nothing was appended", so a retrying caller would duplicate the record on the remote. The mirror is cache maintenance, not part of the append: on a failed write-through, log and degrade to bare growth so lazy fetches heal the unmarked blocks (safe — blocks are only marked fetched after their bytes landed). Only an unresizable mirror still surfaces an error, and the UniversalAppend contract now documents that an append Err does not guarantee nothing was appended: reopen() and re-check the length before retrying. * universal_io: bounds-check positioned io_uring writes against EOF The UniversalAppend contract states that UniversalWrite::write beyond the end-of-file fails and append is the only growth path — mmap enforces it, but IoUringFile's write/write_batch/write_multi were unchecked pwrites that silently extended the file with a zero-filled hole, inflating subsequent append offsets. Check every positioned write against the file length (fstat once per call), matching mmap's OutOfBounds semantics, and generalize the regression test to run on both local backends including the batched path. * io_bridge: reject appends on handles opened without writeable BlobFs::open dropped OpenOptions entirely, so a BlobFile opened with writeable: false still accepted appends — mmap and DiskCache enforce the writeable requirement, the blob backend silently didn't, and a stray append through a nominally read-only handle would mutate a shared object. Thread OpenOptions::writeable into BlobFile and reject appends with PermissionDenied when it is unset, mirroring the other backends. Directly-constructed handles (BlobFile::new/open, which take no OpenOptions) remain writeable. With every backend now enforcing the flag, the UniversalAppend contract drops its 'where the backend enforces open modes' hedge. * simple_disk_cache: answer empty appends from the mirror An empty append still went through remote.append_batch, so it returned the remote's live end-of-file — which can diverge from what this handle's len() and reads observe when the remote grew behind our back — while leaving the stale mirror unhealed (unlike a non-empty append in the same state, which resizes). Accept empty appends early: return the mirror length without touching the remote at all, keeping the answer consistent with the handle's own view. The trait contract now spells out that empty appends return the handle's view of the end of file without growth I/O. * universal_io: grow the mmap in place after appends Every mmap append ended in a full reopen(): an open+fstat+close by path just to learn the new length, and — on the non-Linux fallback, which rebuilds the mapping with the open-time populate flag — a re-population of the ENTIRE file per append, making appends O(file size) for handles opened with Populate::Blocking. Populating after an append is pointless anyway: we just touched the data we wrote. Extract the remap machinery into remap_to() (reopen() keeps its exact semantics, populate included) and add grow_mapping(): a stat-free grow that never re-populates. Appends learn the new length from a single fstat on the already-open O_APPEND fd — kept rather than trusting the mapping length, which is stale exactly in the externally-grown-remote scenario the disk cache heals through lazy fetches (the foreign-growth tests catch the difference). The mirror's resize() passes the length it just set_len'd, dropping its stat round-trip entirely. Per small append this is write+fstat+mremap, down from write+open+fstat+close+mremap, with no populate anywhere. * universal_io: share the mmap append fd across clones The flusher captured the per-clone append_file at flusher-creation time, so a flusher obtained from a sibling clone — or created before the handle's first append — msynced the shared mapping's data pages but skipped the fdatasync that persists the appended file size: a half-persist where a crash loses the acknowledged tail even though a flusher ran after the appends (writer thread + long-lived flush-worker clone is exactly the natural WAL shape). Store the fd in an Arc<OnceLock> shared by all clones and read it at flush time instead of capture time: any clone's append makes every handle's flusher sync the size metadata, whatever the clone/flusher creation order. Initialization races between clones keep exactly one fd. No hot-path cost: reads and positioned writes never touch the cell, and the append path pays one atomic load next to its syscalls. The interior mutability also lets append_fd take &self. * universal_io: document the clone remap hazard truthfully The remap SAFETY comment claimed moving is safe "since we are holding &mut self" — which says nothing about clones: they share the mapping but keep their own raw ptr/len copies, so after a moving (or, on non-Linux, replacing) remap a sibling clone's next read dereferences an unmapped address. The trait contract understated the same hazard as a concurrent-read constraint, while the UB persists after append returns. State the contract once on MmapFile (clones must reopen before reading after any growth; a stale clone read is undefined behavior, not a stale view), correct the SAFETY argument to rely on it explicitly, annotate the as_bytes unsafe blocks that depend on it, and sharpen the UniversalAppend contract bullet accordingly. Making clones structurally safe (resolving ptr/len through the shared Arc) is deliberately left as a separate change. * universal_io: share the vectored append machinery between backends The IOV_MAX-chunking / EINTR-retry / WriteZero / advance_slices loop existed twice — as local_file_ops::write_all_vectored (mmap) and inlined around pwritev2 in IoUringFile::append_slices — along with a verbatim collect/cast/filter-empties preamble in both append_batch impls. Two copies of subtle short-write handling introduced by one branch will diverge the first time only one of them gets a fix. Add an io::Write adapter whose write_vectored issues pwritev2(RWF_APPEND), letting the io_uring append delegate to the shared write_all_vectored, and hoist the slice collection into local_file_ops::collect_append_slices. IOV_MAX becomes private to the one function enforcing it. No behavior change; the existing conformance tests (including the beyond-IOV_MAX batch) cover both backends through the shared path. * io_bridge_object_store: add s3_express to the test config helper The AwsConfig struct gained the s3_express field; update the resolve-endpoint test helper accordingly. * universal_io: run the append conformance suite over the S3 stack Promote the backend-generic UniversalAppend battery (offsets, batches across IOV_MAX, empty appends, read-after-append, reopen visibility, flusher) from a private test into universal_io::conformance, exposed under the testing feature so backend crates can run the identical suite. mmap and io_uring keep running it as before; the object-store bridge now runs it too, over BlobFs/BlobFile with the in-memory offset-CAS append emulation — so local file system and S3 append behavior are asserted by the same test. The real write-offset RPC remains covered by the gated test_native_append_flow integration test. * Swap order * universal_io: disambiguate the io_uring crate import The import reorder dropped the leading `::`, making `io_uring` ambiguous with this very module (pulled into scope by the `use super::*` glob) and breaking the build. * io_bridge: don't materialize the mock object on rejected appends MutableMockSource::append called get_or_insert_with before validating the offset, so a rejected stale append against a missing object left an empty entry behind (exists() flipping true) — a fidelity gap versus the real backends, where a rejected append has no side effects. Check the offset against the current length first and only materialize the buffer on a match. * universal_io: disambiguate the io_uring crate import Restore the leading `::` on the io_uring crate import — without it the name is ambiguous with this very module, which the `use super::*` glob pulls into scope, and the crate fails to compile. Matches the sibling files (pool.rs, runtime.rs), which already import via `::io_uring`. * io_bridge_object_store: treat 404 under a nonzero append offset as a conflict A missing object while the handle expected a nonzero end-of-file is a stale view (the object was deleted behind our back) — the same situation as an offset mismatch, with the same reopen-and-retry recovery, and it is exactly what the in-memory emulation and the io_bridge mock already report. The RPC path mapped every 404 to NotFound instead, so the three implementations disagreed on the same logical case. Keep NotFound for offset-0 appends, where a 404 is a genuine missing-target error (e.g. a missing bucket) that retrying cannot heal. * Preallocate vector * universal_io: disallow appends through the disk cache Appends must go directly to the backing storage (mmap, io_uring, S3) — the disk cache is strictly read-only again. Remove DiskCache's UniversalAppend/UniversalFlush impls and the mirror write-through machinery (LocalState::append_local), reject writeable opens at DiskCacheFs::open, and drop the writeable/prevent_caching plumbing that existed solely for cached appends, restoring read-only remote handles. Attempting to append through the cache is now a compile-time error (the trait impl no longer exists), and opening a cached handle writeable is rejected at runtime, covered by a test in each backend variant. * universal_io: make append idempotent via caller-supplied offset Append now takes the byte offset where the data must land: append(offset, data) -> Result<()>. Every backend validates that the offset equals the current end of file before writing (mmap and io_uring fstat the fd, object stores validate server-side via x-amz-write-offset-bytes); on mismatch nothing is written and the append fails with AppendOffsetConflict. Retrying an already-landed append therefore conflicts instead of appending twice, and recovery is re-deriving the offset from len(). BlobFile no longer tracks the object length locally; the store's own offset check is the compare-and-swap. * Review remarks * Validate file length in S3 append response * Fix linting, we don't mind a large enum variant on index builder * universal_io: conformance-test stale-handle append conflict recovery Promote the two-handle conflict scenario from the in-memory BlobFile test into the backend-generic conformance battery: a second writeable handle grows the file, the stale handle's append conflicts cleanly (the offset check runs against the file, not the handle's view), and the contract's documented recovery — reopen, re-check the length, append at the real end — lands the data exactly once. Now exercised over mmap, io_uring, and the object-store stack instead of only the in-memory emulation. * io_bridge_object_store: stub-server tests for append response handling The native append's HTTP state machine was only exercised by the gated live-store integration test (S3_APPEND_INTEGRATION_TEST=1), so none of its branches ran in CI. Cover them hermetically against a minimal local HTTP stub — one connection per canned response, no new dependencies: - the signed write-offset PUT, and the new-size validation on success (matching, mismatching, unparseable, and absent size headers — the absent case at offset zero and past it); - conflict mapping for 400 InvalidWriteOffset, 412, and 404 under a nonzero offset, with 404 at offset zero staying NotFound, and a 400 without the conflict code staying a plain error; - 429/5xx retries re-sending the same offset, giving up after MAX_ATTEMPTS, and the lost-acknowledgement reconciliation via HEAD (accepted when the object ends at offset + len, rejected otherwise); - the status + body excerpt on unexpected failures. * simple_disk_cache: statically assert the cache stays read-only Disallowing appends through the disk cache made them a compile-time error by removing the impls; pin that with assert_not_impl_any so the UniversalAppend/UniversalFlush/UniversalWrite impls cannot quietly return. Runtime rejection of writeable opens stays covered per backend variant. |
||
|
|
842ddfae10 |
fix: validate lookup_from collection for query and recommend APIs (#9531)
* fix: validate lookup_from collection in query APIs * Move validation to the bottom of the struct implementation * fix: preserve lookup_from missing collection error --------- Co-authored-by: timvisee <tim@visee.me> |
||
|
|
0d2f5c7e85 | Miscellaneous cleanups (#9849) | ||
|
|
355ac9fc2e |
Add Float16 and Uint8 storage datatypes to the model tester (#9815)
* Add Float16 and Uint8 storage datatypes to the model tester
Extend VectorCandidate with a datatype override and fold the DenseTurbo
kind into Dense + Some(Turbo4) so storage datatype has a single source
of truth. Two new initially-active candidates exercise half-precision
("h", dense 6) and unsigned-byte ("y", dense 4) storage; "c" carries an
explicit Some(Float32) to cover schema configs that spell the default
datatype out.
The model predicts lossy read-backs through the engine's own
PrimitiveVectorElement impls (as Turbo4 reuses turbo_storage_roundtrip)
and compares them exactly: both round-trips are deterministic and
idempotent, so they stay bit-stable across optimizer moves, WAL replay,
and reloads. Uint8 components are drawn from 0.0..256.0 since the
storage truncates with `x as u8` and unit-range draws would collapse to
zeros.
A compile-time assertion rejects datatype overrides on non-Dense
candidates: the fixture's sparse/multi-dense arms ignore the field and
multi-dense read-backs are compared without a round-trip prediction, so
a lossy multi-dense candidate would soak-panic with a false divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Plumb Float16 and Uint8 multi-dense support in the model tester
Multi-dense storage converts the flattened matrix component-wise
(from_float_multivector), so per-row round-trips through the same
PrimitiveVectorElement impls predict read-backs exactly. The fixture's
multi-dense arm now applies the candidate datatype (matching the
CreateVectorName path), model_vector predicts per-row, and two new
initially-active candidates exercise the combination: "w"
(MultiDense(5), Float16) and "z" (MultiDense(3), Uint8).
The compile-time candidate check narrows to the combinations that
remain unpredicted: Turbo4 multi-dense (the multivector quantization
path differs from the per-vector turbo_storage_roundtrip) and sparse
with any datatype override.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Make datatype match exhaustive in random_dense_vec
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review findings on datatype plumbing
- Start candidate "c" active so the explicit Float32 schema path runs in
default soaks (CreateVectorName is FORCE_OFF by default)
- Single Float16/Uint8 roundtrip dispatch shared by the dense and
multi-dense arms of model_vector
- Hoist shared fixture builder plumbing into dense_params_builder
- Build one DenseVectorConfig literal in the CreateVectorName generator
- Inline single-caller datatype_of wrapper
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Replace const-eval candidate check with a startup assert
Const eval forbids iterators, forcing an index-based while loop. A plain
function called at the top of run() reads better, still fails before any
op is applied, and names the offending candidate in the panic message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fold INITIAL_ACTIVE into an initially_active candidate field
The hand-maintained list duplicated ALL_CANDIDATES (11 of 12 names) and
had to be kept in sync when adding candidates; forgetting it was silent
since CreateVectorName is FORCE_OFF by default, so a forgotten name got
zero default-soak coverage. Each candidate now declares its activation
inline and the fixture and run() filter on it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
72e5d9b42b |
Improve error message for non-string key in filter conditions (#9847)
* Improve error message for non-string 'key' in filter conditions * Reformat --------- Co-authored-by: timvisee <tim@visee.me> |
||
|
|
40332d271d |
[TQDT] Roundtrip fix for clone_and_mutate_point (#9761)
* Fix TQDT roundtrip issue for `clone_and_mutate_point` * Use TinyMap * Fix doc string * Rebase docstring fixes * [TQDT] Reduce allocations (#9833) * Reduce allocations * Fix edge |
||
|
|
da63a3cdab | remove expect (#9839) | ||
|
|
cb256a801d |
[DiskCache] Dedup overlapping remote requests (#9838)
* [AI] piggyback on in-flight requests * cleanup tests * Insert in-flight fetch through Slab's VacantEntry (#9840) Replace the peek-key-then-insert pattern with slab's vacant_entry() API: inserting through the reserved entry guarantees the fetch lands on the key the remote read was tagged with, instead of relying on nothing touching the slab between the peek and the insert. A failed remote schedule still leaves no trace, as VacantEntry allocates nothing until insert. get_or_init_remote_pipeline now takes the field instead of &mut self so the VacantEntry can hold a disjoint borrow of in_flight across the schedule call. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6ac3c828d4 | exhaustive match on async-like backends (#9837) | ||
|
|
631d9a2045 |
align scalar f16 metrics precision with SIMD thresholds (#9788) (#9811)
* - [AI] fix: align scalar f16 metrics precision with SIMD thresholds (#9788) * remove generated tests |
||
|
|
8888046cd3 |
Add usage skill file for edge-shard-query tool (#9836)
Standalone usage reference for `edge-shard-query`: backends (S3 / GCS / uio-grpc), connection and tuning flags, the scroll / search / search-sparse sub-commands, filtering, and the live-reload diff mode. Written to stand on its own, so it can be shared as a link without also sharing the source. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d4e0f354da |
Single inverse rotation in TurboQuant symmetric L1 scoring (#9835)
The inverse Hadamard rotation is linear, so it distributes over subtraction: |R'd1 - R'd2| = |R'(d1 - d2)|. Subtract the dequantized vectors in rotated space and inverse-rotate the difference once, instead of inverse-rotating both vectors per score call. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a5a0ecc5e9 |
Add search-sparse sub-command to edge-shard-query tool (#9834)
Sparse nearest-neighbour search over a ReadOnlyEdgeShard, reusing the same SearchRequest path as dense search with VectorInternal::Sparse. The query vector is accepted as a JSON object or an index:value pair list, sorted and validated before use. --vector is required (no random fallback: the sparse vocabulary is not recoverable from the shard config), and --hnsw-ef is omitted since it does not apply to the sparse index. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c3ff8ee44b |
Add optional block-index sidecar to accelerate on-disk binary searches (#9810)
* Add optional block-index sidecar to accelerate on-disk binary searches Binary search directly over an unpopulated mmap costs O(log n) random reads scattered across the whole file — one page fault (or remote range read) per probe on high-latency storage. Introduce SortedBlockIndex: an optional sidecar file storing the first element of every 16KiB block of a sorted on-disk array, read whole into RAM at open. A lookup becomes an in-RAM partition_point over the block firsts plus a single contiguous read of one block, bisected in RAM. Wire it into the three remaining scatter-probe sites: - on-disk numeric index: binary_search_pairs over data.bin - on-disk geo index: counts_of_hash over counts_per_hash.bin - on-disk geo index: all_points start boundary over points_map.bin The sidecar is backward and forward compatible: absent (old segments) or failing validation, readers fall back to the existing plain binary search; old code simply ignores the extra file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Validate block-index sidecar snapshot recovery and preopen coverage - snapshot round-trip test (Regular + Streamable): the sidecars written by the on-disk numeric and geo indexes are collected via files() into the snapshot tar and restored byte-for-byte, with correct query results on the reloaded segment - preopen tests for both indexes: after schedule_prefetch, open loads the sidecar from the CachedFs prefetch pool even when the file is unlinked from disk; an absent sidecar neither fails preopen nor open (fallback) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
55f4219a3c |
Fix flaky WAL replay tests by waiting for update worker idle (#9832)
An empty update channel only means the last operation was received, not that it finished applying in spawn_blocking. Use plunge_async as a barrier before asserting point counts. Fixes #9831 Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8cbd218d7c |
Fix flaky test_wait_deferred_does_not_block_update_worker (#9816)
Drain the update worker queue after setup upserts with WaitUntil::Wal. Wal only waits for the WAL write, so on slow CI (notably Windows) B could time out while queued behind the setup backlog rather than because the worker was blocked on A's deferred wait. Fixes #9814 Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
7c2a045574 | fix(segment): count trailing deleted offset on mmap sparse reload (#9798) | ||
|
|
27672b8011 |
[AI] Speed up TurboQuant Hadamard rotation by ~3x (#9660)
* Speed up TurboQuant Hadamard rotation by ~3x Profiling apply_inverse showed 44% of cycles in the Fisher-Yates swap replay (serial LCG plus a 64-bit modulo per element), not in the WHT. - Materialize the shared permutations as index maps once in HadamardRotation::new; each apply-time permutation becomes a flat gather pass ping-ponging between the vector and a thread-local scratch buffer. The replay path stays as the cfg(test) parity oracle. - Fuse consecutive WHT outer butterfly stage pairs (h, 2h) into one pass over the array (AVX2), halving memory traffic for h >= 16. - Fuse the normalization multiply into the transform's final-stage stores (wht_dispatch_scaled), removing the separate normalize pass. Output is bit-identical on all paths: pinned by the existing struct-vs-replay and SIMD-vs-scalar bit-equal tests plus a new wht_dispatch_scaled parity test. NEON is unchanged. Criterion hadamard bench (Zen 5): apply 2.8-3.7x faster across dims 128-4096 (1024: 6.98us -> 2.03us), apply_inverse 2.8-3.5x (1024: 6.10us -> 2.05us). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review: harden gather contract, small-dim parity, grow-only scratch - Mark gather_permuted as unsafe fn: the get_unchecked justification relied on a caller invariant (map indices in range) that the signature did not surface; document it as a # Safety contract instead. - Add dims 5 and 50 to static_rotation_matches_struct_and_roundtrips to pin the map path against the replay oracle on degenerate chunk splits. - Make the thread-local gather scratch grow-only, so threads alternating between dims no longer shrink and re-zero the buffer on every call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Hard-assert input length in apply/apply_inverse Fail fast at the public boundary: with debug_assert only, a release build would WHT-normalize a wrong-length slice before the gather's hard length asserts panic. Flagged by CodeRabbit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1d4d6f02da |
Per-query IDF corpus for sparse vector search (#9661)
* Add per-query IDF corpus for sparse vector search
Let the caller choose, per query, which population sparse IDF statistics
are computed over. `params.idf` is either `"global"` (default, unchanged
behavior) or `{"corpus": <filter>}`, where the corpus filter is
independent of - and usually broader than - the retrieval filter.
Decoupling the two keeps the score scale stable when the retrieval
filter tightens: term importance is measured against a population the
user names, not against whatever subset the filter happens to select.
Design decisions:
- Corpus grammar is restricted to a conjunction (`must`) of `match`
conditions on payload fields; loosening later is backward compatible.
- Strict mode validates the corpus filter like a read filter
(unindexed fields rejected).
- `idf` on a vector without the IDF modifier is a validation error,
never silently ignored.
- An empty corpus yields degenerate but corpus-scoped scores (smoothed
IDF over N=0), never a fallback to global statistics - in multi-tenant
collections a fallback would leak term statistics across tenants.
Implementation:
- QueryContext IDF stats are keyed by corpus, so one batch can mix
requests with different corpora.
- Statistics come from the sparse index: df(term) is counted over the
query terms' posting lists only, never by scanning stored vectors.
Small corpora (under ~1/32 of the segment, by cardinality estimate)
are kept as a sorted id list galloping through posting lists via
skip_to; large ones as a dense membership mask filled streaming from
the filtered-points iterator. A misestimated small corpus degrades
into the mask.
- Exposed uniformly: REST (`params.idf`), gRPC (`IdfParams` message),
edge python bindings; OpenAPI schema regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Apply rustfmt
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix clippy manual_is_multiple_of in sparse IDF corpus test.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Allow any filter as IDF corpus
Drop the must+match grammar restriction on the corpus filter. A
restriction enforced only as a validation step over the full Filter
type buys nothing; if a narrower corpus syntax is ever wanted, it
should be a dedicated API-level type instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix build: add memory field to SparseIndexConfig in idf corpus test
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: root <111755117+qdrant-cloud-bot@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
a647c807d4 | Cleanup | ||
|
|
b31d98d55a |
[audit-M] Propagate the force-abort error in drop_shard_key
drop_shard_key force-aborted any resharding on the key being dropped, but swallowed a failed abort_resharding with a log-only error and dropped the shards anyway. That could leave resharding_state.json referencing the just-dropped key — a latent inconsistent load-time state. Propagate the error with `?`. A ServiceError halts consensus and retries after restart, which is safe because the abort and everything else in drop_shard_key is replay-tolerant; a user error dismisses the entry before any shard is dropped, which is equally consistent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d7c7052eb5 |
[audit-F] Use bad_request instead of service_error in pre-write transfer validations
A ServiceError returned from apply halts consensus on every peer (the entry can never be applied), deterministically stalling the whole cluster. The transfer Start validations that report a missing source or destination shard are reachable — e.g. a committed Start racing a resharding-abort or shard-key-drop that removed the shard — and all run before any durable write, so dismissing the entry with a user error is safe and correct. Convert five such sites to CollectionError::bad_request: - validate_transfer: source shard missing, and destination shard missing in both the resharding and filtered branches (helpers.rs); - start_shard_transfer: the source and target get_shard lookups (shard_transfer.rs). The genuine "single node deployment" service_errors in collection_meta_ops.rs are left untouched — those are real misconfiguration guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7b2799a394 |
[audit-D] Make transfer Start validation replay-tolerant
Start's first durable write registers the transfer record; its last write sets the destination replica state. On re-apply after a crash between the two, check_transfer_conflicts found the operation's own half-applied transfer and returned bad_request, which is dismissed and never retried — so the peer permanently lacked the destination replica entry. Worse, a later Finish on that peer then silently skips both the destination promotion and the source removal, pinning a replica-set divergence. Exclude the transfer's own key from the conflict scan so a replay falls through and re-runs the (idempotent) start: register_start_shard_transfer is a set-insert and the destination replica-state write is absolute, so re-running reconciles the partial state instead of dismissing it. A genuinely conflicting transfer (different key touching the same shard/peers) is still rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3fd4e187dd |
[audit-J] Stop swallowing config.save errors in resharding
start_resharding, finish_resharding and abort_resharding each saved the updated collection config with a log-only `if let Err(err) = config.save(..)` that swallowed the failure. A swallowed save let the operation report success with a stale shard_number persisted on disk, arming a shard-dir/loader panic (or a silently unloaded shard) at the next restart. Propagate the error with `config.save(&self.path)?;` instead. The resulting IO error is a ServiceError, so consensus halts and retries the entry after restart. That is safe because in all three functions the config save is value-idempotent (guarded by `shard_number != new_shard_number`) and every step is replay-tolerant, so the retry converges. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
58d55ba923 |
[audit-H] Persist decremented shard count before dropping shard in abort_resharding
In the up-direction of abort_resharding, ShardHolder::abort_resharding drops the new shard's directory, but the config.params.shard_number update was persisted only at the very end of the function — after the transfers abort. A crash anywhere in that wide window left shard_number pointing at an already-deleted shard directory, which makes the auto-sharding loader panic on the missing dir at startup (crash loop), before the consensus replay that would reconcile the state can run. Move the shard-count update block to before the shard_holder.abort_resharding call. The block keeps its value-idempotence guard, so replay converges. The config write lock is taken while holding the shard_holder write guard, matching the shard_holder -> config ordering already used in finish_resharding, and is released before abort_resharding. The reverse crash window (dir still present but count already decremented) is benign and reconciled by replay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b38c6d2dca |
[audit-H] Persist decremented shard count before dropping shard in finish_resharding
In the down-direction of finish_resharding, the config.params.shard_number update was persisted *after* drop_and_remove_shard. A crash between the two left shard_number pointing at an already-deleted shard directory, which makes the auto-sharding loader panic on the missing dir at startup (crash loop) — before the consensus replay that would reconcile the state can run. Move the shard-count update block to before drop_and_remove_shard (still after remove_shard_from_key_mapping). The block keeps its value-idempotence guard, so replay converges. The reverse crash window (dir still present but count already decremented) is benign and reconciled by replay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f9880fb702 |
[audit-A] Filter from_state in the forward-update failure path
When Medium/Strong write-ordering forwards an update to the leader and it fails with a transient error, the failure path proposed deactivating the leader replica while passing its raw peer state as from_state. Unlike the sibling deactivation site, this did not filter out transient/resharding states, so a Resharding/ReshardingScaleDown leader could be proposed Dead with from_state=Some(Resharding*). On re-apply after a crash inside abort-resharding (which reverts the gated replica to Active or removes it), the from_state gate no longer matches the current state -> bad_input -> the entry is dismissed and never retried, so the peer keeps Active while the rest of the cluster has Dead. Filter the leader state with `.filter(|state| !state.is_partial_or_recovery())`, matching the sibling site (PR #7849), so the proposal omits from_state for transient/resharding states and converges on replay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
dd84044a3e |
refactor: replace StructPayloadIndex::open bool flags with StorageType and IndexLoadMode (#9754)
StructPayloadIndex::open took two adjacent bools (is_appendable, create)
and call sites passed every literal combination: (true, true),
(true, false) and (false, true) all exist. A transposed pair compiles
and silently yields e.g. non-appendable + create instead of
appendable + load-only.
The target enum already existed: open immediately converted the bool
into the private StorageType { Appendable, NonAppendable }, so the bool
survived only at the API boundary, exactly where the swap hazard lives.
Make StorageType public, take it directly, and introduce
IndexLoadMode { CreateIfMissing, LoadExisting } for the create flag.
create_segment had the same trailing create: bool with bare literals at
both callers, so its parameter is lifted to IndexLoadMode as well:
load_segment passes LoadExisting, build_segment passes CreateIfMissing.
No behavior change.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
28b4377f03 |
Advance newest clocks for WAL-replay tail on load + model-test clock durability check (#9759)
* fix(recovery): advance newest clocks for WAL-replay tail on load `load_from_wal` replays the WAL synchronously up to `to` and queues the remaining `[to, last_wal_index)` tail to the update worker. Whether that tail exists depends on how far the persisted `applied_seq` lags the WAL end, independently of the `prevent_unoptimized` flag's value: the flag only gates the worker's deferred-points wait, not whether the tail is queued. The synchronous replay advances `newest_clocks` from each entry's clock tag, but the queued tail did not: the update worker deserializes only the operation and discards the clock tag. As a result the newest-clocks recovery point regressed across a graceful restart by up to the update-queue size, even though those operations are durably in the WAL (which is exactly what the recovery point tracks). Worse, the first post-restart updates would then be assigned clock ticks that earlier WAL entries already carry for different operations, which corrupts WAL-delta resolution in a cluster. Advance `newest_clocks` over the queued tail during load, mirroring the synchronous replay, before handing it to the worker. The range is end-exclusive because `last_wal_index` is one past the last entry. Regression dates to WAL replay honoring `applied_seq` (#8008), which narrowed synchronous replay from the whole WAL to `[from, to)`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(model): assert clock recovery point survives close+reopen Capture each shard's newest-clocks recovery point (flattened to shard_id -> (peer_id, clock_id) -> tick, tokens dropped) on both sides of every close+reopen in the model testing harness (mid-run restart and final reload) and assert exact equality. Both mismatch directions are bugs: a lost tick means clock durability broke, a gained tick means the reload path over-advanced a clock. The check runs after the existing model check so a lost WAL tail keeps its established extra/missing-id postmortem signature, and a clocks-only divergence surfaces distinctly. This is what caught the WAL-replay-tail clock regression fixed in the previous commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(recovery): tolerate unreadable WAL tail entries in clock advance Review follow-ups for the deferred-tail clock advance: - Log and skip a tail entry that fails to read instead of propagating the error: the update worker tolerates the same failure when it re-reads the entry, and failing here turns one bad tail record (or applied_seq/truncation index skew) into a shard, and by default a node, that cannot start. Add a red-green-verified regression test that injects an undeserializable record into the deferred tail. - Fix the pre-existing off-by-one in the send loop: last_wal_index is one past the last entry, so `to..=last_wal_index` enqueued a phantom op_num on every restart with a deferred tail. - Reword new comments (em dashes, and the range-bound note is obsolete now that both loops use the same exclusive bound; document the two-pass shape instead: the WAL iterator is not Send across await). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2735d40ecc |
Reset first_voter and prune address book on first-peer --reinit (#9785)
* Reset first_voter and prune address book on first-peer --reinit
A peer removed from consensus and killed after `RemoveNode(self)` was
committed but before it was applied keeps the old cluster's
`first_voter` and peer addresses in `raft_state.json`. First-peer
`--reinit` reset `conf_state` to a single voter (itself) but left both
untouched (`first_voter` is in fact never reset, even when the removal
is applied).
Both values are served to peers bootstrapping onto the reinitialized
cluster. A joining peer seeds its initial `conf_state` with the
advertised `first_voter`, and Raft conf-changes are deltas on top of
that base - so a stale `first_voter` permanently corrupts the joining
peer's voter set: it ends up with {old first peer, itself}, missing the
actual leader. Its `/readyz` then treats the still-alive old peer as a
cluster member and waits for the old cluster's commit index, which its
own consensus never reaches.
This only manifests when the reinitialized leader replicates its log as
plain entries (nothing applied before the kill, so the log anchor is
index 0). If the leader sends a snapshot instead, the snapshot's full
`conf_state` heals the corrupted seed - which is why
test_reinit_removed_peer only failed sporadically on CI.
Fix first-peer `--reinit` to behave like founding a fresh cluster:
reset `first_voter` to this peer (not `None`, or `recover_first_voter`
would re-derive the old first voter from the retained Raft log) and
prune `peer_address_by_id` to this peer only.
The kill-before-apply state is now injected deterministically into
test_reinit_removed_peer, reproducing the exact CI failure against the
unfixed binary. Since `--reinit` now prunes the address book, the
stale-address injection in
test_reinit_removed_peer_readyz_ignores_old_cluster moved to a restart
without `--reinit`, so it keeps exercising the /readyz `conf_state`
membership filter from #9688.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix test_reinit_consensus expecting stale address book after --reinit
The test waited for cluster size 2 right after starting the reinitialized
first peer, before the second peer was even started. That only passed
because first-peer --reinit used to keep the old cluster's addresses in
the address book - the stale state the previous commit removes. A
reinitialized first peer is a fresh single-member cluster; the other
peers re-join and re-register their new addresses right after.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
43e3d6ea8d |
[UIO] Request-specific load profile for read-only opens (#9797)
* Introduce request-specific LoadProfile with per-component placement A read-only shard opened for one known request (the serverless cold-start path) doesn't have to warm components the request will never touch. LoadProfile captures that from the request: warm components keep the persisted-config placement, everything else is parked cold. All placement decisions live in one place, so the memory placement of a whole segment under a profile is reviewable in one file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzbiZFVHooxKVoJX4k6Ais * Thread LoadProfile through the read-only segment open ReadOnlySegment::open takes an optional profile; first_preopen and open_via resolve it into per-component populate overrides so the opens make the same placement decisions the prefetches did. Pinned components that materialize on open regardless (quantized RAM storage kinds, the immutable-RAM sparse index) and appendable components ignore the override; the HNSW graph and immutable payload indexes demote fully. Config reloads follow the new config alone and pass no override. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzbiZFVHooxKVoJX4k6Ais * Open ReadOnlyEdgeShard under a request-derived load profile ReadOnlyEdgeShard::open takes an optional LoadProfile, applies it to every segment open and keeps it so segments discovered by a later refresh load with the same placement. ScrollRequestInternal and CoreSearchRequest gain load_profile() constructors, and edge-shard-query builds the request before the open and passes its profile (opt out with --no-load-profile). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzbiZFVHooxKVoJX4k6Ais * Demote pinned quantized vectors and sparse index under a cold profile Within the immutable layout the quantized RAM and mmap loaders share the on-disk format — only how the data is brought into memory differs — and the immutable-RAM sparse index has the same lazy mmap open low-memory mode already downgrades to. So a cold populate override now demotes the effective placement itself (Memory::with_populate_override, shared with the HNSW residency mapping) instead of only skipping cache priming: a pinned quantized storage opens the mmap kind cold, and a pinned sparse index opens as Mmap, so neither reads its data on a cold start. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzbiZFVHooxKVoJX4k6Ais * Add LoadProfile::merge for composite queries A composite query runs multiple core requests — e.g. a hybrid search runs one core search per vector, each with its own filter. Its profile is the union of its parts': merge extends the warm sets and ORs the payload-storage flag, so a component either part needs warm stays warm. The union is sound because every placement method is monotone in the warm sets (growing them only turns "park cold" into "keep configured placement"), so the merged profile dominates each input; and minimal, warming nothing no part asked for. Combine profiles with reduce, not fold: merge's identity element is the coldest profile (empty warm sets), the opposite of passing no profile at all — deliberately no empty()/Default constructor exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Adapt vanished-segment test to the profile-aware open signature The test landed on dev (#9777) after the load-profile signature change was written, so the rebase left its ReadOnlySegment::open calls without the new load_profile argument. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Defer vector index open entirely under a cold load profile A cold placement is not enough for the vector index on remote backends: GraphLinksView requires the whole links file as one contiguous slice, and the disk cache can only lend a borrowed slice once every block is locally present — so even a Cold HNSW open mirrors the entire links_compressed.bin (8.2 MB / 1.1 s per segment in the serverless cold-start trace), plus the unconditional graph.bin metadata read. The only way not to fetch the index is not to open it. LoadProfile::vector_index_placement is replaced by vector_index_deferred: a vector the request never scores now gets a DeferredVectorIndex — a new VectorIndexReadEnum variant holding the open arguments (an owned clone of the segment's raw backend, path, config, shared component handles) and a OnceLock. Nothing is opened or prefetched for it at segment open. Per-method policy of the deferred variant: - search, fill_idf_statistics and populate open the index on first use (with the cold placement the profile chose), so the profile contract holds: a request the profile did not predict still works, just pays the open then; - is_index reports true without opening (deferral only ever wraps a real HNSW or sparse index; plain opens no files and is never deferred); - telemetry, indexed_vector_count and sizes answer conservative defaults rather than trigger a remote fetch for a statistic. Tests: deleting the vector_index directory before an open under a scroll profile leaves open, filtered reads and payload reads working — proof that nothing of the index is read — while a search surfaces the missing files; and a segment opened under a scroll profile answers searches identically to an eagerly opened one via the transparent first-use open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make --vector optional in edge-shard-query: random query after open Omitting --vector on the search sub-command now searches with a random vector. The request is still built before the shard opens — the load profile only needs the vector name, not its values — with an empty placeholder; once the shard is open, fill_random_vector reads the dimension of the queried vector from the derived shard config and fills in uniform-random f32s (with a clear error if the named vector is not in the config). The vector is generated once, so live-reload iterations re-run the identical random query and the printed diffs stay meaningful. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Scope index deferral to the HNSW graph via a lazy OnceLock load Replace the DeferredVectorIndex wrapper (and the VectorIndexReadEnum:: Deferred variant) with deferral inside ReadOnlyHNSWIndex itself: the graph lives in a OnceLock (same first-wins arbitration as ReadOnlyRoaringFlags::bitmap) alongside the retained raw backend and residency, and loads on first use with a cold placement. The config read stays eager — one tiny, absence-tolerated file — so telemetry, is_on_disk and indexed_vector_count report real values where the Deferred arms answered with hard defaults. The sparse index needs no deferral: its mmap open reads lazily, with only small JSON metadata eager. A profile that never scores the vector now passes a cold placement override (LoadProfile:: vector_index_placement) into the eager open_sparse, which demotes ImmutableRam to the lazy Mmap open like low-memory mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Express HNSW graph deferral as a populate override, not a bool param Replace the `deferred: bool` on the read-only HNSW open/preopen (and the VectorIndexReadEnum pass-through) with the same `populate_override: Option<Populate>` every other component takes. A cold *override* defers the graph load — graph_deferred() mirrors the cold-override match of open_sparse — while a config-derived cold placement (or the low-memory clamp) keeps the eager load, since only a request-specific override carries the "never scored" prediction. With dense and sparse now consuming the same signal, LoadProfile::vector_index_deferred is gone: a single vector_index_placement() serves both index kinds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Serialize the deferred graph load via once_cell's get_or_try_init Loading outside the lock (std OnceLock's fallible init is still unstable) let a search burst on a deferred vector fetch the whole graph once per thread. Swap the cell for once_cell::sync::OnceCell: the fallible load runs inside the cell's lock, concurrent first users block on the one load, and a failed load leaves the cell empty so the next caller retries. Addresses https://github.com/qdrant/qdrant/pull/9797#discussion_r3573727836 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bb7b995d14 |
[UIO] implement VectorIndexReadEnum::preopen (#9782)
* [UIO] implement VectorIndexReadEnum::preopen Schedule background prefetch of the HNSW graph (config, graph data, links) and sparse index (config, inverted index, version, indices tracker) files, wired into the segment's first_preopen for every dense and sparse vector. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Adapt index preopen to caller-controlled populate Now that CachedFs respects the caller's populate: prefetch Cached HNSW links with a background populate instead of the open's blocking one; populate the immutable-RAM sparse index data (read in full on open) while the mmap variant stays cold; apply the low-memory ImmutableRam downgrade in preopen_sparse since it now changes populate behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Decide sparse index preopen populate from the index type All readable sparse index variants share the compressed-mmap on-disk format, so the datatype dispatch in preopen_sparse selected nothing. Replace the per-TInvertedIndex preopen_ro trait machinery with one populate-parameterized preopen in the sparse crate: the effective index type alone decides whether the index data is warmed (immutable-RAM reads it in full) or parked cold (mmap reads lazily). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Derive sparse index preopen populate from the segment config The segment config's sparse_vector_data already carries the SparseIndexConfig, so preopen_sparse doesn't need to read the persisted copy at all: it derives populate from the segment-side index type and merely schedules the config file for open_sparse to consume — still a single fetch, without threading the parsed config through open_via. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Resolve index preopen placement via memory_placement The HNSW read-only open and preopen used the deprecated on_disk flag directly; resolve the graph residency like the writable open instead — memory parameter with on_disk fallback, clamped by low-memory mode, including the pinned placement. The sparse index preopen likewise derives its populate from the effective memory placement, so the cached mmap index is prefetched warm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1a61d0b09c |
Fix live-reload staleness of in-place-mutated files on caching backends (#9812)
* Mutate same-operation slots in place in append-only mode With append_only_mutations enabled, every mutation of an existing point clones it to a fresh internal id. Shard-level updates decompose one point write into several SegmentEntry steps (upsert_with_payload issues upsert_point plus set_full_payload/clear_payload), so a single upsert burned one slot per step, leaving a chain of immediately-dead clones. A slot whose version already equals op_num was written by an earlier step of the current operation. It cannot be durable yet — the segment write lock is held across the whole operation, so no flush (and no read-only follower) can have observed it, and versions flush last so a crash discards it and WAL replay re-applies the whole operation. Mutate such slots in place: one operation now allocates exactly one slot regardless of how many steps it decomposes into. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stamp payload storage version in write_point_parts overwrite_payload mutates payload storage, but the fused write never bumped version_tracker's payload version — the old CoW path did, via set_full_payload. The segment manifest would stamp payload files with a stale version, letting a partial snapshot skip payload storage that contains the moved point's row, so the restored id tracker would point at an offset with no payload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Resync ReadOnlyRoaringFlags from disk on live-reload The flags file is preallocated to power-of-two capacity and mutated in place within its length, which the held handle's reopen() — an append-only-growth contract on caching backends — never picks up: bits the writer changes inside already-cached DiskCache blocks stayed stale forever on shard followers. Drop the `impl LiveReload for ReadOnlyRoaringFlags` altogether: this storage holds arbitrary flags with no notion of points, so a point-delta interface (deleted/new points) did not belong here — open never applied deleted points either. Replace it with an inherent live_reload(fs) that opens a fresh StoredBitSlice (a fresh open always mirrors the current remote bytes), resyncs the materialized bitmap from it, and swaps the handle. The on-disk flags are the sole source of truth. To make refresh-by-fresh-open safe while the old handle is still alive, every DiskCache open now mirrors into a uniquely-named local file (.{pid}-{counter} suffix) and removes it on drop. Mirrors were already truncated on every open (block validity is in-memory only), so the stable name carried no state. Also add trace logging for live-reload consumed mapping changes and pending deltas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Resync immutable id tracker deleted bitmap via fresh handle on live-reload The immutable tracker's id_tracker.deleted file is a fixed-size bitmap whose bits the writer flips in place — its length never changes — so the held handle's reopen(), an append-only-growth contract on caching backends, never picked it up: the pre-deletion state cached at open was served forever and live-reload never reported deletions on shard followers. live_reload now takes the fs, opens a fresh StoredBitSlice (a fresh open always mirrors the current remote bytes), diffs it against the tracker's current state — mappings already reflect every previously reported deletion, so they are the baseline — and swaps the handle in. ReadOnlyIdTrackerEnum and the segment-level reload pass the fs through; the appendable and disk-resident variants keep their no-arg reloads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Resync disk id tracker deleted bitmap via fresh handle on live-reload Same staleness as the immutable tracker: the deleted file is a fixed-size bitmap mutated in place, so reopen() — append-only-growth on caching backends — served the pre-deletion state forever. live_reload now takes the fs, opens a fresh StoredBitSlice, and swaps it into deleted_file, so the per-point get_bit lookups read fresh state from then on too. The deleted_full take/diff/set baseline logic is unchanged. The enum's DiskResident arm passes the fs through. The regression test now covers both trackers over DiskCacheFs; the disk leg was verified to fail under the old reopen-based behavior. The disk tracker's versions file staleness is a separate, still-open case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Introduce header-stateless ReadOnlyTracker for gridstore live-reload The gridstore tracker file is preallocated and mutated in place (header rewrites, slot updates), which the reader's held handle never picked up on caching backends: Tracker::live_reload read the header through the stale handle — before reopen(), and reopen() would not have refreshed cached blocks anyway — concluded "no new pointers", and never reloaded pages. New points' payloads read as empty on shard followers. Replace the reader's tracker with a dedicated ReadOnlyTracker that holds nothing but the storage handle: reads don't need header state (slot addressing is positional, unwritten slots read as None in the zero-initialized file) and readers have no pending-updates buffer. Its live_reload opens a fresh handle and swaps it in. Tracker::live_reload is deleted. A new TrackerRead trait (max_point_offset/get/iter) is implemented by both the writable Tracker and ReadOnlyTracker, and GridstoreView is generic over it, so writer and reader share the read logic. max_point_offset reads the stored header count as plain data through the current handle (fresh as of the last reload) rather than deriving it from slot capacity: sparse vector storage builds total_vector_count from it, and the capacity of the preallocated file (1MB -> 65536 slots) would inflate every follower segment's sparse count. The method is now fallible; consumers propagate the error. GridstoreReader::live_reload refreshes tracker and pages unconditionally — the tracker carries no reliable cheap change signal. Making this incremental again is a deferred follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Re-open last held chunk on chunked-vectors live-reload Chunk files are preallocated to full size, so appended vectors are in-place writes within the existing file length. On caching backends a held handle keeps serving blocks fetched earlier — and a block fetched near the old tail extends into then-unwritten space — so vectors appended into that block read back as stale bytes after a reload. Mirror Pages::live_reload: on a len change (the status file is read through a fresh open every reload, so it stays a reliable gate), drop and re-open the last held chunk — the only one that can have gained vectors — alongside adopting newly created chunk files. Earlier, fully-filled chunks keep their handles; their contents cannot change. Covers dense, multi-dense, and quantized-chunked storages, which all delegate to ChunkedVectorsRead::live_reload. Avoiding the whole-chunk refetch per reload is a deferred follow-up, together with the analogous gridstore pages/tracker case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix lint: fs_err::create_dir_all in tests, drop unnecessary cast CI clippy runs with --all-targets and disallows std::fs methods in favor of fs_err; the new live-reload regression tests used std::fs::create_dir_all directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Tombstone-only deletes on non-appendable segments in append-only mode The tombstone-only delete path existed but was gated on is_append_only(), which requires the segment to be appendable. Deletes landing on non-appendable segments — notably the CoW update deleting the point's old copy — still went through delete_point_internal, which clears the payload row in place before the id-tracker drop. Clearing the payload destroys committed state of an offset that stays visible to live-reload followers until the drop is flushed: a follower refreshing in that window resolves the point through its (unchanged) id-tracker view and reads an empty payload — observed as a one-refresh {} payload flicker on a point update. Writer-side flush ordering cannot close the window, since the follower samples the id tracker and the payload storage at different instants; the versions commit protocol protects inserts exactly because the marker is read first, and deletes have no marker. This is the same failure class for which vector-deletion propagation was disabled (see the comment in delete_point_internal). Gate deletes on the new is_append_only_delete() — append_only_mutations alone, no appendability requirement: tombstoning needs nothing from the segment but the id tracker. Normal deployments keep eager payload clearing and prompt space reuse; clone-based mutations keep requiring appendability via is_append_only(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: chunked vectors reload --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Daniel Boros <dancixx@gmail.com> |
||
|
|
c6a13505d0 |
fix: return error for missing point IDs in Query API recommend (Fixes #9390) (#9638)
* fix: return error for missing point IDs in Query API recommend The Query API recommend handler (RecommendAverageVector, RecommendBestScore, RecommendSumScores) used filter_map in resolve_reco_reference, which silently dropped point IDs that could not be resolved. The legacy /points/recommend API correctly returns a PointNotFound error in this case. Fix: change resolve_reco_reference to return CollectionResult and propagate errors instead of filtering them out, matching the pattern used by Nearest, Discover, Context, and Feedback query types. Signed-off-by: rtmalikian <rtmalikian@gmail.com> * fix: apply cargo fmt to collection_query.rs resolve_reco_reference Format resolve_reference() calls as method chains to satisfy rustfmt. Addresses reviewer feedback from timvisee: CI lint failure. Signed-off-by: rtmalikian <rtmalikian@gmail.com> * test: add unit tests for resolve_reco_reference error on missing point IDs (Fixes #9390) - test_missing_point_id: verifies error when ID not in ReferencedVectors - test_valid_point_id: verifies success when ID is present - test_mixed_point_ids: verifies error when mix of valid/invalid IDs Requested by @timvisee in PR review. Signed-off-by: rtmalikian <rtmalikian@gmail.com> * fix: apply cargo fmt to resolve_reco_reference tests Signed-off-by: rtmalikian <rtmalikian@gmail.com> * fix: apply cargo fmt to resolve_reco_reference test imports Move `use super::*` after external crate imports to satisfy rustfmt. Signed-off-by: rtmalikian <rtmalikian@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: rtmalikian <rtmalikian@gmail.com> Co-authored-by: root <111755117+qdrant-cloud-bot@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
3a9bff3e5b |
[UIO] implement ReadOnlyQuantizedVectors::preopen (#9781)
* [UIO] implement ReadOnlyQuantizedVectors::preopen Schedule background prefetch of the quantization config, per-method metadata, quantized data and multivector offsets, wired into the segment's first_preopen for every dense vector with quantization configured. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * additional stage for quantized config preopen * better quantized preopening * Partially populate the first quantized vector in preopen The load reads the first vector off quantized.data (and off the first chunk in the chunked layout) to validate the stored vector size — on a cold open that was a round-trip. Use Populate::Partial (#9769) to prefetch exactly that prefix; the exact size comes from the quantized config the preopen already read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restore the layout-probing quantized preopen Recovers the pre-rewrite design: preopen never reads the quantization config — it probes both layout candidates against the listing snapshot (a segment only contains the layout its real config selects), derives warmth from the segment-side quantization config (placement resolution incl. cached and low-memory), iterates storage types exhaustively for layout coverage, and schedules the config for open to parse once off the parked handle — no threading, no second preopen stage. Keeps the first-vector partial populate, with the size now derived from the segment config via construct_vector_parameters, and keeps VectorStorageType::is_pinned. Restores the full preopen test matrix and OneshotFile::preopen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review fixes --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Luis Cossío <luis.cossio@outlook.com> |
||
|
|
f982ae63fc |
feat: read-only edge grouping/matrix search + object-storage read path (#9691)
* feat: read-only edge grouping/matrix search + object-storage read path Add query_groups (group by a payload field) and search_matrix (single-shard distance matrix over a random sample) to the read-only edge shard's EdgeShardRead API, in new grouping and matrix modules plus edge test helpers. Make the object-storage read path available outside tests: drop the #[cfg(test)] gate on the BlobFile UniversalReadExt impl and move io_bridge_object_store/object_store to segment's normal dependencies, so a ReadOnlyEdgeShard can serve segments read from S3. * Share group-by building blocks between server and edge Move GroupsAggregator, group candidate query shaping (is-empty filter, group_by payload selector, prefetch limit scaling) and result-order derivation into shard::grouping / shard::query, so the collection and edge grouping implementations cannot silently diverge. Edge grouping now handles multi-valued group keys, u64 keys, wildcard group_by paths, prefetch limits and score-ordered groups the same way as the server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Drive group-by through a shared sans-IO state machine Extract the multi-request collect/fill loop into shard::grouping::GroupByDriver: next_request() yields shaped backend queries, add_points() advances the state, distill() returns the groups. Query execution stays with the caller, so the async server path and the sync edge path drive the same machine, and the request shaping helpers become private to shard::grouping. Edge now uses the same request budget (5 collect + 5 fill requests) and per-request candidates limit (groups * group_size, computed inside the driver) as the server, replacing its single 4x-oversampled request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: generall <andrey@vasnetsov.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
273bbe1729 |
Fix use-after-free in u8-quantized vector reads on owning storages (#9801)
* Fix use-after-free in u8-quantized vector reads on owning storages EncodedVectorsU8::get_vec_ptr extracted a raw pointer from the buffer returned by EncodedStorage::get_vector_data and dropped the buffer before the pointer was dereferenced. For storages returning Cow::Borrowed (mmap) this happened to be sound, but for storages that return Cow::Owned (disk-cache misses, uring backends) every user of get_vec_ptr read freed memory: the internal SIMD scorers, encode_internal_vector, and get_quantized_vector_offset_and_code, which exported the dangling buffer through a safe &[u8]. Make parse_vec_data return the code as a slice borrowing the input, so the borrow checker forces callers to keep the storage buffer alive while reading it, and drop get_vec_ptr entirely. This matches how encoded_vectors_binary already binds the buffers at its scoring sites. Change get_quantized_vector_offset_and_code to return the code as a sub-view of the buffer Cow itself, and adapt its GPU caller. The new regression test drives these paths through a storage that always returns owned buffers; under Miri it reproduces the use-after-free on the previous code and passes with this fix. Fixes #9799 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * additional assertion --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Ivan Pleshkov <pleshkov.ivan@gmail.com> |
||
|
|
f82d7584a2 |
Resolve vanished segments against the manifest in follower refresh (#9792)
Third step of the ReadOnlyEdgeShard not-found handling (after #9763 and #9777): shard-level resolution of live-reload failures, per the live-reload design requirements. refresh() is now a bounded retry loop over single-manifest-snapshot attempts. Within an attempt every survivor live-reloads even if one fails (they are independent); failures split by classification: - Not-found: resolved against a re-read manifest. Gone from the manifest means the leader removed the segment mid-reload — drop it and re-run the attempt to pick up its replacements immediately. Still listed means essential files are genuinely missing — escalate. - Anything else: escalate after reloading the other survivors, replacing the previous warn-and-swallow that could hide a corrupted segment forever. Safe: a failed segment keeps serving its pre-refresh state and pending_reload replays the unapplied delta on the next refresh. If attempts run out (leader churning segments continuously) refresh logs a warning and returns Ok: every swap was atomic, so the shard is consistent, just possibly not the newest; the next refresh continues. open_with_enumerator now reuses the refresh machinery: empty holder + refresh() + the open-only merge_follower_config overlay, removing the duplicated load/derive logic. At open there are no survivors, so open behavior is unchanged (#9762 already made it tolerant of unloadable segments via the superset-biased manifest contract). Load-side failures stay as settled by #9762: unloadable listed segments are skipped and retried on every refresh. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
da7eedeaf3 |
Allocate a single slot per operation in append-only mutations (#9804)
* Fuse CoW point move into a single destination write The CoW arm of apply_points_with_conditional_move wrote a moved point in three SegmentEntry steps: upsert_point_raw, update_vectors, set_full_payload. With append_only_mutations enabled every step clones the whole point to a fresh internal id, so one moved point burned three slots (the first holding an empty point for plain upserts, which clear raw_vectors) and left two immediately-dead clones behind, tripling the id-tracker changelog and vector writes. Add SegmentEntry::upsert_moved_point, which writes raw vectors, the decoded overlay, and the payload in one operation — allocating exactly one slot — and use it on the CoW move path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Mutate same-operation slots in place in append-only mode With append_only_mutations enabled, every mutation of an existing point clones it to a fresh internal id. Shard-level updates decompose one point write into several SegmentEntry steps (upsert_with_payload issues upsert_point plus set_full_payload/clear_payload), so a single upsert burned one slot per step, leaving a chain of immediately-dead clones. A slot whose version already equals op_num was written by an earlier step of the current operation. It cannot be durable yet — the segment write lock is held across the whole operation, so no flush (and no read-only follower) can have observed it, and versions flush last so a crash discards it and WAL replay re-applies the whole operation. Mutate such slots in place: one operation now allocates exactly one slot regardless of how many steps it decomposes into. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stamp payload storage version in write_point_parts overwrite_payload mutates payload storage, but the fused write never bumped version_tracker's payload version — the old CoW path did, via set_full_payload. The segment manifest would stamp payload files with a stale version, letting a partial snapshot skip payload storage that contains the moved point's row, so the restored id tracker would point at an offset with no payload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bb0e5d7544 |
Fix flaky disk exhaustion in edge-test CI job (#9807)
The publish/ directory is a separate Cargo workspace, so it does not inherit the root workspace's [profile.dev] debug = "line-tables-only" override. Example binaries were built with full debug info, each statically linking qdrant-edge at ~700 MB per binary. Linking several of them concurrently ran the runner out of disk, surfacing as "mold: failed to write to an output file. Disk full?" + SIGBUS. Mirror the debug info override in the publish workspace and free ~20 GB of unused preinstalled software on Linux runners as headroom. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5593bc5564 |
Add optional last-modified timestamp to ListedFile and CachedFs FileInfo (#9803)
* Add optional last-modified timestamp to ListedFile and CachedFs FileInfo Filled where the listing backend exposes one: local filesystems (entry metadata) and object stores (ObjectMeta::last_modified). The uio-grpc backend reports None since the RPC does not carry mtimes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Carry last-modified over the StorageRead ListFiles RPC Extend ListFilesEntry with an optional google.protobuf.Timestamp, fill it on the server from the listing metadata, and convert it back to SystemTime in the uio-grpc client. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Update lib/common/io_bridge_object_store/src/source.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix missing SystemTime import in io_bridge_object_store The CodeRabbit-suggested last_modified mapping used SystemTime::from without importing std::time::SystemTime, breaking compile and CI. Co-authored-by: Cursor <cursoragent@cursor.com> * Retrigger CI after flaky integration-tests-consensus timeout The compile fix is in; the prior run failed on an unrelated 30s timeout in test_collection_recovery, not on PR changes. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: root <111755117+qdrant-cloud-bot@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
e7340ed160 |
Stamp payload storage version on append-only clone paths (#9806)
clone_and_mutate_point and clone_and_replace_point_raw rewrite the point's payload row at a fresh internal id, mutating payload storage — but never bumped version_tracker's payload version. Payload operations bump it at the caller, so their clones were stamped; vectors-only operations (upsert_point, upsert_point_raw, update_vectors, delete_vector) never stamp payload, leaving the manifest with a stale payload version. A partial snapshot could then skip payload storage files that changed, and the restored id tracker would point at offsets with no payload row. Stamp inside the clone helpers, where the mutation happens. The payload entry points move their bump into the in-place closure so the clone arm is not double-bumped: a same-version double bump collapses the tracked version to None, degrading the stamp to the segment version. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
348a7428ab |
docs(segment): fix mislabeled doc comments in index (#9794)
- geo_hash: reference the real MAX_LEN const (was MAX_LENGTH); inline code since it is private - hnsw config: realign field doc comments (m/m0/ef_construct/ef were shifted by one) and document ef - query_estimator: adjust_to_available_vectors doc named a non-existent total_vectors param (it is available_points) |