mirror of
https://github.com/qdrant/qdrant.git
synced 2026-09-25 07:27:41 -05:00
edge-docs-diff
190
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6cd816acac |
Add match: { substring } filter condition (#10711)
* 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>
|
||
|
|
5e32ea89cb |
Reject snapshot upload without collection config, without exposing the temp path (#10556)
* Reject snapshot upload without collection config before loading it The raw IO error from `CollectionConfigInternal::load` embedded the server-side temporary path in the API response. Check for the file first and return a fixed bad-input error instead. Part of #10553 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3o9eWSZNMa6WAs5F2HMZC * Fix missing-config check to run before restore_snapshot loads config The path-leak guard lived after Collection::restore_snapshot, but that function already calls CollectionConfigInternal::load and surfaced the temp path as a 500. Require a regular config.json file before loading, and cover a directory-shaped config entry in the openapi test. * Use a valid empty TAR in the missing-config snapshot upload test Avoid depending on malformed-archive handling; exercise the missing collection-config path with a real TAR that has no entries. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: qdrant-cloud-bot <111755117+qdrant-cloud-bot@users.noreply.github.com> |
||
|
|
95eb3c3f79 |
Support cached id tracker memory placement (#10598)
Thread a `Populate` through the disk-resident id tracker's open paths (`DiskMappingReader`, `DiskIdTracker::open`, `ReadOnlyDiskIdTracker`, `ReadOnlyIdTrackerEnum`) so a `cached` placement primes the page cache with the mapping files on load instead of leaving them to page in on demand. The populate is derived from the segment config's placement at load time, clamped by low-memory mode, in both the writable segment open and the read-only one. The update-only lookup path keeps its transfer-nothing policy, and the build-time open stays cold: the built segment is reloaded anyway. `cached` is no longer rejected by validation. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
81bb80a5d3 |
Expose id tracker memory placement in collection config (#10597)
Add `id_tracker: { memory: cold | pinned }` to CollectionParams,
CollectionParamsDiff and CreateCollection (REST + gRPC `IdTrackerParams`),
mirroring `payload: { memory }`. `cold` builds the disk-resident id tracker,
`pinned` the in-RAM immutable one. Unset keeps the current behavior: the
`serverless_compatible` feature flag decides.
The requested placement is persisted as an optional `id_tracker_memory` on
SegmentConfig (skipped when unset, so existing configs are unchanged); the
segment builder resolves it through `SegmentConfig::id_tracker_memory_placement`
instead of reading the feature flag directly.
The config mismatch optimizer rebuilds non-appendable segments whose effective
placement differs from the requested one. Appendable segments are skipped: they
always use the mutable tracker and get the current config when indexed.
`cached` is rejected by validation: the disk mapping reader has no
populate-on-open path.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
aa4a62f5b2 |
docs(schema): declare enforced 1..=65536 bound on VectorParams.size (#10324)
* docs(schema): declare enforced 1..=65536 bound on VectorParams.size The REST layer enforces an upper bound of 65536 on VectorParams.size via a custom validator (validate_nonzerou64_range_min_1_max_65536), but custom validators contribute no bounds to the generated JSON schema - so the published OpenAPI document only declared minimum: 1, while DenseVectorConfig.size already documented both bounds. Add an explicit #[schemars(range(min = 1, max = 65536))] attribute so clients validating requests against the schema see the same contract the server enforces, update docs/redoc/master/openapi.json accordingly, and pin the bound with a unit test asserting the generated schema. Fixes #9942 * test(openapi): accept documented size bound as a rejection path test_vector_dimension_limit asserted that an oversized VectorParams.size reaches the server and returns the exact runtime 422 message. Now that the enforced 1..=65536 bound is documented in the served OpenAPI schema (#9942), request_with_validation rejects such payloads client-side before sending. Accept either layer: a client-side jsonschema.ValidationError or the server-side validation error. * test(openapi): handle both rejection layers in dimension limit pytest.raises only covered the client-side jsonschema rejection; if the request reached the server instead, the test would fail on an unhandled response. Use try/except around request_with_validation and assert the server-side status and exact error message in the else branch. * test(openapi): assert exact HTTP 422 on server-side rejection A broad not-ok check would pass on any error status carrying the same error text; pin the documented contract to 422. * refactor(tests): address review feedback Remove the unit test asserting the generated VectorParams schema shape - it only restates the schemars attribute and adds maintenance cost. Reduce test_vector_dimension_limit to its actual contract: an oversized dimension is rejected by the documented OpenAPI schema before the request is sent. |
||
|
|
83fe47c90a |
feat: add a dedicated min operator to score formulas (#10296)
Follow-up to #10287, which added `max`. Expressing a minimum still required spelling out `(a + b - |a - b|) / 2`, the sign flip of the max identity — drop the `neg` and you silently get a maximum instead. It also only works for two operands and mentions each one twice, so the scorer walks every sub-tree twice per candidate point. The pair is what makes clamping expressible: {"max": [0.0, {"min": [1.0, "$score"]}]} `min` mirrors `max` throughout, and both guard helpers introduced in #10287 already took an `operator: &str`, so they are reused unchanged: an empty operand list is rejected at parse time rather than folding to +infinity, and the Edge FFI rejects it at construction time. The result needs no `is_finite` check, since `min` cannot produce a non-finite value from finite inputs. The unindexed-field walker shares one arm for `Max | Min` as the bodies are identical, with a test pinning `min` separately so a later split cannot silently drop it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e2d42462fa |
feat: add a dedicated max operator to score formulas (#10287)
* feat: add a dedicated max operator to score formulas
Expressing a maximum in a score formula required spelling out the
arithmetic identity `(a + b + |a - b|) / 2`. That is easy to get wrong
(the `/ 2` is load-bearing), only works for two operands, and mentions
each operand twice, so the scorer evaluates every sub-tree twice per
candidate point.
`max` is variadic, mirroring `sum` and `mult`:
{"max": ["$score", {"mult": [0.5, "popularity"]}]}
Unlike `sum` and `mult`, `max` has no identity element for the empty
case, so an empty operand list is rejected at parse time rather than
folding to -infinity and scoring every point with a non-finite value.
The check lives in `ExpressionInternal::parse_and_convert`, which every
entry point passes through, and the Edge FFI additionally rejects it at
construction time to match how that crate validates elsewhere.
The result needs no `is_finite` check: unlike `log10`, `exp`, `div`,
`sqrt` and `pow`, `max` cannot produce a non-finite value from finite
inputs, so it follows the existing `sum` convention.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: cover max error propagation and datetime operands
An operand that fails must fail the whole expression rather than being
passed over in favour of a finite sibling. Covered with the failure both
before and after the finite operand: `mult` short-circuits on zero and
so can skip evaluating later operands, and this pins down that `max`
must not grow a similar shortcut that would swallow an error.
Also covers `max` over datetime operands, which reach the scorer through
a separate conversion to seconds, so that "score by whichever timestamp
is newer" is verified rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e32d3fbf89 |
Add acosh expression to formula query (#10231)
Unary inverse hyperbolic cosine, parallel to sqrt/ln/exp/log10, in REST, gRPC, and edge (FFI + Python) interfaces. Inputs below 1 produce the same NonFiniteNumber error as an invalid sqrt or ln. Closes #10186 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e45c248d38 |
add missing timeout parameter for get point API (#10235)
GET /collections/{name}/points/{id} already accepts ReadParams.timeout;
OpenAPI only documented consistency. Match the POST /points query parameters.
|
||
|
|
3a2bb87dab | tests: add request headers in validation tests (#10219) | ||
|
|
aa6c5d8403 |
Global quota API (#10035)
* feat: global quota API Memory and disk are node-wide resources, so configuring their thresholds per collection through strict mode makes little sense. Move them behind a single cluster-wide `QuotaManager`. The quota config is seeded from `storage.quotas` in the settings (and so from env vars), overridden by `quota.json` in the storage directory, and updated cluster-wide through a new `SetQuotaConfig` consensus operation which rewrites that file on every peer. Raft snapshots carry it too, so a peer that joins by snapshot picks it up. Quotas are enforced wherever the strict mode memory and disk checks used to run, but no longer gated behind `strict_mode.enabled`: a value set in an enabled strict mode config still wins per resource, the quota is the default. Rejections name both the condition that tripped and the config that governs it. `GET /quotas` reports the config plus current utilization to global read users; `PUT /quotas` replaces it for global manage users. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: cover the quota endpoints in the API consistency checks `test_all_rest_endpoints_are_covered` and the OpenAPI endpoint count both break on any new REST endpoint. Add `GET`/`PUT /quotas` to `ACTION_ACCESS` with their JWT access tests, and bump the expected API count. The quota endpoints stay out of `REST_ENDPOINT_WHITELIST`: that list is for data-plane endpoints reported per-endpoint in metrics. Also add a Raft snapshot CBOR compatibility test — snapshots are exchanged between peers of different versions during a rolling upgrade, so `quota_config` must be absent-tolerant in both directions. Review feedback: persist through `SaveOnDisk`, which already implements the write-before-swap protocol this was doing by hand; validate the config at both persistence boundaries, since a hand-edited quota file or a config arriving through consensus does not pass the REST handler's validation, and a `0%` limit would reject every update forever. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: assert seeding a quota from invalid settings persists nothing Follow-up to review feedback claiming `SaveOnDisk::load_or_init` writes the init value before it is validated. It does not — only `SaveOnDisk::new` persists — but the property matters: were seeding to persist first, invalid settings would leave a `quota.json` that fails validation on every subsequent start, and the node could only be recovered by deleting it by hand. Pin it down with a test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: make QuotaManager the single reader of memory and disk The quota checks measured memory and disk themselves, while the optimizer and the WAL disk watcher each called `fs4::available_space` behind their own ad-hoc caches. Fold all of it into QuotaManager: it owns the readings, the freshness policy, and the limits they are compared against. Moves the module to `lib/shard`, since the optimizer sits below `storage` and has to reach it; `storage::quota` re-exports it, so consensus, the `/quotas` API and StorageConfig are unchanged. The manager is installed as a process singleton by TableOfContent, ahead of loading any collection. - Callers hand in QuotaLimits overrides instead of a StrictModeConfig, and an override can now only tighten. A collection-level admin could raise `max_disk_usage_percent` past a cluster-wide limit that needed global manage rights to set; ties resolve to the quota so the rejection names the knob that actually has to change. - Measurements are cached for 5s, but a reading at or above its limit is never reused: a rejected client retries, and freeing the resource has to take effect on the next request rather than a TTL later. - `fits_on_disk` sizes an optimization against physical free space only, never the configured limits. Optimizations are what free a full disk, so the quota must not be what stops one. - `percent_of` widens to u128 instead of saturating the multiply, which under-reported utilization (failing open) above ~184 PB. - StorageConfig::quotas is optional; absent means no quota is enforced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: don't recover dead replicas onto a node at a resource limit Recovering a dead replica pulls a whole copy of its shard onto this node. If it is already at its memory or disk quota that transfer cannot finish, and starting it only pushes the node further past the limit. Skip it and reconsider on a later sync, once the resource frees up. Adds QuotaManager::check_capacity for work that lands bytes here without being an update. Unlike fits_on_disk the configured limits do apply: taking on a replica is not what frees a full node, so there is no deadlock to avoid by letting it through. The check is hoisted out of the per-shard loop because a node over its limit re-measures on every call, so checking per dead shard would cost a statvfs each. It is free when no quota is configured. Also trims the comments across the quota module, which had grown well past what the code needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: drop trivial and duplicated quota tests Six tests removed, ~140 lines, with no loss of coverage: - a_rejection_names_the_knob_that_has_to_change asserted that a format! contains its own literals; the message is covered end-to-end by the override test and by test_global_quota.py. - a_node_over_its_quota_has_no_capacity_to_take_on_a_replica was 30 lines for check_capacity, a one-line delegation to check_update the test above it already calls. - a_rejecting_measurement_is_never_served_from_the_cache duplicated the meter test, which proves the same rule with an injected reader instead of inferring it from the real filesystem. - free_space_is_reported_without_enforcing_anything covered a one-line accessor, and its point is what the fits_on_disk test is for. - The two resolve tests and the three meter tests each collapse into one. DiskFit::Unknown keeps its coverage as two lines inside the fits_on_disk test rather than its own fixture. The snapshot compat pair becomes one test: the second only asserted cluster_metadata.is_empty(), which says nothing about quotas — the real check was the deserialize, now an expect that states it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: re-measure free space as the disk fills, and drop a Windows-only assert Two CI failures, both from this branch. e2e test_low_disk: the DiskUsageWatcher I replaced escalated to checking on every call once free space fell below 512 MB. Folding it into the quota manager lost that — available_bytes passed no limit, so a reading was reused for the full 5s however little space was left. On a disk filling as fast as that test fills it, 5s blind is enough to actually run out and the WAL write dies instead of returning "No space left on device". available_bytes now takes a watch_below level and never reuses a reading under it, which is what the old ladder was expressing. The watcher passes max(min_free, 512 MB), so the escalation point is back; above it the 5s cache still costs fewer syscalls than the old 128-call ladder. fits_on_disk gets the same rule by passing required_bytes, so a merge that does not fit re-checks rather than sitting on a stale sample. Windows: fits_on_disk on a missing path was asserted to be Unknown, but GetDiskFreeSpaceEx resolves up to the containing drive and succeeds — as common::disk_usage's own test documents. Dropped; the branch is a two-line else and is not portably reachable. Also renames an_optimization_is_sized_against_the_disk_not_the_quota, which needed explaining to be understood. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: report the quota config in telemetry Reads it from the quota manager rather than the settings, so it is the config the node is actually enforcing: a peer that missed a consensus update reports what it is applying, not what the cluster agreed on. Gated on global access, the same access `GET /quotas` requires, and left out of `PeerTelemetry` — a quota is per-node state, so each peer reports its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: regenerate OpenAPI, and cover the quota in the telemetry key sets Two CI failures from the previous commit. Referencing QuotaConfig from TelemetryData moves its definition earlier in `components/schemas`, because TelemetryData is generated ahead of QuotaStatus. Regenerated rather than hand-patched, so the schema is a pure move. test_telemetry_detail asserts the exact set of top-level telemetry keys. The quota is reported at every level, including 0 — it is three scalars, it is the default the endpoint serves, and it is what explains an update being rejected — so both key sets gain it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: remove `max_disk_usage_percent` from strict mode Disk is a node-wide resource, so a per-collection percentage of it never meant anything a caller could act on: the limit describes how full the *node* is, and which collection the write happens to target has nothing to do with it. The global quota is where it belongs. It shipped in 1.18.2 without documentation, so this drops it outright rather than deprecating. Removal is soft in every direction: StrictModeConfig has no `deny_unknown_fields`, so a client still sending it gets it ignored rather than a 400, and the same struct deserializes the persisted collection config, so collections created on 1.18.2+ keep loading. Proto field 22 is reserved so the number is never reused. The e2e test becomes a quota test — the fixture and the timing are the interesting parts and they carry over unchanged; only how the threshold is configured differs. `max_resident_memory_percent` was documented and stays for now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: enforce the strict mode memory limit outside the quota `max_resident_memory_percent` was folded into the quota as an override, which meant the quota check had to know about strict mode, and retiring the setting would mean unpicking `EffectiveLimit` and `LimitSource` from the resolution logic. It is now a check of its own in `verification/mod.rs`, next to the strict mode checks it belongs with, borrowing only the measurement from the quota manager — which stays the node's single reader of process memory, so both checks still share one reading. Deleting the setting later is deleting one function and its one caller. `QuotaManager::check_update` takes no arguments and consults the quota alone. A collection can still only tighten the limit for itself, because its own check runs in addition rather than in place of the quota's, and each rejection now names the config that has to change without having to carry a `LimitSource` to say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: enforce the quota on the update path, not in strict mode The quota check sat inside `check_strict_mode_toc_batch` only because that was the one place holding the collection's strict mode config. It doesn't need one any more, and the placement had a real cost: coverage depended on each handler remembering to ask for a strict mode check, and four of the internal update RPCs do — `sync_internal`, which moves the most bytes onto a node, does not. It now runs in `Collection::update_from_client` and `update_from_peer`, which every update passes through. `update_from_client` checks ahead of the shard split, so an operation is accepted or refused whole rather than landing on some shards and being refused by others. Classification moves with it, from ~10 `consumes_memory` impls on request DTOs to one exhaustive `CollectionUpdateOperations::consumes_quota`. The internal enum has variants — raw upserts, conditional upserts, the syncs — that have no client-facing request type, so per-DTO impls structurally could not classify them. Shard-transfer syncs stay excluded, as they are today: a transfer is sized up once before it starts, and refusing its batches partway abandons work that is nearly done only for it to restart from the beginning. Index and named-vector creation reach shards through consensus, past this check — a peer must not refuse what the cluster agreed to — so they keep their pre-consensus check, now against the quota manager directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: deprecate `max_resident_memory_percent` in strict mode Same reason the disk threshold went: memory is node-wide, so a per-collection percentage of it caps how full the *node* is, which has nothing to do with which collection is being written to. The node-wide quota caps it once for everything. Unlike the disk threshold this one shipped documented, in 1.18.0, so it keeps working — as a limit a collection can tighten for itself, never lift — and gets the usual markers: `#[deprecated]` on both Rust structs, `[deprecated = true]` on proto field 21, and `deprecated: true` in the OpenAPI schema, which schemars derives from the attribute. The note names 1.21 as the removal. Recording a version matters here: the audit in docs/plans/overdue-deprecations.md found that this repo has never written a removal deadline down, and members of the 1.15.0 deprecation batch are still in tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: reconcile the quota readers with #9891 #9891 landed effective (cgroup) figures in telemetry while this branch was making QuotaManager the single reader of memory and disk. Two collisions, neither of which git sees. `segment::utils::mem::total_memory_bytes` is now a shared accessor with a 5s TTL, so a cgroup resize is picked up. The quota module had its own `OnceLock` copy that froze the value at startup — exactly what #9891 set out to fix — so it delegates to the shared one instead. Telemetry's new `disk_size` called `common::disk_usage::disk_usage` directly. That reader lost its TTL cache on this branch when the caching moved into the quota manager's meter, so it would have taken an uncached `statvfs` on every telemetry request, and it put a second disk reader back in the tree. It goes through `QuotaManager::disk_capacity_bytes` now, sharing the reading the quota check already takes. Verified it still reports the storage filesystem, matching `df`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: split the quota manager by what each half does `manager.rs` had grown to 450 lines holding three separate jobs: owning the config and its file, taking the readings, and comparing one against the other. - `manager/store.rs` — the `Store` enum, `QUOTA_CONFIG_FILE`, and config validation, which is now the store's own business rather than something every caller has to remember to do first. - `manager/measure.rs` — every reading, and `DiskFit`. The "nothing else calls `statvfs` or reads process RSS" claim is now checkable by looking at one file. - `manager/enforce.rs` — `check_update` / `check_capacity` and the threshold comparison. - `manager/mod.rs` — the struct, its construction, and the config accessors: what a reader needs to see first. Tests move with their subject. No behaviour change: `set_config` used to validate before delegating to the store, and now the store validates on write, which is the same order of operations from the outside. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: count copy-on-write deletes toward the quota Dropping a vector or a payload key does not free anything on its own: copy-on-write rewrites the point to produce the version without that field, so storage grows first and is only reclaimed once the optimizer gets to it. Gating those as if they were reclaiming space let a full node keep taking writes that make it fuller. Deleting whole points stays exempt. That is the one operation that has to work on a node at its limit, or there is no way back under it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: say that the quota's reported usage is per node `GET /quotas` returns one cluster-wide config and one set of utilization figures, which reads as though both describe the cluster. They do not: memory and disk are node-local, so `usage` is whatever the peer that served the request is seeing, and a peer under its limit says nothing about the others. Also corrects `resident_memory_percent`, which claimed to be a share of total system memory. It is a share of the memory available to the process, which under a cgroup is the limit rather than the host's RAM. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: treat a node over its quota as a failed replica, not a bad request A quota rejection described the request as invalid (400) and was classified non-transient, which is how the replica set recognises errors that every replica would produce alike. A quota is the opposite: the input is fine and the answer depends on which machine you ask. On the default `wait=false` path that combination silently dropped the write — `update.rs` only deactivates transient failures when nothing completed — leaving the replica Active and permanently missing data its co-replicas had. It is now `InsufficientStorage`, transient, HTTP 507 / gRPC `ResourceExhausted`. So a node that is out of room is handled like one that is offline: - last active replica, or every replica over quota: nothing could take the write, and the client is told the cluster is out of room. - more than one replica: the full node is deactivated through the same path a dead peer takes, and the update stands if enough replicas accepted it. `check_capacity` already keeps recovery off that node until it has room. The check also moves off `update_from_client`, which applied the coordinator's own limit to the whole operation even when it held no replica of the shards being written. Each replica set now gates its own local write and records the refusal as a failure of this peer, so a node only ever answers for itself. `ResourceExhausted` is shared with rate limiting, and the reverse conversion mapped it straight to `RateLimitExceeded` — a forwarded rejection came back as 429. Statuses now carry a marker so the two stay distinguishable across the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: report quota pressure per node, and across the cluster A quota is node-local, so finding out which node has hit one meant asking each of them in turn — and nothing at all showed up in monitoring. `/metrics` gains a `quota_exceeded` gauge for the local node. It is emitted only while the quota is enabled: with it off the value would be a constant 0 that says nothing about the node, and an alert built on it would go quiet rather than fire if someone disabled the quota. Telemetry's `quota` field carries the same verdict alongside the config, since that is where the metric is derived from. `GET /quotas` now answers for the whole cluster. A new `GetQuotaUsage` RPC on the internal `QdrantInternal` service returns what one peer is using, and the handler fans it out to every known peer in parallel. Peers that do not answer are left out rather than failing the request — the nodes that are out of room are exactly the ones most likely to time out, and a partial answer still names them. Outside distributed mode the field is absent rather than a map of one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: report the quota metric per resource `quota_exceeded` was one flag for the whole node, which does not say what to go and fix — disk is freed by deleting or optimizing, memory by unloading. It now carries a `resource` label: quota_exceeded{resource="memory"} 0 quota_exceeded{resource="disk"} 1 A resource with no limit gets no series at all, for the same reason the metric is absent while the quota is disabled: a series that can never reach 1 reads as healthy and would quietly carry an alert that cannot fire. `QuotaManager::exceeded` returns the per-resource verdict, with `None` for a resource this node does not cap. Telemetry reports the same breakdown, since the metric is derived from it. The peer usage RPC keeps a single flag — it sits next to both percentages, so it only has to answer "is this peer refusing writes". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: drop a no-op error conversion the linter caught `check_global_access` already returns a `StorageError`, so mapping it through `StorageError::from` converted the type to itself and tripped `clippy::useless_conversion`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: hold a tripped quota until usage clears a release margin A resource resting on its limit crosses it in both directions on the noise between two readings, and each crossing is expensive: the node refuses a write, its replica is deactivated, usage dips, recovery starts sending a whole shard copy back, and the arriving data pushes it over again. The loop sustains itself, and every lap costs a shard transfer. A limit now trips at its configured value but only clears once usage has fallen 5 percentage points below it, so the crossing has to be real. The margin is floored at 1%, since a limit smaller than the margin would otherwise be impossible to fall back under and would strand the node. The verdict is carried on the manager rather than recomputed, which makes it the thing reporting shows: expect `exceeded` to be set while the utilization next to it is already back under the limit. Rejections say so too, rather than claiming a limit that is no longer exceeded: Disk usage is at 87% of total capacity. It reached the configured limit of 90% and has to fall below 85% before this node takes writes again. Changing the config clears the verdicts. New limits are a deliberate act, and should not be held back by the margin of a limit that no longer exists. Both resources are now evaluated on every check instead of stopping at the first failure, so a verdict is never left behind reporting a reading that has since been superseded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: make the quota release margin configurable 5 points is a guess about how noisy a deployment's usage is, which is not something one number can be right about: a node whose disk moves in gigabyte steps needs a wider margin than one that creeps, and an operator who wants the old flip-on-every-reading behaviour should be able to ask for it. `release_margin_percent` joins the rest of the quota config, so it seeds from `QDRANT__STORAGE__QUOTAS__RELEASE_MARGIN_PERCENT`, replicates through consensus, and changes with `PUT /quotas`. Defaults to 5 and is filled in when a request omits it, so it always answers with the margin actually in force rather than leaving the caller to assume one. `0` releases as soon as usage is back under the limit. `QuotaConfig` grows a hand-written `Default` for it, since deriving one would have quietly defaulted the margin to 0 and disabled the hysteresis for anyone constructing a config in code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: leave the release margin unset by default, and hold verdicts in atomics `release_margin_percent` is `null` unless someone sets it, rather than materialising 5 into every config. A quota written today then does not pin a number a later release may want to revise, and `{"enabled": false}` still round-trips as itself. `QuotaConfig::limits` resolves it, next to `enabled`, so enforcement never sees the unset case. The verdicts move from a `Mutex<QuotaExceeded>` to one `AtomicBool` per resource. They are judged independently and nothing reads them as a pair, so the lock only added contention to the path every update takes; a verdict that races a concurrent check is re-decided by the next one from a fresh reading. That also drops the tri-state. Only "was this over its limit" has to survive between checks — whether a resource is enforced at all follows from the config and the reading, so it is derived when reporting rather than stored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: drop a comment arguing with a design that was never here Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8a7325ad5d |
Remove deprecated search endpoints from OpenAPI, deprecate them in gRPC (#9982)
* Remove deprecated search/recommend/discover endpoints from OpenAPI Remove deprecated REST API endpoint definitions from the OpenAPI generator. These endpoints were deprecated in v1.13.3 (`f4ced2567`, #5907, 2025-01-30) in favor of the universal `/points/query` endpoint: - POST /points/search - POST /points/search/batch - POST /points/search/groups - POST /points/recommend - POST /points/recommend/batch - POST /points/recommend/groups - POST /points/discover - POST /points/discover/batch Also removes the corresponding request types from the schema generator and updates the expected API count in the consistency check. Co-authored-by: Cursor <cursoragent@cursor.com> * Migrate OpenAPI integration tests to /points/query The deprecated /points/search, /points/recommend and /points/discover endpoints (along with their /batch and /groups variants) were removed from the OpenAPI spec, which caused validation failures in the Python integration test harness. This commit migrates the affected tests to the universal /points/query endpoint: - Delete tests dedicated to the deprecated endpoints: test_recommend.py, test_discover.py, test_multicollection_reco.py, test_recommendation_multivector.py - Refactor remaining tests to call /points/query (and /query/batch, /query/groups), translating request bodies (vector -> query / using, positive/negative -> query.recommend, target/context -> query.discover) and unwrapping the new result.points response shape. - Drop equivalence assertions against the now-removed legacy endpoints. Co-authored-by: Cursor <cursoragent@cursor.com> * Relax non-empty assertions in migrated recommend/discover tests The previous migration added `len(...) > 0` assertions to tests that previously only checked equivalence between the deprecated and new API. These assertions are too strict because the parametrized `query_filter` cases legitimately produce empty result sets. Drop the `> 0` assertion and rely on `request_with_validation` to verify the response is well-formed and HTTP OK. Co-authored-by: Cursor <cursoragent@cursor.com> * Migrate remaining OpenAPI tests off deprecated search endpoints Tests added to dev after the original migration was written still call /points/search and /points/recommend/groups through `request_with_validation`, which resolves the endpoint against the OpenAPI spec and therefore breaks once the endpoint is not in the spec: - test_turbo4_storage.py, test_sparse_idf_corpus.py, test_validation.py: translate /points/search to /points/query (vector{name,vector} -> query + using, result -> result.points). - test_group.py: drop the /points/recommend/groups half of the lookup_from validation test in favour of the query equivalent. test_sparse_idf_corpus.py's test_query_api_supports_idf_corpus goes away: with the helper on /points/query every test in the file now exercises what it asserted. Also record why test_recommend_group cannot assert on its groups: it uses every point in the collection as a recommend example, so all of them are excluded and the result is legitimately empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Regenerate openapi.json without the deprecated search endpoints Drops the 8 deprecated paths and the request schemas that only they referenced: Search/Recommend/Discover request (+Batch, +Groups) types and their exclusive dependencies (NamedVector, NamedSparseVector, NamedVectorStruct, UsingVector, RecommendExample, ContextExamplePair). Regenerated output is a strict subset of the previous spec, and every remaining $ref still resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Deprecate the search/recommend/discover RPCs in gRPC The REST counterparts have carried `deprecated: true` since v1.13.3 and are now gone from the OpenAPI spec, while the gRPC RPCs never got any deprecation annotation at all. Mark all 8 with `option deprecated = true` so generated clients warn, and point each doc comment at its `Query` replacement. tonic puts `#[deprecated]` on the generated client methods only; the server trait gets the doc comment alone, so our own `impl` is unaffected. The RPCs keep serving traffic — this is annotation only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Restore the deleted recommend/discover suites on /points/query The earlier migration deleted these four files outright, but the query-side tests it left behind are all shallow smoke tests (`len(result) > 0`, `"points" in result[0]`). The deleted ones carried invariants with no query-API equivalent anywhere, so deleting them was a real loss of coverage rather than de-duplication: - test_recommend.py: default strategy equals average_vector; batch results identical to sequential singles across six request shapes; best_score with only negatives yields all-negative scores; best_score with a single positive orders identically to a nearest query; raw vectors as examples equal ids as examples. - test_discover.py: context-only scores are all <= 0; target-only orders identically to a nearest query but scores differently; with a fixed context the integer part of the score is stable while the decimal part moves, and vice versa with a fixed target; batch equals singles; lookup_from by id equals by vector. - test_multicollection_reco.py: cross-collection lookup_from, plus wrong-vector-size, unknown-collection and unknown-vector rejections. - test_recommendation_multivector.py: the same recommend invariants over a max_sim multivector collection, which the query suite never covered. Only test_recommend_missing_lookup_from_collection_with_raw_vector is dropped as genuinely redundant — test_query.py's test_query_missing_lookup_from_collection covers query, query/batch and prefetch. Two request-shape differences the translation had to absorb: - Giving no examples at all is 422 (a RecommendInput validation rule), where the legacy API reported 400 from the query itself. A malformed example, such as an empty vector, is still 400. - DiscoverInput requires the `context` key and accepts only an explicit null to mean "no context", so target-only discover must spell it out. The legacy API let it be omitted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
446d140c2d |
Slice filtering condition: sliced scroll / deterministic sampling (#9899)
* feat: slice filtering condition for sliced scroll and deterministic sampling
Add a `slice` filter condition selecting points where
`stable_hash(point_id) % total == index`. The hash is SipHash-2-4 with a
zero key over canonical id bytes (8 LE bytes for numeric ids, 16 RFC 4122
bytes for UUIDs) — a frozen public contract, independent of the internal
resharding ring hash, reproducible by clients to predict membership.
For a fixed `total`, slices are disjoint and cover all points, enabling
parallel scroll streams (ES sliced-scroll style) and reproducible sampling
that composes with any other filter condition.
- REST: `{"slice": {"total": N, "index": R}}`; gRPC: `SliceCondition` in
the condition oneof (tag 8)
- Evaluated per point via id_tracker external-id lookup; no payload index
needed; cardinality estimated as `points / total` with no primary clause
- `total >= 1` enforced by NonZeroU32 at parse time, `index < total` by
validation in both REST and gRPC paths
- Hash contract locked by test vectors independently reproduced with a
reference SipHash-2-4 implementation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* tests: minimal OpenAPI test for slice filter condition
Scrolls all slices of a fixed total over numeric + UUID ids asserting
disjointness and full coverage, checks must_not inversion, and pins the
two rejection paths (422 for index >= total, 400 for total = 0). Requests
and responses are validated against the regenerated OpenAPI spec by the
test harness.
Note: the spec cannot itself reject total = 0 client-side — the Condition
anyOf falls through to the permissive Filter schema, as with any invalid
condition — so rejection is asserted via the server response.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
323ea66bfc | Add openapi tests for TQDT (#9884) | ||
|
|
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> |
||
|
|
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>
|
||
|
|
ab0d3ecc62 |
Add unified memory: cold|cached|pinned placement parameter for collection components (#9684)
* Add unified `memory: cold|cached|pinned` placement parameter for collection components
Introduce a single `memory` parameter that controls how each collection
component's data is held in RAM, replacing the inconsistent zoo of
`on_disk` / `always_ram` / `on_disk_payload` flags:
- `cold`: not pre-loaded from disk, cached with usage
- `cached`: pre-populated into page cache on load, evictable under pressure
- `pinned`: materialized on heap, never evicted by cache pressure
The parameter is available on dense vectors, HNSW config, all quantization
configs, the sparse index, all payload field index types, and payload
storage (as a new `payload: { memory }` sub-object on collection params).
When set, it overrides the deprecated legacy flag; when unset, behavior is
unchanged. Legacy flags are marked deprecated (Rust + proto) but keep
working; conflicts are resolved in favor of `memory` with a warning.
New capabilities enabled by the tri-state model:
- HNSW graph links can be pinned (first production caller of the existing
`GraphLinksResidency::Pinned`)
- sparse mmap index, quantized vectors and on-disk payload field indexes
gain a `cached` tier (mmap + populate on open)
`pinned` is rejected by API validation for components without a heap
variant (dense vector storage, payload storage). Low-memory mode degrades
placements at load time via `Memory::clamp_to_low_memory`, matching the
existing `prefer_disk`/`skip_populate` behavior. Effective-placement
comparison in the config-mismatch optimizer avoids spurious rebuilds when
the same placement is expressed through the new parameter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix gpu-gated tests for the new `memory` field
CI clippy runs with --all-features, which compiles the gpu-gated tests
that were missed locally: add the `memory` field to config literals and
allow deprecated placement params, same as in the rest of the tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add OpenAPI tests for memory placement, keep sparse config downgrade-clean
- OpenAPI tests: create/update collections with `memory` on every component,
assert the parameters are echoed in collection info, assert legacy-only
collections expose no new fields, and assert `pinned` is rejected (422)
for dense vector storage and payload storage on both create and update.
- Persist only the explicitly requested `memory` parameter in
`sparse_index_config.json` instead of the legacy-resolved placement, so
configurations using only the deprecated `on_disk` flag keep byte-identical
files that older Qdrant versions load without unknown fields.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Validate collection meta ops at construction, not only in the API layer
The `memory: pinned` rejection for dense vectors and payload storage
lived in `Validate` impls on the internal request types, which only ran
through the REST actix extractor. gRPC validates just the proto message,
so a gRPC client could persist `pinned` where it is not supported and
have it silently treated as `cached`.
Run the derived validation in `CreateCollectionOperation::new` and
`UpdateCollectionOperation::new` instead: the constructors are the
common chokepoint for all API paths, before the operation is proposed
to consensus. This covers every validator on these types, not just the
`memory` checks, and keeps consensus-apply unaffected so mixed-version
clusters never reject already-committed operations.
`UpdateCollectionOperation::new` becomes fallible; `remove_replica` now
uses `new_empty` since it carries no user config. Regression tests drive
the gRPC conversion path and assert `InvalidArgument` for `pinned` on
create and update, with `cold`/`cached` accepted as a control.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
efab63d024 |
Add prefix matching option to keyword index (#9683)
* Add prefix matching option to keyword index
Introduce an opt-in `prefix` option for the keyword payload index and a
new `match: { "prefix": ... }` filter condition, enabling efficient
byte-wise prefix filtering over keyword values (e.g. URL prefixes,
web-ui value autocompletion via facet + prefix filter).
Index side: a new `prefix_index.bin` file stores a sorted, front-coded
key dictionary with a resident block index (cumulative counts per
block); it is an ordered view over the keys of `values_to_points.bin`
and stores no postings. Presence of the file signals prefix support at
load time, so legacy segments load unchanged and enabling the option
goes through the standard incompatible-schema rebuild. The mutable
variant keeps an in-RAM ordered key set (not persisted), the immutable
variant builds a sorted key vector at load, and the on-disk variant
reads the dictionary lazily (block index resident, 1-2 block reads per
prefix lookup; reader is generic over UniversalRead).
Query side: prefix conditions are served from the dictionary when
available (filter + cardinality estimation from per-block aggregates),
from the forward index as per-point checks, and degrade to the payload
full-scan fallback otherwise - same execution model as other match
conditions. Strict mode (`unindexed_filtering_*`) rejects prefix
queries on fields without a prefix-enabled keyword index via a new
KeywordPrefix capability.
HNSW payload blocks: prefix-enabled indexes additionally emit prefix
blocks for heavy branching trie nodes (single-child chains collapsed to
their longest common prefix, one block per distinct point set, emitted
largest-first) so filtered search with prefix conditions gets navigable
subgraphs without rebuilding the same subset repeatedly.
API: `prefix` flag on KeywordIndexParams (REST bool, gRPC empty
message for extensibility), `prefix` variant in the Match oneof, edge
python bindings, regenerated OpenAPI spec.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Split prefix index into a dedicated module, fix clippy in tests
Reorganize the flat prefix_index.rs / prefix_read.rs into a
map_index/prefix_index/ module: format.rs (on-disk layout primitives),
writer.rs, reader.rs (PrefixIndex), map_read.rs (StrMapIndexPrefixRead
with per-variant impls) and tests.rs, with a file-format diagram and a
read-path walkthrough in the module docs. No logic changes.
Also fix clippy --all-targets complaints in test code: replace a
wildcard Match arm with an exhaustive list and a field-reassign-with-
default with a struct literal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add OpenAPI test for prefix match and snapshot file-tracking test
- tests/openapi/test_prefix_match.py: index-less fallback, prefix index
creation with schema echo, scroll/count parity against ground truth,
facet + prefix filter (the autocompletion flow), strict-mode rejection
without the prefix capability.
- test_prefix_index_file_tracking: `prefix_index.bin` is listed in
`files()` / `immutable_files()` exactly when built with the option.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Replace hand-rolled varint parsing with bytemuck Pod records
Per review: the prefix index format now uses fixed-size little-endian
Pod records (BlockEntry 24 B, KeyEntry 12 B, Header 40 B) written with
bytemuck::bytes_of and read back by copy via pod_read_unaligned — no
manual varint encode/decode, no alignment requirement, one shared
read_record helper. Costs ~9 bytes per key on disk versus LEB128; the
raw key bytes dominate dictionary size, so the simplification wins.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fetch the whole candidate block range with a single storage read
Candidate key blocks of a prefix lookup are contiguous in the file, so
enumerate them from one ranged read instead of one read per block; the
over-read versus the exact key range is bounded by the two boundary
blocks. Block decoding is split into a storage-free helper reused by
the per-block path of stats estimation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Align prefix payload blocks with the geo index granularity principle
Geo's large_hashes emits only the smallest geohash regions above the
threshold — a disjoint antichain, never a parent nested with its
children. Prefix payload blocks now follow the same rule: a heavy
collapsed trie node is emitted only if nothing heavy is nested inside
it, counting both deeper qualifying prefixes and single heavy values
(which already get their own exact-match blocks). Emitted blocks are
therefore mutually disjoint and disjoint from exact-value blocks; no
near-collection-sized ancestor subgraphs, no reliance on the HNSW
connectivity check to skip nested duplicates.
Implemented as a `covered` flag propagated through the existing
LCP-interval scan, still one O(total key bytes) pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Document block wire format and unaligned-read rationale in decode_block
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9941ed06a5 |
Add e2e tests for turbo4 vector storage datatype (#9674)
Covers turbo4 as vector storage (config persistence, search across Cosine/Dot/Euclid) and binary / turbo quantization layered on top of turbo4 storage (quantized search with rescore/oversampling on an optimized segment). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7b25a08799 |
test: add openapi regression test for values_count on missing fields (#9606)
* test: add openapi regression test for values_count on missing fields Reproduces https://github.com/qdrant/qdrant/issues/9586 where points with a missing payload field are not matched by values_count filters that should treat the count as 0. Co-authored-by: Cursor <cursoragent@cursor.com> * test: use module fixture for values_count missing field test Follow the standard openapi test structure with setup/teardown fixture instead of explicit drop_collection calls in the test body. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: treat missing field as count 0 for values_count filter (#9607) `FieldCondition::check_empty()` ignored the `values_count` condition, so a point with a missing payload field was never matched by a `values_count` filter. The desired semantics (and the existing `ValuesCount::check_empty`) treat a missing field as having a value count of 0. Forward the empty check to `values_count.check_empty()` so bounds like `lt: 1`, `gte: 0` and `lte: 0` correctly match points whose field is absent. Fixes #9586 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2c241a0b64 |
Respond HTTP 405 on cluster endpoints when in standalone mode (#9431)
* Return HTTP 405 on cluster endpoints when running in standalone mode * Add test * Skip some tests if not running in distributed mode |
||
|
|
fd04db2b14 | Fix inverted delete_vector return in appendable mmap storages (#9410) | ||
|
|
25a4d4906d |
fix: validate hnsw_ef search parameter (#9320)
* fix: validate hnsw_ef search parameter * chore: regenerate openapi spec |
||
|
|
ffe4087e0c | Add test to validate shard number and replication factor is positive (#9178) | ||
|
|
5e79cd76a2 |
fix(api): validate shard snapshot upload checksum (#9302)
Co-authored-by: Marcelo Machuca <skarL007@users.noreply.github.com> |
||
|
|
d7b6713dd0 |
test: reproduce MatchAny(any=[]) strict-mode rejection on integer index (#9260)
* test: reproduce MatchAny(any=[]) strict-mode rejection on integer index
Adds an openapi test that demonstrates the strict-mode bug where
`match: {"any": []}` on an integer-indexed payload field is rejected with:
Bad request: Index required but not found for "<field>" of one of
the following types: [keyword, uuid]
even though the field is indexed as integer. Root cause is that an empty
`any` list deserializes as `AnyVariants::Strings(empty)` (untagged enum;
Strings variant listed first), and strict mode infers a keyword/uuid
index requirement from the variant tag — ignoring that the list is
empty (i.e. a no-op condition).
The test covers:
- Baseline no-op semantics with and without indexes (passes today).
- Reproduction under strict mode for `must`, `must_not`, and a
FormulaQuery FieldCondition (currently failing; will pass once
empty `any`/`except` no longer requires an index).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: treat empty MatchAny/MatchExcept as no-op in strict-mode index check
An empty `match: {"any": []}` (or `{"except": []}`) is a no-op: `any: []`
matches nothing and `except: []` excludes nothing, regardless of the
field's data type. Because an empty list cannot carry type information it
deserializes as the keyword `AnyVariants::Strings(empty)` variant, which
previously caused strict mode to demand a keyword/uuid index and reject
the request with:
Index required but not found for "<field>" of one of the following
types: [keyword, uuid]
even on a field indexed as integer.
Fix:
- `infer_index_from_any_variants` returns no required index for an empty
variant set.
- `Extractor::update_from_condition` skips a condition whose required
index set is empty, so a no-op condition is never reported as needing
an index (this also covers the FormulaQuery condition path).
This makes the openapi reproduction in test_match_any_empty.py pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
7a8703f166 |
[TQDT] API (#9172)
* [ai] TQDT in the API * [ai] unify new sparse error for TQDT * Rename to `turbo4` * Add `Turbo4` to comments and doc strings. * Also validate named sparse vector creation |
||
|
|
ff4e41ed88 |
Fix empty vector panic (#9070)
* Add test * Validate empty vector name * Update test assertions |
||
|
|
89fa850edd |
fix: validate vector dimensions before WAL write for async upserts (#9058)
* fix: validate vector dimensions before WAL write for async upserts When upserting points with wait=false (the default), dimension mismatches were silently discarded during background processing. The API returned 200 "acknowledged" but the points were never stored, causing silent data loss with no error feedback to the user. This adds an early dimension validation check in do_upsert_points() that runs before the operation is written to WAL. This ensures that dimension errors are returned to the client regardless of the wait parameter, matching the behavior of wait=true. The validation handles all vector types: - Dense single vectors - Multi-dense vectors - Named vectors (dense, multi-dense, sparse) - Sparse vectors are skipped (no fixed dimension) Closes #9039 Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: move vector dimension validation into dedicated module Extract validate_vector_dimensions and helper functions from update.rs into src/common/validate_vectors.rs for better code organization. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: update shard update test for early dimension validation The test expected a shard-level error message, but now dimension mismatches are caught before reaching the shards. Update the assertion to accept either the early validation error or the shard-level error. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: assert actual dimension error message in shard update test Check for the descriptive error ("Vector dimension error: expected dim: 4, got 3") rather than the generic shard failure wrapper. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor Agent <agent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
50e9c7bba7 |
Fix match: {except: []} returning zero results with payload index (#9055)
* Add integration test for `match: {except: []}` with integer index
Regression test for https://github.com/qdrant/qdrant/issues/9050
An empty `except` list (NOT IN []) should always match all points that
have the field, both with and without a payload index. Currently the
integer (and keyword) indexed path incorrectly returns zero results
because:
1. serde deserializes `except: []` as `AnyVariants::Strings([])` (first
variant of the untagged enum)
2. The map index filter_impl returns `iter::empty()` for empty
cross-type variant, instead of matching everything
The test covers:
- except: [] without any index (baseline, currently works)
- except: [] with an integer index (currently broken)
- except: [] with a keyword index (currently broken)
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix `match: {except: []}` returning zero results with payload index
Fixes https://github.com/qdrant/qdrant/issues/9050
Root cause: `except: []` deserializes as `AnyVariants::Strings([])`
due to the untagged serde enum trying `Strings` first. On an integer
index, the filter_impl matched `Except + Strings(empty)` and returned
`iter::empty()` (zero results). The same issue existed symmetrically
on keyword indexes with `Integers(empty)` and on UUID indexes with
`Integers(empty)`.
The fix: when the cross-type variant reaches the Except branch (e.g.
Strings on an integer index, Integers on a keyword/UUID index), return
`None` unconditionally — regardless of whether the set is empty. This
delegates to the fallback condition checker, which already handles
`except: []` correctly by matching all values.
The `estimate_cardinality_impl` functions had the same bug (returning
`CardinalityEstimation::exact(0)` for the empty cross-type case) and
are fixed the same way.
Affected index types: integer, keyword (str), UUID.
Bool index is NOT affected — it already returns `None` for all
Any/Except conditions.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Expand match-except test to cover all index types and cross-type filters
Extends the regression test for #9050 to comprehensively cover:
- Integer, keyword, and UUID field indexes
- Empty except list (the original bug) for each index type
- Non-empty except list with matching types (normal filtering)
- Cross-type except values (e.g. strings on integer index, integers on
keyword/UUID index) — type mismatch should exclude nothing → all match
- Exclude-all: listing every value should return zero results
- All scenarios tested both with and without a payload index
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
ce074b1a70 |
fix(openapi-tests): inject QDRANT_HOST_HEADERS (#9022)
Raw requests already use QDRANT_HOST, but bypass request_with_validation and missed QDRANT_HOST_HEADERS. Without these headers, tests cannot run behind host-based reverse proxies or mocks. |
||
|
|
be82eceb40 |
Fix flaky strict mode collection size tests (#8922)
These tests rely on the collection size stats cache being refreshed to detect that a size limit has been exceeded. Without wait=true, upsert operations are written to WAL and acknowledged immediately without being applied to segments. When the cache refreshes, it reads segment data which may not yet reflect the pending WAL operations, causing the size check to see stale values and not reject the request. Adding wait=true ensures operations are applied to segments before the response returns, so the cache refresh sees the correct sizes. Co-authored-by: Cursor Agent <agent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
e108a11ae3 | [ai] OpenApi tests for TurboQuant (#8845) | ||
|
|
d02ef48f24 |
Dynamic cpu pool (#8790)
* [AI] inptoduce CPU process measurement * use parking_lot + 4 seconds refresh rate * [AI] AdaptiveSearchHandle * fmt * openapi schema * keep Runtime field * fix test * [AI] instead of async semaphore, use 2 runtimes * Adjust usage window to 2 seconds * Address CodeRabbit review comments for dynamic CPU pool - OpenAPI / telemetry: user-facing cpu_cores_used description (2s window, when null). - process_cpu_usage: backoff after procfs errors; serialize Linux unit tests on CACHE. - Docs: decouple runtime thread comments from hardcoded 4× multiplier; name search_runtime in test. - consensus test: replace stale runtime comment. Made-with: Cursor * chore(openapi): regenerate master spec via generate_openapi_models.sh Replace hand-edited cpu_cores_used description with output from schema_generator + merge pipeline so openapi_consistency_check passes. Made-with: Cursor --------- Co-authored-by: Cursor Agent <agent@cursor.com> |
||
|
|
4d435e5734 |
Fix Openapi spec for new vector crud ops (#8779)
* Fix Openapi spec for new vector crud ops * validate spec conformance in tests |
||
|
|
4cb494d3b6 | Fix missing validation on adding named vector (#8776) | ||
|
|
2481fd32ed | Fix panic in validate_iter when two siblings fail validation (#8762) | ||
|
|
3ac7e8409f |
Fix IsEmpty condition on null rebuilt index (#8734)
* [ai] Fix IsEmpty condition on null rebuilt index * [#8734] Alternative fix (#8736) * don't grow mmap * fix iter_falses * Fix is_empty and has_values mix up --------- Co-authored-by: Luis Cossío <luis.cossio@outlook.com> |
||
|
|
f51c334554 |
Fix grpc stop words (#8728)
* Fix stopwords always being lowered in grpc path only * [ai] add integration test, stopwords grpc vs rest |
||
|
|
e160fb9ed4 |
Fix datetime parsing for YYYY-MM-DDTHH:MM format (T separator, no seconds) (#8719)
The documented format `2023-02-08T10:49` was not accepted because the parser only had `%Y-%m-%d %H:%M` (space separator) but was missing `%Y-%m-%dT%H:%M` (T separator). Add the missing format variant and tests. Closes #8718 Made-with: Cursor Co-authored-by: Cursor Agent <agent@cursor.com> |
||
|
|
acfb6503b1 |
crud named vectors (#8605)
* Add empty placeholder vector storage types for named vector CRUD Introduce EmptyDenseVectorStorage and EmptySparseVectorStorage as placeholder storages for newly created named vectors on immutable segments. These report all vectors as deleted, consume no disk space, and are reconstructed from segment config on load via the new VectorStorageType::Empty and SparseVectorStorageType::Empty variants. Key design decisions: - is_on_disk is derived from original user config, not hardcoded - MultiVectorConfig is preserved for multi-vector support - Config mismatch optimizer skips Empty storage to avoid false rebuilds - Quantization delegates normally (handles 0 vectors gracefully) - get_vector includes debug_assert to catch unexpected access Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * [AI] segment-level operations for creating and deleting anmed vectors * [AI] implement named vector creation and deleting in proxy segment * [AI] Step 3: Proxy Segment Handling for Named Vector Operations * [AI] implement for Edge * [AI] implement consensus operations for named vector operations * [AI] refactor VectorNameConfig, remove VectorNameConfigInternal * [AI] handle vector schema inconsistency in raft snapshot recovery * [AI] rest + grpc API * [AI] clippy * [AI] generate openAPI schema * fmt * ci fixes * [AI] fix jwt access test * [AI] nop operation for awaiting of consensus-commited update ops * [AI] move vector name operations into points service * [AI] implement internal api for vector name operations * [AI] change collection-level config along with segment level operation * [AI] vector schema reconceliation instead of error * fmt * missing compile-time option * [AI] integration test * [AI] fix missing JWT tests * [AI] remove NOP * [AI] openapi test * [AI] fix initialization of mutable segment * [AI] more simple integration tests * fmt * [AI] make cluster test a bit harder * [AI] make test less flacky * [AI] rabbit comments * [AI] check params compatibility before writing vector config * [AI] make sure to register vector storages in structure payload index * [AI] vector name validation * lower vector length validation to 200 chars to account for prefix in filename * [AI] proxy segment: prevent stale data leak through optimization * fmt * [AI] filter out removed vectors from proxy response * [AI] handle vector name in proxy * fmt * adjust proxy info based on dropped vectors * [AI] proxy segment: update filters to correct has_vector condition * fmt * clippy * Fix consensus snapshot applicaiton for vector schema --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
b8a78c18eb |
Fix empty sparse vector name validation (#8194)
* Fix empty sparse vector name validation * Reorder validation to check empty sparse name before duplicate name --------- Co-authored-by: leohenon <77656081+lhenon999@users.noreply.github.com> |
||
|
|
b47fa7d1ee | fix: timeout tests (#8591) | ||
|
|
fb704e5d13 |
Feature/strict mode search max batchsize (#8469)
* feat: add search_max_batchsize to strict mode config * added test case for search_max_batchsize * Changes for fixing CI issue dure openapi * Modify check_strict_mode_batch --------- Co-authored-by: Arnaud Gourlay <arnaud.gourlay@gmail.com> |
||
|
|
3230f1d952 |
Implement Per collection metrics for Promethus (#8214)
* Implement Per-Collection Prometheus Metrics
* Update config/config.yaml
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Update tests/per_collection_metrics_test.sh
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Update tests/per_collection_metrics_test.sh
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix ci
* comment
* Update tests/per_collection_metrics_test.sh
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* adress revew
* fix: linter
* refactor: cardinality limit, anonymize fix, cleanup
- Add max_per_collection_metrics config (default 256) to bound
per-collection label cardinality
- Fix anonymize: strip per_collection_responses entirely instead of
leaking hashed collection names
- Move CollectionName into requests_telemetry, remove
telemetry_context.rs
- Add unit tests for cardinality limits
- Add integration test assertions for default mode
- Regenerate OpenAPI spec
- Document why internal gRPC doesn't attach CollectionName
* avoid cloning
* fix: address pr reviews
* fix: linter
* fix: linter
* feat: enforce {name} in actix api
* chore: remove empty line
* feat: add actix pre-commit hook
* feat: potential enforce tonic collection name
* feat: use proc_macro instead
* feat: use collection_name instead of name
* feat: add tonic telemetry tests
* fix: linter
* fix: linter
* chore: remove unneccessary clone
* fix: use collection_name everywhere
* fix: python tests and openapi
* chore: simplify collection_name tests
* actix collection_name enforcign: use relative path in test + avoid grep
* fix: remove macro
* chore: add some comment to telemetry_wrapper
* fix: clippy
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Daniel Boros <dancixx@gmail.com>
Co-authored-by: generall <andrey@vasnetsov.com>
|
||
|
|
356040312b |
Correct calculation of deferred point counts (#8366)
* Don't account for deferred points in some places # Conflicts: # lib/collection/src/shards/local_shard/scroll.rs * Add in QueryContext * Cover more places * Coderabbit review remarks * Properly count amount of deleted deferred points (#8386) * Properly count amount of deleted deferred points * Prevent double-counting of the same point * Remove hints to estimations * Properly handle counts in ProxySegment * Add tests and fix deleted point count issue * Adjusts tests + fix issues * Separte fields for deferred points in telemetry (SegmentInfo) * Remove deferred_points_count() * openapi * Fix test by manually calculating visible points * Adjust ProxySegment test to revertion of SegmentInfo * Throw error if collection was not found in telemetry (e2e Test) * Update lib/segment/src/segment/segment_ops.rs Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com> * Make new fields in API optional * Don't take range if no deferred point exist * Review remarks --------- Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com> |
||
|
|
09c0ad6fb4 |
Fix timeout in test_shard_snapshot_transfer_deferred (#8398)
* Fix timeout in test_shard_snapshot_transfer_deferred * more specific exception net * set client timeout explicitly |
||
|
|
9074b9640b |
Integration test for deferred points (#8365)
* Integration test for deferred points * test facets * rework |
||
|
|
8ab0057566 | DiscoveryQuery -> DiscoverQuery (#8378) | ||
|
|
b14e0be4ed |
Restrict snaphsot recovery to snapshots directory, hide file contents (#8341)
* restrict recovery from snapshot folder * fmt * clippy * Extend test, path must be inside snapshots directory * In release builds, hide detailed tar error message to not leak contents * Change from bad request to forbidden * fix(auth_tests): place snapshot in peer snapshots dir for recover_collection_snapshot The recover from snapshot API now requires file:// paths to be inside the configured snapshots directory. Write the test snapshot into the peer's snapshots directory instead of a temp file so the request is allowed. Made-with: Cursor --------- Co-authored-by: timvisee <tim@visee.me> Co-authored-by: Tim Visée <tim+github@visee.me> Co-authored-by: Cursor Agent <agent@cursor.com> |