Skip to content

Conversation

@joein
Copy link
Member

@joein joein commented Sep 12, 2025

Current generation was done based on:

Local mode issue:
#1070

@netlify
Copy link

netlify bot commented Sep 12, 2025

Deploy Preview for poetic-froyo-8baba7 ready!

Name Link
🔨 Latest commit cd7f811
🔍 Latest deploy log https://app.netlify.com/projects/poetic-froyo-8baba7/deploys/6916f850a7fa490009d4ce7f
😎 Deploy Preview https://deploy-preview-1069--poetic-froyo-8baba7.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Sep 12, 2025

📝 Walkthrough

Walkthrough

This diff adds collection-level JSON metadata and CollectionWarning; introduces Rrf/RrfQuery, AcornSearchParams, BM25/Bm25Config, and Match.text_any; extends ExtendedPointId to accept uuid.UUID and normalizes UUIDs in local flows. Vector representations migrated from flat to dense/multi-dense/sparse structures. New fields added across configs: max_payload_index_count, inline_storage, wal_retain_closed, ascii_folding, initial_state for shard keys. update_filter is threaded through Upsert/UpdateVectors/PointsUpdateOperation and uploaders. Protobufs, generated stubs, bidirectional Grpc↔Rest conversions, uploaders (gRPC/REST), client surfaces (sync/async/remote/local), local persistence, and many tests/fixtures were updated. Several legacy search/recommend/discover paths were removed or consolidated into query-centric APIs.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Areas to focus review on:

  • Protobuf changes and generated stubs (collections_pb2, points_pb2, service/snapshots descriptors and builder-based rewrites).
  • Conversion layer (qdrant_client/conversions/conversion.py and common_types.py): metadata, update_filter, Rrf, AcornSearchParams, Match.text_any, multi-dense/sparse vector mappings, UUID handling.
  • Client/public surfaces and remotes (qdrant_client/qdrant_client.py, async_qdrant_client.py, qdrant_remote.py, async_qdrant_remote.py): signature additions/removals and parameter propagation (metadata, update_filter, consistency, timeout, initial_state).
  • Uploaders (uploader/grpc_uploader.py, uploader/rest_uploader.py): id normalization, update_filter propagation, shard_key_selector conversions, retry/upload semantics.
  • Local implementation and persistence (local/*): UUID normalization, update_filter enforcement, metadata persistence, and removal of legacy search/recommend flows.
  • Tests and fixtures: widespread test updates to QueryRequest/QueryResponse shapes, vector formats, and UUID-related tests.

Possibly related PRs

Suggested reviewers

  • generall
  • coszio
  • timvisee

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title is concise and specific, indicating model updates by removing vectors_count field and updating HTTP/gRPC models.
Description check ✅ Passed Description provides context by referencing related PRs in upstream projects (pydantic_openapi_v3 and qdrant core), and identifies a related local mode issue.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch update-models-1.16

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (13)
qdrant_client/http/api/search_api.py (3)

97-98: Docstring: tighten phrasing (“differently”; “calculated as +1”).

Minor grammar polish for clarity.

-... the score behaves a little different: The integer part ...
+... the score behaves a little differently: The integer part ...
-... is calculated +1 if the point is closer ...
+... is calculated as +1 if the point is closer ...

524-525: Docstring: mirror the same phrasing fix as above.

Keep async/sync docs consistent.

-... behaves a little different: The integer part ...
+... behaves a little differently: The integer part ...
-... is calculated +1 if ...
+... is calculated as +1 if ...

747-748: Docstring: mirror the same phrasing fix.

Consistency with the other two docstrings.

-... behaves a little different: The integer part ...
+... behaves a little differently: The integer part ...
-... is calculated +1 if ...
+... is calculated as +1 if ...
qdrant_client/http/api/distributed_api.py (1)

159-173: Add timeout to remove_peer: looks good; consider documenting units.

Implementation follows existing query param pattern. Add “seconds” note in the docstring for clarity.

qdrant_client/proto/points.proto (2)

544-545: Nit: stray indent on timeout in CountPoints.

Align with surrounding fields.

-   optional uint64 timeout = 6; // If set, overrides global timeout setting for this request. Unit is seconds.
+  optional uint64 timeout = 6; // If set, overrides global timeout setting for this request. Unit is seconds.

1124-1125: Match.text_any — good addition; consider doc note on tokenization.

If tokenization differs from “text”/“phrase”, mention it in API docs.

qdrant_client/http/models/models.py (5)

93-133: BM25 config looks good; consider constraining language type.

Optional: tighten language to accepted values without losing 'none' by using Union[Language, Literal["none"]] instead of bare str. Keeps schema self-documenting while preserving the special-case.

Apply within this block:

-    language: Optional[str] = Field(
+    language: Optional[Union["Language", Literal["none"]]] = Field(
         default=None,
         description="Defines which language to use for text preprocessing. This parameter is used to construct default stopwords filter and stemmer. To disable language-specific processing, set this to `'language': 'none'`. If not specified, English is assumed.",
     )

239-243: Minor doc punctuation fix for CollectionConfig.metadata.

Add a period after “collection” for readability.

-    metadata: Optional["Payload"] = Field(
-        default=None,
-        description="Arbitrary JSON metadata for the collection This can be used to store application-specific information such as creation time, migration data, inference model info, etc.",
-    )
+    metadata: Optional["Payload"] = Field(
+        default=None,
+        description="Arbitrary JSON metadata for the collection. This can be used to store application-specific information such as creation time, migration data, inference model info, etc.",
+    )

596-599: Nit: spacing in description.

“1.Defaults” → “1. Defaults”.

-        description="The midpoint of the decay. Should be between 0 and 1.Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
+        description="The midpoint of the decay. Should be between 0 and 1. Defaults to 0.5. Output will be this value when `|x - target| == scale`.",

1071-1074: Doc variable mismatch: full_scan_threshold vs full_scan_threshold_kb.

The field is full_scan_threshold, but the text references full_scan_threshold_kb. Align to the actual field name.

-        description="Minimal size threshold (in KiloBytes) below which full-scan is preferred over HNSW search. This measures the total size of vectors being queried against. When the maximum estimated amount of points that a condition satisfies is smaller than `full_scan_threshold_kb`, the query planner will use full-scan search instead of HNSW index traversal for better performance. Note: 1Kb = 1 vector of size 256",
+        description="Minimal size threshold (in kilobytes) below which full-scan is preferred over HNSW search. This measures the total size of vectors being queried against. When the maximum estimated amount of points that a condition satisfies is smaller than `full_scan_threshold`, the query planner will use full-scan search instead of HNSW index traversal for better performance. Note: 1 kB ≈ 1 vector of size 256.",

And similarly in HnswConfigDiff.

Also applies to: 1098-1101


3301-3304: Fill in missing description for UpdateVectors.update_filter.

Keep it consistent with PointsList/PointsBatch.

-class UpdateVectors(BaseModel, extra="forbid"):
+class UpdateVectors(BaseModel, extra="forbid"):
     points: List["PointVectors"] = Field(..., description="Points with named vectors")
     shard_key: Optional["ShardKeySelector"] = Field(default=None, description="")
-    update_filter: Optional["Filter"] = Field(default=None, description="")
+    update_filter: Optional["Filter"] = Field(
+        default=None,
+        description="If specified, only points that match this filter will be updated",
+    )
qdrant_client/grpc/collections_pb2.pyi (1)

792-798: Minor doc polish: threshold name and units.

Mirror the HTTP model: reference full_scan_threshold (not _kb) and use “kilobytes”.

-    `full_scan_threshold`, the query planner will use full-scan search instead of HNSW index
-    traversal for better performance.
-    Note: 1Kb = 1 vector of size 256
+    `full_scan_threshold`, the query planner will use full-scan search instead of HNSW index
+    traversal for better performance.
+    Note: 1 kB ≈ 1 vector of size 256.
qdrant_client/grpc/points_pb2.pyi (1)

3398-3420: PointsUpdateOperation.PointStructList.update_filter: consistent with UpsertPoints

Good symmetry for batch upsert. Consider adding a brief doc/example in client docs showing “conditional upsert” with update_filter.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d29466 and ee2ec61.

📒 Files selected for processing (11)
  • qdrant_client/conversions/conversion.py (0 hunks)
  • qdrant_client/grpc/collections_pb2.py (5 hunks)
  • qdrant_client/grpc/collections_pb2.pyi (16 hunks)
  • qdrant_client/grpc/points_pb2.pyi (18 hunks)
  • qdrant_client/http/api/distributed_api.py (4 hunks)
  • qdrant_client/http/api/points_api.py (10 hunks)
  • qdrant_client/http/api/search_api.py (3 hunks)
  • qdrant_client/http/models/models.py (24 hunks)
  • qdrant_client/proto/collections.proto (7 hunks)
  • qdrant_client/proto/points.proto (10 hunks)
  • tests/conversions/fixtures.py (0 hunks)
💤 Files with no reviewable changes (2)
  • qdrant_client/conversions/conversion.py
  • tests/conversions/fixtures.py
🧰 Additional context used
🧬 Code graph analysis (6)
qdrant_client/http/api/distributed_api.py (1)
qdrant_client/http/models/models.py (1)
  • InlineResponse200 (1200-1204)
qdrant_client/grpc/collections_pb2.py (2)
qdrant_client/grpc/collections_pb2.pyi (6)
  • CreateCollection (1404-1529)
  • MetadataEntry (1407-1422)
  • MetadataEntry (1536-1551)
  • MetadataEntry (1763-1778)
  • UpdateCollection (1533-1622)
  • CollectionConfig (1760-1824)
qdrant_client/http/models/models.py (3)
  • CreateCollection (469-520)
  • UpdateCollection (3245-3276)
  • CollectionConfig (222-242)
qdrant_client/http/models/models.py (2)
qdrant_client/grpc/points_pb2.pyi (20)
  • b (3343-3344)
  • options (318-319)
  • options (360-361)
  • options (403-404)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • Rrf (2753-2768)
  • rrf (2811-2812)
qdrant_client/grpc/collections_pb2.pyi (7)
  • stopwords (1972-1973)
  • metadata (1478-1479)
  • metadata (1589-1590)
  • metadata (1806-1807)
  • languages (1935-1936)
  • custom (1938-1939)
  • WalConfigDiff (878-904)
qdrant_client/grpc/collections_pb2.pyi (1)
qdrant_client/grpc/json_with_int_pb2.pyi (2)
  • value (61-61)
  • Value (84-131)
qdrant_client/grpc/points_pb2.pyi (1)
qdrant_client/http/models/models.py (2)
  • Fusion (913-922)
  • Rrf (2429-2434)
qdrant_client/http/api/points_api.py (2)
qdrant_client/grpc/points_pb2.pyi (1)
  • ReadConsistency (228-245)
qdrant_client/http/models/models.py (2)
  • FacetRequest (793-803)
  • InlineResponse20021 (1296-1300)
🪛 Buf (1.55.1)
qdrant_client/proto/collections.proto

6-6: import "json_with_int.proto": file does not exist

(COMPILE)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
🔇 Additional comments (51)
qdrant_client/http/api/distributed_api.py (2)

266-276: Async remove_peer updated with timeout: LGTM.

Signature and forwarding are consistent.


343-353: Sync remove_peer updated with timeout: LGTM.

Signature and forwarding are consistent.

qdrant_client/proto/collections.proto (7)

175-181: HNSW full_scan_threshold doc: clearer semantics — nice.

Good improvement to explain KB-based threshold and planner behavior.


219-220: WAL config: wal_retain_closed addition — OK.

Backwards compatible field number; no collisions.


408-409: init_from_collection marked Deprecated — OK.

Doc-only; no wire change.


413-414: Collection metadata map added — confirm Value source and merge semantics.

Wire looks fine; ensure server implements merge behavior as described.


426-427: UpdateCollection.metadata — confirm “merge” behavior is deep vs. replace.

Proto map updates typically replace entry values; clarify expected merge semantics for nested objects.


466-467: CollectionConfig.metadata — OK; aligns with HTTP models.

Just ensure json_with_int.Value is available (see blocking comment above).


6-7: Resolved — json_with_int.proto is vendored and generated Python files are present.
Found at qdrant_client/proto/json_with_int.proto and qdrant_client/grpc/json_with_int_pb2.py, qdrant_client/grpc/json_with_int_pb2.pyi.

qdrant_client/proto/points.proto (7)

138-139: update_filter on UpsertPoints — specify exact semantics.

Clarify authoritative behavior when a point matches filter vs. doesn’t (partial update vs. insert) and how conflicts across shards are handled.


166-167: update_filter on UpdatePointVectors — consistent with UpsertPoints?

Confirm identical filtering semantics here to avoid surprises.


642-642: Decay midpoint doc tweak — nice clarity.

Improves understanding without wire changes.


672-676: New Rrf message — good extension for parameterized fusion.

Backwards compatible and complements Fusion.RRF.


688-689: Query.rrf alongside Fusion — ensure no ambiguity.

Since both “fusion” (enum) and “rrf” (message) exist in the oneof, document precedence/usage guidelines in public docs.


1036-1037: FacetResponse.usage — OK.

Matches other responses; non-breaking.


8-9: Not blocking — dependency present

Found qdrant_client/proto/json_with_int.proto and generated modules qdrant_client/grpc/json_with_int_pb2.py (and .pyi).

qdrant_client/http/api/points_api.py (6)

262-278: facet builder: added consistency + preserved timeout — OK.

Query params set consistently.


564-576: Async count_points signature updated — OK; same positional risk applies.

Encourage keyword use in release notes.


633-643: Async facet signature updated — OK.

Forwarding is correct.


871-882: Sync facet signature updated — OK.

Matches builder and async.


126-140: Added consistency to count_points — breaks positional-arg callers; verify and add compatibility.

Inserting the new "consistency" parameter before "timeout" changes the positional order so existing callers using positional args may pass their timeout into consistency; provide a compatibility plan (options: accept both orders via a thin wrapper, require keyword-only usage with a deprecation window, or move the new param to the end). Automated scans failed to conclusively find positional callers — manual verification required.


802-814: Sync count_points signature updated — approve but positional-arg compatibility risk

Change is fine, but callers that pass >1 positional argument (i.e., rely on positional consistency/timeout/count_request) will break. Automated repo-wide checks did not complete; manually verify there are no such calls outside qdrant_client/http/api (qdrant_client/http/api/points_api.py — def count_points at ~lines 799–804), or make the new params keyword-only.

qdrant_client/grpc/collections_pb2.py (2)

150-159: MetadataEntry nested types registered — LGTM.

Generated surfaces align with proto map additions.


16-17: No action needed — json_with_int_pb2 present
qdrant_client/grpc/json_with_int_pb2.py (and json_with_int_pb2.pyi) exist; the import will succeed.

qdrant_client/http/models/models.py (13)

205-206: Telemetry peer metadata addition — LGTM.

Shape matches intent; no concerns.


253-254: CollectionConfigTelemetry.metadata — LGTM.

Consistent with CollectionConfig.


750-761: Document model/options update — LGTM.

DocumentOptions union with Bm25Config is a nice improvement.


848-851: Feature flag appendable_quantization — LGTM.


1562-1568: MatchTextAny addition — LGTM.

New variant is correctly wired into Match.

Also applies to: 3599-3605


1926-1932: PeerMetadata and ClusterTelemetry.peer_metadata — LGTM.

Also applies to: 200-207


1971-1974: update_filter on upsert APIs — LGTM.

Descriptions are clear and match gRPC semantics.

Also applies to: 1980-1983


2429-2439: RRF models and query variant — LGTM.

Types look correct; union updated.

Also applies to: 3667-3676


3067-3073: StopwordsSet typing — LGTM.

Enum-typed languages plus custom list is a good balance.


3273-3276: UpdateCollection.metadata — LGTM.

Merge semantics are clearly documented.


3577-3580: DocumentOptions alias — LGTM.

Union placement works with forward-ref usage in Document.


3671-3676: Query union wiring — LGTM.


3491-3494: Confirm server default/semantics for WAL retention.

WalConfig.wal_retain_closed defaults to 1 in the HTTP model while gRPC exposes it as an optional Diff — verify the server-side default and align the HTTP model (set to server default or remove the hard default so Diff semantics are preserved) to avoid API drift.
Locations: qdrant_client/http/models/models.py (wal_retain_closed default=1 at ~3491–3493; Diff default=None at ~3499–3502), qdrant_client/proto/collections.proto (~line 219).

qdrant_client/grpc/collections_pb2.pyi (5)

883-905: WAL retention diff field — LGTM.


1407-1501: CreateCollection.metadata map — LGTM.

Typing via json_with_int_pb2.Value is appropriate; ClearField includes “metadata”.


1536-1606: UpdateCollection.metadata map — LGTM.

Merge semantics reflected in the docstring.


1763-1821: CollectionConfig.metadata map — LGTM.


2191-2257: gRPC still exposes vectors_count — confirm intended parity with HTTP

HTTP removed vectors_count, but gRPC CollectionInfo and conversion/test code still define/use it. Key locations: qdrant_client/grpc/collections_pb2.pyi, qdrant_client/conversions/conversion.py (checks/sets vectors_count), qdrant_client/local/local_collection.py (vectors_count=None), tests/conversions/fixtures.py, tests/test_async_qdrant_client.py (commented assertions). If removal across surfaces is intended: remove/adjust the gRPC field and update conversion logic and tests. Otherwise document the deliberate divergence.

qdrant_client/grpc/points_pb2.pyi (9)

157-166: Doc tweak for Fusion.RRF is fine

Clarifies default-params intent; consistent with HTTP enum.


2672-2672: DecayParamsExpression.midpoint doc tightened

Good clarification on valid range.


2753-2771: New Rrf message: API surface looks sound

Public alias (global___Rrf) present; field number and oneof helpers generated correctly.


4411-4428: FacetResponse.usage added: aligns with other responses

Brings facets in line with Search/Query/Count responses. Backward compatible (optional oneof).


4730-4755: Document oneof precedence for Match.text_any vs text/phrase

  • Clarify precedence/behavior when clients set text_any vs text or phrase — oneof semantics mean only the last-set field is retained; add a short example showing which field “wins” and update the generated .pyi docstrings.
  • Verify and update qdrant_client/grpc/points_pb2.pyi lines 4730–4755 (also applies to 4767–4772); I could not confirm the snippet programmatically (rg returned no matches), so please confirm the file/lines and apply the doc change.

651-651: Add/verify client tests for UpsertPoints.update_filter (round‑trip)

Wiring looks present — proto, generated pb2/pyi expose update_filter and Upsert is exercised from qdrant_client/qdrant_remote.py, async_qdrant_remote.py and qdrant_client/uploader/grpc_uploader.py.

Add/verify tests that:

  • upsert with update_filter matching existing points updates only those points;
  • upsert with update_filter matching no points results in inserts.

792-792: update_filter added to UpdatePointVectors — OK, documented as update-only

Type/oneof plumbing is correct; proto (qdrant_client/proto/points.proto — UpdateVectors) and generated stub (qdrant_client/grpc/points_pb2.pyi — UpdatePointVectors) state "If specified, only points that match this filter will be updated" (no implicit insert).


3556-3578: PointsUpdateOperation.UpdateVectors.update_filter — wiring verified; add test

Proto, generated stubs, and conversion wiring include update_filter; add a unit/integration test asserting points that do NOT match update_filter remain untouched. Reuse fixture update_vectors_operation in tests/conversions/fixtures.py:1317–1321; see qdrant_client/proto/points.proto:824–827, qdrant_client/grpc/points_pb2.pyi:3564–3571, and qdrant_client/conversions/conversion.py:4471–4474.


2784-2830: fusion and rrf share the Query "variant" oneof — confirm intent

qdrant_client/grpc/points_pb2.pyi shows Query's WhichOneof lists both "fusion" and "rrf" (setting one clears the other); if intentional, document the exclusivity and add a unit test that only one is set; if not, remove rrf from the oneof or update the conversion path (qdrant_client/conversions/conversion.py — around convert_fusion -> grpc.Query(...)) and adjust affected tests (e.g., tests/conversions/fixtures.py, tests/* referencing FusionQuery).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
qdrant_client/embed/_inspection_cache.py (4)

1452-1521: Polish BM25 schema: add missing titles and clarify descriptions.
Add titles for tokenizer/stemmer and make language note less confusing.

Apply:

 "tokenizer": {
   "anyOf": [{"$ref": "#/$defs/TokenizerType"}, {"type": "null"}],
   "default": None,
-  "description": "Configuration of the local bm25 models.",
+  "description": "Tokenizer type used for BM25 preprocessing.",
+  "title": "Tokenizer",
 },
 ...
 "language": {
   "anyOf": [{"type": "string"}, {"type": "null"}],
   "default": None,
-  "description": "Defines which language to use for text preprocessing. This parameter is used to construct default stopwords filter and stemmer. To disable language-specific processing, set this to `'language': 'none'`. If not specified, English is assumed.",
+  "description": "Defines which language to use for text preprocessing. Used to construct default stopwords and stemmer. To disable language-specific processing, set to 'none'. If not specified, English is assumed.",
   "title": "Language",
 },
 ...
 "stemmer": {
   "anyOf": [{"$ref": "#/$defs/SnowballParams"}, {"type": "null"}],
   "default": None,
   "description": "Configuration of the stemmer. Processes tokens to their root form. Default: initialized Snowball stemmer for specified `language` or English if not specified.",
+  "title": "Stemmer",
 },

444-446: Fix punctuation in midpoint description.
Missing space after period.

- "description": "The midpoint of the decay. Should be between 0 and 1.Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
+ "description": "The midpoint of the decay. Should be between 0 and 1. Defaults to 0.5. Output will be this value when `|x - target| == scale`.",

1953-1955: Normalize units for clarity (KB vs Kb).
Avoid “Kb” (kilobits) and mixed casing.

- "description": "Minimal size threshold (in KiloBytes) below which full-scan is preferred over HNSW search. This measures the total size of vectors being queried against. When the maximum estimated amount of points that a condition satisfies is smaller than `full_scan_threshold_kb`, the query planner will use full-scan search instead of HNSW index traversal for better performance. Note: 1Kb = 1 vector of size 256",
+ "description": "Minimal data-size threshold (in KB) below which full-scan is preferred over HNSW search. This measures the total size of vectors being queried against. When the maximum estimated amount of points that a condition satisfies is smaller than `full_scan_threshold_kb`, the planner will use full-scan instead of HNSW traversal for better performance. Note: 1 KB ≈ 1 vector of size 256.",

4233-4237: Clarify update_filter semantics; fill missing description in UpdateVectors.
Keep wording consistent and accurate (UpdateVectors can’t insert).

 # PointsBatch.update_filter
- "description": "If specified, only points that match this filter will be updated, others will be inserted",
+ "description": "If specified, only points that match this filter will be updated; others will be inserted.",

 # PointsList.update_filter
- "description": "If specified, only points that match this filter will be updated, others will be inserted",
+ "description": "If specified, only points that match this filter will be updated; others will be inserted.",

 # UpdateVectors.update_filter (missing text)
- "description": "",
+ "description": "If specified, only points that match this filter will be updated; non-matching points are left unchanged.",

Also applies to: 4266-4270, 4306-4310

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee2ec61 and ffc3e84.

📒 Files selected for processing (1)
  • qdrant_client/embed/_inspection_cache.py (20 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
  • GitHub Check: Python 3.12.x on ubuntu-latest test
  • GitHub Check: Python 3.13.x on ubuntu-latest test
  • GitHub Check: Python 3.11.x on ubuntu-latest test
  • GitHub Check: Python 3.10.x on ubuntu-latest test
  • GitHub Check: Python 3.9.x on ubuntu-latest test
🔇 Additional comments (7)
qdrant_client/embed/_inspection_cache.py (7)

9-9: LGTM: added Bm25Config to CACHE_STR_PATH.


83-83: LGTM: MatchTextAny wired through (cache path, FieldCondition, schema).

Also applies to: 590-592, 1061-1074


209-211: LGTM: RRF (Rrf/RrfQuery) added and allowed in query unions.

Also applies to: 3435-3436, 3558-3578, 3665-3666


1537-1541: LGTM: Document.options now accepts Bm25Config.


2385-2390: LGTM: WAL retention knob (wal_retain_closed).


1599-1719: LGTM: Language/Stopwords/Tokenizer defs added.
Choice to keep Bm25Config.language as free string (to allow 'none') makes sense alongside enumerated Language for stopwords.


3035-3038: LGTM: Fusion description updated to include RRF.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
qdrant_client/conversions/conversion.py (1)

1-1: Fix unsafe HasField on metadata and verify RRF conversion

  • qdrant_client/conversions/conversion.py:267 — Replace model.HasField("metadata") with has_field(model, "metadata") (or check len(model.metadata)) — HasField on map fields is invalid and will raise at runtime.
  • Sanity-check RRF handling: ensure Query rrf variant conversion reads the rrf message (val.k) rather than using model.k; tests reference grpc.Query(rrf=grpc.Rrf(...)) in tests/conversions/fixtures.py:1509–1511.
qdrant_client/async_qdrant_remote.py (2)

1728-1739: REST path: accept grpc.PointVectors too.

Normalize inputs to REST model to avoid serialization errors.

         else:
+            # Normalize gRPC PointVectors to REST model if provided by caller
+            if isinstance(points, list):
+                points = [
+                    (GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
+                    for p in points
+                ]
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
             return (

1694-1711: Change public client signatures: use Optional[types.Filter] for update_filter

qdrant_client/async_qdrant_client.py:1524, 1599 and qdrant_client/qdrant_client.py:1572, 1653 currently annotate update_filter as Optional[types.Payload]; replace with Optional[types.Filter], update any public stubs/docs, and run type checks.

🧹 Nitpick comments (3)
qdrant_client/conversions/conversion.py (1)

2167-2173: Minor: Incorrect HasField usage condition.

if model.HasField("index") is not None is always True. Use the boolean directly to avoid converting unset defaults.

Apply:

-                if model.HasField("index") is not None
+                if model.HasField("index")
qdrant_client/qdrant_remote.py (1)

1962-1962: Fix assertion message copy-paste.

Should reference UpdateVectors, not Upsert.

-            assert grpc_result is not None, "Upsert returned None result"
+            assert grpc_result is not None, "Update vectors returned None result"
qdrant_client/async_qdrant_remote.py (1)

1725-1725: Fix assertion message copy-paste.

Should reference UpdateVectors.

-            assert grpc_result is not None, "Upsert returned None result"
+            assert grpc_result is not None, "Update vectors returned None result"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffc3e84 and 7dd66fe.

📒 Files selected for processing (6)
  • qdrant_client/async_qdrant_client.py (15 hunks)
  • qdrant_client/async_qdrant_remote.py (17 hunks)
  • qdrant_client/conversions/conversion.py (10 hunks)
  • qdrant_client/qdrant_client.py (15 hunks)
  • qdrant_client/qdrant_remote.py (15 hunks)
  • tests/conversions/fixtures.py (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
qdrant_client/qdrant_remote.py (3)
qdrant_client/http/models/models.py (2)
  • Filter (877-887)
  • UpdateVectors (3300-3303)
qdrant_client/grpc/points_pb2.pyi (3)
  • models (5151-5151)
  • UpsertPoints (638-686)
  • UpdatePointVectors (784-828)
qdrant_client/conversions/conversion.py (6)
  • RestToGrpc (2491-4791)
  • convert_filter (166-182)
  • convert_filter (2493-2513)
  • GrpcToRest (140-2481)
  • convert_payload (581-582)
  • convert_payload (2795-2796)
qdrant_client/async_qdrant_remote.py (6)
qdrant_client/http/models/models.py (2)
  • Filter (877-887)
  • UpdateVectors (3300-3303)
qdrant_client/conversions/conversion.py (8)
  • RestToGrpc (2491-4791)
  • convert_filter (166-182)
  • convert_filter (2493-2513)
  • GrpcToRest (140-2481)
  • convert_update_result (465-469)
  • convert_update_result (2677-2681)
  • convert_payload (581-582)
  • convert_payload (2795-2796)
qdrant_client/http/api/points_api.py (4)
  • upsert_points (746-761)
  • upsert_points (984-999)
  • update_vectors (729-744)
  • update_vectors (967-982)
qdrant_client/async_qdrant_client.py (1)
  • update_vectors (1592-1648)
qdrant_client/qdrant_client.py (1)
  • update_vectors (1646-1704)
qdrant_client/qdrant_remote.py (1)
  • update_vectors (1929-1977)
tests/conversions/fixtures.py (4)
qdrant_client/grpc/points_pb2.pyi (16)
  • Match (4718-4771)
  • FieldCondition (4656-4714)
  • match (4671-4672)
  • Condition (4537-4574)
  • field (4548-4548)
  • params (1461-1462)
  • params (1617-1618)
  • params (1877-1878)
  • params (2025-2026)
  • params (2199-2200)
  • params (2856-2857)
  • params (2928-2929)
  • params (3069-3070)
  • Query (2772-2829)
  • rrf (2811-2812)
  • Rrf (2753-2768)
qdrant_client/http/models/models.py (3)
  • FieldCondition (853-874)
  • CollectionConfig (222-242)
  • Rrf (2429-2434)
qdrant_client/grpc/collections_pb2.pyi (20)
  • metadata (1478-1479)
  • metadata (1589-1590)
  • metadata (1806-1807)
  • CollectionConfig (1760-1824)
  • params (492-492)
  • params (513-513)
  • params (1571-1572)
  • params (1788-1789)
  • params (2170-2171)
  • hnsw_config (352-353)
  • hnsw_config (397-398)
  • hnsw_config (1443-1444)
  • hnsw_config (1574-1575)
  • hnsw_config (1791-1792)
  • optimizer_config (1794-1795)
  • wal_config (1446-1447)
  • wal_config (1797-1798)
  • strict_mode_config (1475-1476)
  • strict_mode_config (1586-1587)
  • strict_mode_config (1803-1804)
qdrant_client/grpc/json_with_int_pb2.pyi (1)
  • Value (84-131)
qdrant_client/conversions/conversion.py (3)
qdrant_client/grpc/collections_pb2.pyi (8)
  • metadata (1478-1479)
  • metadata (1589-1590)
  • metadata (1806-1807)
  • shard_key (2472-2473)
  • shard_key (2502-2503)
  • shard_key (2556-2556)
  • shard_key (2755-2756)
  • shard_key (2786-2787)
qdrant_client/grpc/points_pb2.pyi (55)
  • HasField (243-243)
  • HasField (269-269)
  • HasField (307-307)
  • HasField (348-348)
  • HasField (369-369)
  • HasField (391-391)
  • HasField (412-412)
  • HasField (467-467)
  • HasField (514-514)
  • HasField (611-611)
  • HasField (677-677)
  • HasField (720-720)
  • HasField (771-771)
  • HasField (819-819)
  • HasField (849-849)
  • HasField (889-889)
  • rrf (2811-2812)
  • Rrf (2753-2768)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • upsert (3659-3659)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • shard_key (3842-3843)
  • shard_key (4206-4207)
  • shard_key_selector (662-663)
  • shard_key_selector (709-710)
  • shard_key_selector (756-757)
  • shard_key_selector (804-805)
  • shard_key_selector (877-878)
  • shard_key_selector (941-942)
  • shard_key_selector (994-995)
  • shard_key_selector (1038-1039)
  • shard_key_selector (1478-1479)
  • shard_key_selector (1639-1640)
  • shard_key_selector (1771-1772)
  • shard_key_selector (1821-1822)
  • shard_key_selector (1905-1906)
  • shard_key_selector (2058-2059)
  • shard_key_selector (2217-2218)
  • shard_key_selector (2309-2310)
  • UpdateVectors (3551-3578)
  • Match (4718-4771)
  • Query (2772-2829)
  • PointStructList (3393-3419)
qdrant_client/http/models/models.py (6)
  • MatchTextAny (1562-1567)
  • RrfQuery (2437-2438)
  • Rrf (2429-2434)
  • UpsertOperation (3310-3311)
  • PointsList (1977-1983)
  • UpdateVectors (3300-3303)
🪛 GitHub Actions: Integration tests
qdrant_client/conversions/conversion.py

[error] 267-267: HasField() used on non-presence-enabled field 'metadata' causing ValueError: Field qdrant.CollectionConfig.metadata does not have presence.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
🔇 Additional comments (18)
tests/conversions/fixtures.py (4)

38-38: Additions for Match.text_any look correct and cover filter wiring.

This mirrors the proto’s Match.text_any oneof and is exercised via FieldCondition/Condition and Filter. LGTM.

Also applies to: 52-52, 129-129, 150-151


369-369: Collection metadata fixture is valid.

Using grpc.Value(integer_value=3) matches the new map<string, Value> metadata; extra config instance is helpful for round-trip tests. LGTM.

Also applies to: 379-386


1509-1511: RRF query fixtures: good coverage (k set, default, explicit None).

These will surface conversion bugs quickly. LGTM.

Also applies to: 1738-1741


1571-1571: Include collection_config_w_metadata in fixtures registry.

Nice—ensures REST<->gRPC conversion paths see metadata. LGTM.

qdrant_client/conversions/conversion.py (7)

679-681: text_any support is correct.

Maps Match.text_any → rest.MatchTextAny. LGTM.


1979-1987: Propagating update_filter for Upsert is correct.

Matches new proto semantics. LGTM.


2088-2096: Propagating update_filter for UpdateVectors is correct.

Round-trip symmetry is maintained later in RestToGrpc. LGTM.


2869-2870: REST→gRPC metadata conversion looks right.

No presence check; passes through only when provided. LGTM.


3045-3046: REST MatchTextAny → gRPC text_any mapping is correct.

Covers the new oneof case. LGTM.


4382-4392: REST→gRPC Upsert update_filter wiring is correct.

Field is optional and handled safely. LGTM.


4497-4509: REST→gRPC UpdateVectors update_filter wiring is correct.

Matches proto UpdateVectors.update_filter. LGTM.

qdrant_client/qdrant_client.py (3)

2258-2258: Metadata support for update_collection looks correct.

Signature + forwarding are consistent with docstring.

Also applies to: 2299-2300


2342-2342: Create-collection metadata passthrough LGTM.

Parameter order and forwarding are consistent across clients.

Also applies to: 2412-2413


2435-2435: Recreate-collection metadata passthrough LGTM.

Matches update/create; deprecation notice preserved.

Also applies to: 2512-2513

qdrant_client/async_qdrant_client.py (3)

2184-2184: Async update_collection metadata passthrough LGTM.

Matches sync surface and forwards correctly.

Also applies to: 2222-2223


2264-2264: Async create_collection metadata passthrough LGTM.

Consistent and non-breaking.

Also applies to: 2333-2334


2354-2354: Async recreate_collection metadata passthrough LGTM.

Consistent with other methods.

Also applies to: 2429-2430

qdrant_client/qdrant_remote.py (1)

1847-1847: API surface addition looks good.

update_filter on upsert/update_vectors is consistently threaded across gRPC/REST code paths.

Also applies to: 1936-1936

Comment on lines 2400 to 2402
if isinstance(metadata, models.Payload):
metadata = RestToGrpc.convert_payload(metadata)
return (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix isinstance check for metadata; always convert for gRPC.

Same issue as sync client: convert whenever metadata is provided.

-            if isinstance(metadata, models.Payload):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)
...
-                        timeout=timeout,
-                        metadata=metadata,
+                        timeout=timeout,
+                        metadata=metadata,

Also applies to: 2414-2415

🤖 Prompt for AI Agents
In qdrant_client/async_qdrant_remote.py around lines 2400-2402 and 2414-2415,
the isinstance(metadata, models.Payload) guard prevents converting provided
metadata for gRPC in many cases; change the logic to always convert metadata
when it is not None (or truthy) rather than only when it's an instance of
models.Payload — call RestToGrpc.convert_payload(metadata) unconditionally when
metadata is present and return that converted value (or pass it into the gRPC
payload) so gRPC always receives the converted payload.

Comment on lines +2515 to 2576
metadata = RestToGrpc.convert_payload(metadata)
create_collection = grpc.CreateCollection(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same metadata conversion bug in create_collection (gRPC path).

Unconditionally convert here too.

-            if isinstance(metadata, models.Payload):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)

Also applies to: 2532-2533

🤖 Prompt for AI Agents
In qdrant_client/async_qdrant_remote.py around lines 2515-2516 (and similarly at
2532-2533), the create_collection gRPC path is missing an unconditional
conversion of the metadata payload; call RestToGrpc.convert_payload(metadata)
and assign the result back to metadata before building the grpc.CreateCollection
object so the metadata is always converted for gRPC, and apply the same change
at the second occurrence (lines 2532-2533).

Comment on lines +1965 to 2046
if isinstance(update_filter, grpc.Filter):
update_filter = GrpcToRest.convert_filter(model=update_filter)

return self.openapi_client.points_api.update_vectors(
collection_name=collection_name,
wait=wait,
update_vectors=models.UpdateVectors(
points=points,
shard_key=shard_key_selector,
update_filter=update_filter,
),
ordering=ordering,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

REST path: accept grpc.PointVectors too.

If callers pass gRPC point vectors, current REST path will fail JSON encoding. Convert when needed.

         else:
+            # Normalize gRPC PointVectors to REST model if provided by caller
+            if isinstance(points, list):
+                points = [
+                    (GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
+                    for p in points
+                ]
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
 
             return self.openapi_client.points_api.update_vectors(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if isinstance(update_filter, grpc.Filter):
update_filter = GrpcToRest.convert_filter(model=update_filter)
return self.openapi_client.points_api.update_vectors(
collection_name=collection_name,
wait=wait,
update_vectors=models.UpdateVectors(
points=points,
shard_key=shard_key_selector,
update_filter=update_filter,
),
ordering=ordering,
# Normalize gRPC PointVectors to REST model if provided by caller
if isinstance(points, list):
points = [
(GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
for p in points
]
if isinstance(update_filter, grpc.Filter):
update_filter = GrpcToRest.convert_filter(model=update_filter)
return self.openapi_client.points_api.update_vectors(
collection_name=collection_name,
wait=wait,
update_vectors=models.UpdateVectors(
points=points,
shard_key=shard_key_selector,
update_filter=update_filter,
),
ordering=ordering,
🤖 Prompt for AI Agents
In qdrant_client/qdrant_remote.py around lines 1965 to 1976, the REST
update_vectors call will fail if callers pass gRPC PointVectors because they are
not JSON-serializable; detect when points are gRPC types (single
grpc.PointVectors or an iterable of grpc.PointVectors) and convert them to
REST-compatible structures before constructing models.UpdateVectors (use the
existing GrpcToRest conversion helpers — e.g. GrpcToRest.convert_point_vectors
or convert_point for each element). Perform the conversion in-place just before
calling openapi_client.points_api.update_vectors so update_vectors.points is
always a REST-friendly object. Ensure you handle both single and list inputs and
preserve shard_key_selector, update_filter conversion logic unchanged.

Comment on lines 2790 to 2792
if isinstance(metadata, models.Payload):
metadata = RestToGrpc.convert_payload(metadata)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same metadata conversion bug in create_collection (gRPC path).

Mirror the unconditional conversion here as well.

-            if isinstance(metadata, models.Payload):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)

Also applies to: 2809-2809

🤖 Prompt for AI Agents
In qdrant_client/qdrant_remote.py around lines 2790-2792 (and also at 2809), the
metadata conversion is guarded by an isinstance(models.Payload) check causing
the gRPC create_collection path to skip conversion; remove the isinstance check
and unconditionally call RestToGrpc.convert_payload(metadata) for those
occurrences so metadata is always converted to the gRPC payload format.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
qdrant_client/qdrant_remote.py (2)

2695-2706: Convert gRPC metadata to REST JSON for update_collection (REST path)

If callers pass gRPC map containers, OpenAPI client will choke on non-JSON values. Normalize to REST JSON before request.

         result: Optional[bool] = self.http.collections_api.update_collection(
             collection_name,
             update_collection=models.UpdateCollection(
                 optimizers_config=optimizers_config,
                 params=collection_params,
                 vectors=vectors_config,
                 hnsw_config=hnsw_config,
                 quantization_config=quantization_config,
                 sparse_vectors=sparse_vectors_config,
                 strict_mode_config=strict_mode_config,
-                metadata=metadata,
+                metadata=GrpcToRest.convert_payload(metadata) if metadata is not None and not isinstance(metadata, Mapping) else metadata,
             ),
             timeout=timeout,
         ).result

2828-2843: Convert gRPC metadata to REST JSON for create_collection (REST path)

Mirror the normalization when building the CreateCollection request.

-        create_collection_request = models.CreateCollection(
+        create_collection_request = models.CreateCollection(
             vectors=vectors_config,
             shard_number=shard_number,
             replication_factor=replication_factor,
             write_consistency_factor=write_consistency_factor,
             on_disk_payload=on_disk_payload,
             hnsw_config=hnsw_config,
             optimizers_config=optimizers_config,
             wal_config=wal_config,
             quantization_config=quantization_config,
             init_from=init_from,
             sparse_vectors=sparse_vectors_config,
             sharding_method=sharding_method,
             strict_mode_config=strict_mode_config,
-            metadata=metadata,
+            metadata=GrpcToRest.convert_payload(metadata) if metadata is not None and not isinstance(metadata, Mapping) else metadata,
         )
🧹 Nitpick comments (8)
tests/conversions/fixtures.py (1)

38-38: Good coverage for text_any; optionally include base Match fixture

Adding Match(text_any=...) and wiring it through FieldCondition/Condition/filter is solid. Optionally, also add match_text_any into the "Match" fixtures list to hit convert_match directly without going through FieldCondition.

Also applies to: 52-52, 129-151

qdrant_client/conversions/conversion.py (2)

2869-2870: Nit: avoid passing None into map fields (constructor)

While passing None usually no-ops, prefer skipping the arg to avoid surprises with map fields in some protobuf versions.

Option:

-        return grpc.CollectionConfig(
+        kwargs = dict(
             params=cls.convert_collection_params(model.params),
             hnsw_config=cls.convert_hnsw_config(model.hnsw_config),
             optimizer_config=cls.convert_optimizers_config(model.optimizer_config),
             wal_config=cls.convert_wal_config(model.wal_config),
             quantization_config=(
                 cls.convert_quantization_config(model.quantization_config)
                 if model.quantization_config is not None
                 else None
             ),
             strict_mode_config=(
                 cls.convert_strict_mode_config_output(model.strict_mode_config)
                 if model.strict_mode_config is not None
                 else None
             ),
-            metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
-        )
+        )
+        if model.metadata is not None:
+            kwargs["metadata"] = cls.convert_payload(model.metadata)
+        return grpc.CollectionConfig(**kwargs)

3644-3646: LGTM with minor robustness: RRF (REST -> gRPC)

Works as-is. Optionally avoid setting k when None.

Suggested tweak:

-            return grpc.Query(rrf=grpc.Rrf(k=model.rrf.k))
+            return grpc.Query(rrf=grpc.Rrf(k=model.rrf.k if model.rrf.k is not None else None))
qdrant_client/async_qdrant_client.py (1)

2354-2355: Recreate-collection metadata support is consistent

Docs and forwarding look fine. Minor nit: “Arbitrary JSON” vs “JSON-like” phrasing differs from update/create; unify wording if you re-generate docs.

-            metadata: Arbitrary JSON metadata for the collection
+            metadata: Arbitrary JSON-like metadata for the collection

Also applies to: 2401-2401, 2428-2429

qdrant_client/async_qdrant_remote.py (3)

1701-1702: Fix assert message; optionally harden REST branch conversions

  • The assert message mentions “Upsert” in update_vectors; fix the wording.
  • Optional: in REST path, convert grpc.PointVectors to REST models (parity with gRPC path) to avoid JSON-serialization issues if callers pass gRPC objects.
-            assert grpc_result is not None, "Upsert returned None result"
+            assert grpc_result is not None, "UpdateVectors returned None result"
         else:
+            # Optional hardening: accept grpc.PointVectors when prefer_grpc=False
+            points = [
+                (GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
+                for p in points
+            ]
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
             return (
                 await self.openapi_client.points_api.update_vectors(
                     collection_name=collection_name,
                     wait=wait,
                     update_vectors=models.UpdateVectors(
-                        points=points, shard_key=shard_key_selector, update_filter=update_filter
+                        points=points, shard_key=shard_key_selector, update_filter=update_filter
                     ),
                     ordering=ordering,
                 )
             ).result

Also applies to: 1711-1712, 1720-1721, 1725-1725, 1728-1739


2378-2379: Convert metadata to REST payload in REST path (when given gRPC map)

If callers pass a gRPC map container, the REST client receives non-JSON objects. Convert to REST payload before building the request.

         if isinstance(strict_mode_config, models.StrictModeConfig):
             strict_mode_config = RestToGrpc.convert_strict_mode_config(strict_mode_config)
-        if isinstance(metadata, models.Payload):
-            metadata = RestToGrpc.convert_payload(metadata)
+        if isinstance(metadata, models.Payload):
+            metadata = RestToGrpc.convert_payload(metadata)
         return (
             await self.grpc_collections.Update(
                 grpc.UpdateCollection(
                     collection_name=collection_name,
                     ...
                     timeout=timeout,
-                    metadata=metadata,
+                    metadata=metadata,
                 ),
                 timeout=timeout if timeout is not None else self._timeout,
             )
         ).result
-    if isinstance(optimizers_config, grpc.OptimizersConfigDiff):
+    if isinstance(optimizers_config, grpc.OptimizersConfigDiff):
         optimizers_config = GrpcToRest.convert_optimizers_config_diff(optimizers_config)
     ...
+    # Normalize gRPC metadata to REST payload if needed
+    if metadata is not None and not isinstance(metadata, dict):
+        metadata = GrpcToRest.convert_payload(metadata)
     result: Optional[bool] = (
         await self.http.collections_api.update_collection(
             collection_name,
             update_collection=models.UpdateCollection(
                 ...
-                metadata=metadata,
+                metadata=metadata,
             ),
             timeout=timeout,
         )
     ).result

Also applies to: 2400-2415, 2440-2444


2483-2484: Do the same metadata normalization for create_collection (REST path)

Mirror the conversion so REST always receives JSON-serializable payloads.

         if isinstance(strict_mode_config, models.StrictModeConfig):
             strict_mode_config = RestToGrpc.convert_strict_mode_config(strict_mode_config)
-        if isinstance(metadata, models.Payload):
-            metadata = RestToGrpc.convert_payload(metadata)
+        if isinstance(metadata, models.Payload):
+            metadata = RestToGrpc.convert_payload(metadata)
         create_collection = grpc.CreateCollection(
             ...
             strict_mode_config=strict_mode_config,
-            metadata=metadata,
+            metadata=metadata,
         )
         return (
             await self.grpc_collections.Create(create_collection, timeout=self._timeout)
         ).result
@@
-    create_collection_request = models.CreateCollection(
+    # Normalize gRPC metadata to REST payload if needed
+    if metadata is not None and not isinstance(metadata, dict):
+        metadata = GrpcToRest.convert_payload(metadata)
+    create_collection_request = models.CreateCollection(
         ...
-        metadata=metadata,
+        metadata=metadata,
     )

Also applies to: 2515-2516, 2532-2533, 2561-2562

qdrant_client/qdrant_remote.py (1)

1936-1977: Parity: convert grpc.PointVectors in REST path

REST branch does not convert grpc.PointVectors to REST models; gRPC branch does. For symmetry and to honor the “accept both gRPC and REST structures” contract, convert items when needed.

         else:
+            # Normalize potential gRPC inputs to REST models
+            if isinstance(points, list):
+                points = [
+                    (GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
+                    for p in points
+                ]
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
 
             return self.openapi_client.points_api.update_vectors(
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ffc3e84 and 7dd66fe.

📒 Files selected for processing (6)
  • qdrant_client/async_qdrant_client.py (15 hunks)
  • qdrant_client/async_qdrant_remote.py (17 hunks)
  • qdrant_client/conversions/conversion.py (10 hunks)
  • qdrant_client/qdrant_client.py (15 hunks)
  • qdrant_client/qdrant_remote.py (15 hunks)
  • tests/conversions/fixtures.py (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
qdrant_client/async_qdrant_remote.py (5)
qdrant_client/http/models/models.py (2)
  • Filter (877-887)
  • UpdateVectors (3300-3303)
qdrant_client/conversions/conversion.py (8)
  • RestToGrpc (2491-4791)
  • convert_filter (166-182)
  • convert_filter (2493-2513)
  • GrpcToRest (140-2481)
  • convert_update_result (465-469)
  • convert_update_result (2677-2681)
  • convert_payload (581-582)
  • convert_payload (2795-2796)
qdrant_client/async_qdrant_client.py (1)
  • update_vectors (1592-1648)
qdrant_client/qdrant_client.py (1)
  • update_vectors (1646-1704)
qdrant_client/qdrant_remote.py (1)
  • update_vectors (1929-1977)
qdrant_client/qdrant_remote.py (4)
qdrant_client/http/models/models.py (2)
  • Filter (877-887)
  • UpdateVectors (3300-3303)
qdrant_client/grpc/points_pb2.pyi (3)
  • models (5151-5151)
  • UpsertPoints (638-686)
  • UpdatePointVectors (784-828)
qdrant_client/conversions/conversion.py (8)
  • RestToGrpc (2491-4791)
  • convert_filter (166-182)
  • convert_filter (2493-2513)
  • GrpcToRest (140-2481)
  • convert_update_result (465-469)
  • convert_update_result (2677-2681)
  • convert_payload (581-582)
  • convert_payload (2795-2796)
qdrant_client/async_qdrant_remote.py (2)
  • grpc_points (276-284)
  • update_vectors (1694-1739)
tests/conversions/fixtures.py (4)
qdrant_client/grpc/points_pb2.pyi (16)
  • Match (4718-4771)
  • FieldCondition (4656-4714)
  • match (4671-4672)
  • Condition (4537-4574)
  • field (4548-4548)
  • params (1461-1462)
  • params (1617-1618)
  • params (1877-1878)
  • params (2025-2026)
  • params (2199-2200)
  • params (2856-2857)
  • params (2928-2929)
  • params (3069-3070)
  • Query (2772-2829)
  • rrf (2811-2812)
  • Rrf (2753-2768)
qdrant_client/http/models/models.py (3)
  • FieldCondition (853-874)
  • CollectionConfig (222-242)
  • Rrf (2429-2434)
qdrant_client/grpc/collections_pb2.pyi (20)
  • metadata (1478-1479)
  • metadata (1589-1590)
  • metadata (1806-1807)
  • CollectionConfig (1760-1824)
  • params (492-492)
  • params (513-513)
  • params (1571-1572)
  • params (1788-1789)
  • params (2170-2171)
  • hnsw_config (352-353)
  • hnsw_config (397-398)
  • hnsw_config (1443-1444)
  • hnsw_config (1574-1575)
  • hnsw_config (1791-1792)
  • optimizer_config (1794-1795)
  • wal_config (1446-1447)
  • wal_config (1797-1798)
  • strict_mode_config (1475-1476)
  • strict_mode_config (1586-1587)
  • strict_mode_config (1803-1804)
qdrant_client/grpc/json_with_int_pb2.pyi (1)
  • Value (84-131)
qdrant_client/conversions/conversion.py (4)
qdrant_client/grpc/collections_pb2.pyi (8)
  • metadata (1478-1479)
  • metadata (1589-1590)
  • metadata (1806-1807)
  • shard_key (2472-2473)
  • shard_key (2502-2503)
  • shard_key (2556-2556)
  • shard_key (2755-2756)
  • shard_key (2786-2787)
qdrant_client/grpc/points_pb2.pyi (56)
  • HasField (243-243)
  • HasField (269-269)
  • HasField (307-307)
  • HasField (348-348)
  • HasField (369-369)
  • HasField (391-391)
  • HasField (412-412)
  • HasField (467-467)
  • HasField (514-514)
  • HasField (611-611)
  • HasField (677-677)
  • HasField (720-720)
  • HasField (771-771)
  • HasField (819-819)
  • HasField (849-849)
  • HasField (889-889)
  • rrf (2811-2812)
  • Rrf (2753-2768)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • shard_key (3842-3843)
  • shard_key (4206-4207)
  • shard_key_selector (662-663)
  • shard_key_selector (709-710)
  • shard_key_selector (756-757)
  • shard_key_selector (804-805)
  • shard_key_selector (877-878)
  • shard_key_selector (941-942)
  • shard_key_selector (994-995)
  • shard_key_selector (1038-1039)
  • shard_key_selector (1478-1479)
  • shard_key_selector (1639-1640)
  • shard_key_selector (1771-1772)
  • shard_key_selector (1821-1822)
  • shard_key_selector (1905-1906)
  • shard_key_selector (2058-2059)
  • shard_key_selector (2217-2218)
  • shard_key_selector (2309-2310)
  • update_vectors (3671-3671)
  • UpdateVectors (3551-3578)
  • Match (4718-4771)
  • Query (2772-2829)
  • PointsUpdateOperation (3390-3694)
  • PointStructList (3393-3419)
qdrant_client/async_qdrant_remote.py (2)
  • rest (309-315)
  • update_vectors (1694-1739)
qdrant_client/http/models/models.py (6)
  • MatchTextAny (1562-1567)
  • RrfQuery (2437-2438)
  • Rrf (2429-2434)
  • PointsList (1977-1983)
  • UpdateVectorsOperation (3306-3307)
  • UpdateVectors (3300-3303)
🪛 GitHub Actions: Integration tests
qdrant_client/conversions/conversion.py

[error] 267-267: ValueError: Field qdrant.CollectionConfig.metadata does not have presence. Command: coverage run --include=qdrant_client/conversions/conversion.py -m pytest tests/conversions/test_validate_conversions.py -vv -s

🔇 Additional comments (20)
tests/conversions/fixtures.py (2)

369-369: CollectionConfig.metadata fixture looks correct

The metadata map uses json_with_int.Value as required and is threaded into a dedicated CollectionConfig fixture. This pairs well with the new conversion paths.

Also applies to: 379-386, 1571-1571


1509-1511: RRF query fixtures are well-formed; they will surface a conversion bug

The three RRF variants (k set, default, explicit None) are useful. Note: current GrpcToRest.convert_query incorrectly reads k from the wrong object (see separate comment in conversion.py), so these tests should catch it.

Also applies to: 1727-1741

qdrant_client/conversions/conversion.py (6)

679-681: LGTM: text_any support in Match conversion

New variant correctly maps to rest.MatchTextAny.


1979-1987: LGTM: upsert update_filter propagation (gRPC -> REST)

Correctly threads optional update_filter into UpsertOperation.PointsList.


2088-2096: LGTM: update_vectors update_filter propagation (gRPC -> REST)

Correctly threads optional update_filter into UpdateVectors.


3044-3046: LGTM: text_any support (REST -> gRPC)

Maps MatchTextAny to grpc.Match(text_any=...).


4382-4392: LGTM: upsert update_filter propagation (REST -> gRPC)

Correctly forwards optional update_filter.


4497-4509: LGTM: update_vectors update_filter propagation (REST -> gRPC)

Correctly forwards optional update_filter.

qdrant_client/async_qdrant_client.py (2)

2184-2185: Collection metadata API addition looks good

Signature, docs, and wiring to remote all align. No issues.

Also applies to: 2201-2202, 2222-2223


2264-2265: Create-collection metadata support is correctly wired

Good addition and pass-through to remote.

Also applies to: 2311-2312, 2333-2334

qdrant_client/async_qdrant_remote.py (2)

2590-2591: Recreate-collection metadata passthrough is fine

Reuse via create_collection keeps behavior consistent.

Also applies to: 2610-2611


1623-1624: update_filter is exposed via the generated REST models — use point_insert_operations.update_filter

upsert_points (qdrant_client/http/api/points_api.py — async ~line 746, sync ~984) accepts a point_insert_operations model; qdrant_client/http/models/models.py includes update_filter (e.g., UpdateVectors.update_filter ~line 3303 and the PointInsertOperations-related field ~line 1971), so the REST upsert supports update_filter via the model (no change required).

qdrant_client/qdrant_remote.py (4)

1847-1894: LGTM: update_filter support in upsert

Addition, conversions, and forwarding look correct for both gRPC and REST paths.

Also applies to: 1916-1925


2632-2633: LGTM: collection-level metadata (gRPC path) in update_collection

New field is correctly wired through gRPC.

Also applies to: 2675-2676


2746-2747: LGTM: collection-level metadata (gRPC path) in create_collection

Field is propagated and converted for gRPC.

Also applies to: 2809-2810


2871-2872: LGTM: metadata passthrough in recreate_collection

Parameter is consistently forwarded to create step.

Also applies to: 2892-2893

qdrant_client/qdrant_client.py (4)

1598-1600: Docstring correctness for update_filter

After fixing the type, the existing descriptions remain accurate. No further changes needed.

Also applies to: 1682-1684


2258-2260: LGTM: expose metadata on update_collection wrapper

Public surface matches underlying client and docs.

Also applies to: 2299-2301


2392-2392: LGTM: expose metadata on create_collection wrapper

Parameter is properly forwarded.

Also applies to: 2412-2413


2435-2436: LGTM: expose metadata on recreate_collection wrapper

Consistent with update/create paths.

Also applies to: 2512-2514

Comment on lines 267 to 268
metadata=cls.convert_payload(model.metadata) if model.HasField("metadata") else None,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix: HasField on protobuf map (metadata) causes runtime error

Map fields don’t have presence; calling HasField("metadata") raises “does not have presence”. Use length/emptiness instead.

Apply:

-            metadata=cls.convert_payload(model.metadata) if model.HasField("metadata") else None,
+            metadata=cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
metadata=cls.convert_payload(model.metadata) if model.HasField("metadata") else None,
)
metadata=cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None,
)
🧰 Tools
🪛 GitHub Actions: Integration tests

[error] 267-267: ValueError: Field qdrant.CollectionConfig.metadata does not have presence. Command: coverage run --include=qdrant_client/conversions/conversion.py -m pytest tests/conversions/test_validate_conversions.py -vv -s

🤖 Prompt for AI Agents
In qdrant_client/conversions/conversion.py around lines 267-268, the code calls
model.HasField("metadata") on a protobuf map which raises a runtime error
because map fields have no presence; replace the HasField check with an
emptiness check (e.g., if model.metadata and len(model.metadata) > 0 or simply
if model.metadata) and pass metadata=cls.convert_payload(model.metadata) only
when the map is non-empty (otherwise None), ensuring no use of HasField for map
fields.

💡 Verification agent

🧩 Analysis chain

Quick verification for all map-field presence checks and RRF k usage

Run to ensure no other HasField calls on map fields and to confirm there are no remaining incorrect reads of k.

Also applies to: 1355-1358


🏁 Script executed:

#!/bin/bash
# Find HasField misuse on map fields
rg -nP 'HasField\(\s*"metadata"\s*\)|HasField\(\s*"payload_schema"\s*\)' -C2

# Spot any other RRF conversions reading k from the wrong symbol
rg -nP 'Rrf.*\bk\b' qdrant_client/conversions/conversion.py -n -C2

Length of output: 797


Fix HasField on map field and wrong RRF.k source

  • qdrant_client/conversions/conversion.py:267 — "metadata" is a proto map; HasField("metadata") is invalid. Replace with a presence check (e.g., bool(model.metadata) or len(model.metadata) > 0) and call convert_payload when non-empty.
  • qdrant_client/conversions/conversion.py:1355–1357 — code does val = model.rrf then returns rest.Rrf(k=model.k); model.k is wrong. Use val.k or model.rrf.k (serialization at 3644–3645 uses model.rrf.k, make them consistent).
🧰 Tools
🪛 GitHub Actions: Integration tests

[error] 267-267: ValueError: Field qdrant.CollectionConfig.metadata does not have presence. Command: coverage run --include=qdrant_client/conversions/conversion.py -m pytest tests/conversions/test_validate_conversions.py -vv -s

🤖 Prompt for AI Agents
In qdrant_client/conversions/conversion.py around line 267 and lines ~1355–1357,
the code uses model.HasField("metadata") for a proto map (invalid) and later
reads model.rrf into val but constructs rest.Rrf(k=model.k) using the wrong
source; change the metadata check to test presence/emptiness (e.g.,
bool(model.metadata) or len(model.metadata) > 0) and only call convert_payload
when non-empty, and in the rrf conversion return rest.Rrf(k=val.k) (or
rest.Rrf(k=model.rrf.k)) to use the k from the rrf object consistently with
serialization lines 3644–3645.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
qdrant_client/conversions/conversion.py (1)

2169-2175: Bug: incorrect HasField test makes index always set.

if model.HasField("index") is not None is always true; it should be a plain boolean check.

-            index=(
-                cls.convert_sparse_index_config(model.index)
-                if model.HasField("index") is not None
-                else None
-            ),
+            index=(
+                cls.convert_sparse_index_config(model.index)
+                if model.HasField("index")
+                else None
+            ),
♻️ Duplicate comments (1)
qdrant_client/conversions/conversion.py (1)

267-268: Fix presence check for protobuf map field metadata.

Map fields have no presence; model.metadata is not None is always true. Use emptiness to decide whether to include the field.

-            metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
+            metadata=cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7dd66fe and 43bdab6.

📒 Files selected for processing (1)
  • qdrant_client/conversions/conversion.py (13 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
qdrant_client/conversions/conversion.py (4)
qdrant_client/grpc/collections_pb2.pyi (8)
  • metadata (1478-1479)
  • metadata (1589-1590)
  • metadata (1806-1807)
  • shard_key (2472-2473)
  • shard_key (2502-2503)
  • shard_key (2556-2556)
  • shard_key (2755-2756)
  • shard_key (2786-2787)
qdrant_client/qdrant_remote.py (3)
  • rest (351-357)
  • upsert (1840-1927)
  • update_vectors (1929-1977)
qdrant_client/http/models/models.py (5)
  • MatchTextAny (1562-1567)
  • RrfQuery (2437-2438)
  • Rrf (2429-2434)
  • PointsList (1977-1983)
  • UpdateVectors (3300-3303)
qdrant_client/grpc/points_pb2.pyi (57)
  • rrf (2811-2812)
  • Rrf (2753-2768)
  • HasField (243-243)
  • HasField (269-269)
  • HasField (307-307)
  • HasField (348-348)
  • HasField (369-369)
  • HasField (391-391)
  • HasField (412-412)
  • HasField (467-467)
  • HasField (514-514)
  • HasField (611-611)
  • HasField (677-677)
  • HasField (720-720)
  • HasField (771-771)
  • HasField (819-819)
  • HasField (849-849)
  • HasField (889-889)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • upsert (3659-3659)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • shard_key (3842-3843)
  • shard_key (4206-4207)
  • shard_key_selector (662-663)
  • shard_key_selector (709-710)
  • shard_key_selector (756-757)
  • shard_key_selector (804-805)
  • shard_key_selector (877-878)
  • shard_key_selector (941-942)
  • shard_key_selector (994-995)
  • shard_key_selector (1038-1039)
  • shard_key_selector (1478-1479)
  • shard_key_selector (1639-1640)
  • shard_key_selector (1771-1772)
  • shard_key_selector (1821-1822)
  • shard_key_selector (1905-1906)
  • shard_key_selector (2058-2059)
  • shard_key_selector (2217-2218)
  • shard_key_selector (2309-2310)
  • update_vectors (3671-3671)
  • UpdateVectors (3551-3578)
  • Match (4718-4771)
  • Query (2772-2829)
  • PointsUpdateOperation (3390-3694)
  • PointStructList (3393-3419)
🪛 GitHub Actions: Integration tests
qdrant_client/conversions/conversion.py

[error] 766-766: pydantic ValidationError: UpdateCollection.metadata expects a dictionary but received a tuple (input_value=({},), input_type=tuple) in convert_update_collection (conversion.py:766).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
🔇 Additional comments (4)
qdrant_client/conversions/conversion.py (4)

679-681: LGTM: adds gRPC→REST support for Match.text_any.

Mapping to rest.MatchTextAny(text_any=val) is correct.


1358-1361: LGTM: correct RRF gRPC→REST conversion and optional k.

Reading k from model.rrf and preserving optionality is right.


1982-1984: LGTM: wires update_filter for upsert/update_vectors (gRPC→REST).

Propagating update_filter to REST payload aligns with new API.

Also applies to: 1989-1990, 2091-2093, 2098-2099


3049-3050: LGTM: adds REST→gRPC support for MatchTextAny.

grpc.Match(text_any=...) matches the proto.

Comment on lines 792 to 795
metadata=(
cls.convert_payload(model.metadata) if model.metadata is not None else None,
),
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: tuple passed to UpdateCollection.metadata (causes ValidationError).

The extra parentheses + trailing comma build a tuple ({},). Return a dict or None instead. Also, avoid presence checks on map fields; use emptiness.

-            metadata=(
-                cls.convert_payload(model.metadata) if model.metadata is not None else None,
-            ),
+            metadata=cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
metadata=(
cls.convert_payload(model.metadata) if model.metadata is not None else None,
),
)
metadata=cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None,
)
🤖 Prompt for AI Agents
In qdrant_client/conversions/conversion.py around lines 792 to 795, the code
currently wraps the metadata result in parentheses and a trailing comma causing
a tuple (e.g. ({},),) to be passed to UpdateCollection.metadata which triggers a
ValidationError; change the call to pass either the dict returned by
cls.convert_payload(model.metadata) or None (no tuple), and replace any presence
checks on map fields with emptiness checks (e.g., use if model.metadata and
len(model.metadata) > 0 or simply if model.metadata) so an empty map becomes
None or an empty dict as appropriate before assignment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
qdrant_client/conversions/conversion.py (3)

267-268: Fix presence check for protobuf map field metadata (use emptiness, not None).

Map fields are always non-None; use len(model.metadata) to decide None vs dict.

Apply:

-            metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
+            # Map fields have no presence; use emptiness to omit when empty
+            metadata=(
+                cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None
+            ),

792-793: Same map-field presence issue for UpdateCollection.metadata.

Use emptiness check to avoid always populating metadata.

Apply:

-            metadata=(cls.convert_payload(model.metadata) if model.metadata is not None else None),
+            metadata=(
+                cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None
+            ),

3646-3648: Preserve optional k in REST→gRPC RRF (don’t set k=None).

Avoid passing None into proto optional; set only when present.

Apply:

-        if isinstance(model, rest.RrfQuery):
-            return grpc.Query(rrf=grpc.Rrf(k=model.rrf.k))
+        if isinstance(model, rest.RrfQuery):
+            rrf = grpc.Rrf()
+            if model.rrf.k is not None:
+                rrf.k = model.rrf.k
+            return grpc.Query(rrf=rrf)
🧹 Nitpick comments (1)
tests/conversions/fixtures.py (1)

369-370: Metadata fixture looks good; add an empty-metadata variant to exercise “omit when empty”.

Helps validate converters return None (not {}) when map is empty.

Also applies to: 379-386

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43bdab6 and 6a24cc5.

📒 Files selected for processing (2)
  • qdrant_client/conversions/conversion.py (13 hunks)
  • tests/conversions/fixtures.py (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
qdrant_client/conversions/conversion.py (4)
qdrant_client/grpc/collections_pb2.pyi (8)
  • metadata (1478-1479)
  • metadata (1589-1590)
  • metadata (1806-1807)
  • shard_key (2472-2473)
  • shard_key (2502-2503)
  • shard_key (2556-2556)
  • shard_key (2755-2756)
  • shard_key (2786-2787)
qdrant_client/qdrant_remote.py (3)
  • rest (351-357)
  • upsert (1840-1927)
  • update_vectors (1929-1977)
qdrant_client/http/models/models.py (6)
  • MatchTextAny (1562-1567)
  • RrfQuery (2437-2438)
  • Rrf (2429-2434)
  • UpsertOperation (3310-3311)
  • PointsList (1977-1983)
  • UpdateVectors (3300-3303)
qdrant_client/grpc/points_pb2.pyi (57)
  • rrf (2811-2812)
  • Rrf (2753-2768)
  • HasField (243-243)
  • HasField (269-269)
  • HasField (307-307)
  • HasField (348-348)
  • HasField (369-369)
  • HasField (391-391)
  • HasField (412-412)
  • HasField (467-467)
  • HasField (514-514)
  • HasField (611-611)
  • HasField (677-677)
  • HasField (720-720)
  • HasField (771-771)
  • HasField (819-819)
  • HasField (849-849)
  • HasField (889-889)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • upsert (3659-3659)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • shard_key (3842-3843)
  • shard_key (4206-4207)
  • shard_key_selector (662-663)
  • shard_key_selector (709-710)
  • shard_key_selector (756-757)
  • shard_key_selector (804-805)
  • shard_key_selector (877-878)
  • shard_key_selector (941-942)
  • shard_key_selector (994-995)
  • shard_key_selector (1038-1039)
  • shard_key_selector (1478-1479)
  • shard_key_selector (1639-1640)
  • shard_key_selector (1771-1772)
  • shard_key_selector (1821-1822)
  • shard_key_selector (1905-1906)
  • shard_key_selector (2058-2059)
  • shard_key_selector (2217-2218)
  • shard_key_selector (2309-2310)
  • update_vectors (3671-3671)
  • UpdateVectors (3551-3578)
  • Match (4718-4771)
  • Query (2772-2829)
  • PointsUpdateOperation (3390-3694)
  • PointStructList (3393-3419)
tests/conversions/fixtures.py (4)
qdrant_client/grpc/points_pb2.pyi (16)
  • Match (4718-4771)
  • FieldCondition (4656-4714)
  • match (4671-4672)
  • Condition (4537-4574)
  • field (4548-4548)
  • params (1461-1462)
  • params (1617-1618)
  • params (1877-1878)
  • params (2025-2026)
  • params (2199-2200)
  • params (2856-2857)
  • params (2928-2929)
  • params (3069-3070)
  • Query (2772-2829)
  • rrf (2811-2812)
  • Rrf (2753-2768)
qdrant_client/http/models/models.py (3)
  • FieldCondition (853-874)
  • CollectionConfig (222-242)
  • Rrf (2429-2434)
qdrant_client/grpc/collections_pb2.pyi (20)
  • metadata (1478-1479)
  • metadata (1589-1590)
  • metadata (1806-1807)
  • CollectionConfig (1760-1824)
  • params (492-492)
  • params (513-513)
  • params (1571-1572)
  • params (1788-1789)
  • params (2170-2171)
  • hnsw_config (352-353)
  • hnsw_config (397-398)
  • hnsw_config (1443-1444)
  • hnsw_config (1574-1575)
  • hnsw_config (1791-1792)
  • optimizer_config (1794-1795)
  • wal_config (1446-1447)
  • wal_config (1797-1798)
  • strict_mode_config (1475-1476)
  • strict_mode_config (1586-1587)
  • strict_mode_config (1803-1804)
qdrant_client/grpc/json_with_int_pb2.pyi (1)
  • Value (84-131)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
🔇 Additional comments (6)
tests/conversions/fixtures.py (3)

38-39: Nice addition: text_any fixtures are wired through match → field_condition → condition → filter.

Covers the full path for text_any. No changes requested.

Also applies to: 52-53, 129-130, 150-151


1510-1513: Good RRF coverage (k set, default, explicit None).

Ensure REST→gRPC preserves optional k by not setting it when None (see conversion counterpart fix suggestion in conversion.py).


1572-1573: Including both configs in fixtures is 👍

Ensures metadata path is covered alongside the base config.

qdrant_client/conversions/conversion.py (3)

679-681: text_any conversion added correctly.

Mapping grpc.Match.text_any → rest.MatchTextAny is correct and complete.


1356-1359: RRF (gRPC→REST) fix is correct.

Now reads k from rrf and preserves optionality. LGTM.


1981-1988: Approve — update_filter plumbing verified

update_filter propagation confirmed: RestToGrpc/GrpcToRest conversions are invoked and update_filter is forwarded at HTTP and gRPC call sites (qdrant_client/qdrant_remote.py, async_qdrant_remote.py, qdrant_client.py, async_qdrant_client.py). No changes required.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
qdrant_client/qdrant_remote.py (2)

1841-1848: Public API type mismatch — update_filter must be types.Filter (not types.Payload)

Remote layers use Optional[types.Filter] but QdrantClient/AsyncQdrantClient expose Optional[types.Payload]; standardize the public wrappers to types.Filter to avoid API/type-hint drift and mypy errors.

  • qdrant_client/qdrant_remote.py — update_filter: Optional[types.Filter] (lines 1847, 1936)
  • qdrant_client/async_qdrant_remote.py — update_filter: Optional[types.Filter] (lines 1623, 1701)
  • qdrant_client/qdrant_client.py — update_filter: Optional[types.Payload] (lines 1572, 1653) ← change to Optional[types.Filter]
  • qdrant_client/async_qdrant_client.py — update_filter: Optional[types.Payload] (lines 1524, 1599) ← change to Optional[types.Filter]

Action: update QdrantClient and AsyncQdrantClient signatures + docstrings/tests to use Optional[types.Filter] and adjust any callers.


1930-1938: Public API type mismatch — update_filter must be Optional[types.Filter]

  • Change public client wrappers that use Optional[types.Payload] (qdrant_client/qdrant_client.py:~1572, qdrant_client/async_qdrant_client.py:~1524 and corresponding client_base/async_client_base wrappers) to Optional[types.Filter] to match qdrant_client/qdrant_remote.py, the conversion logic, and the HTTP/gRPC models.
  • Update docs/type hints to reflect the same public API.
♻️ Duplicate comments (4)
qdrant_client/qdrant_remote.py (4)

1911-1925: REST upsert: pass update_filter inside body object, not as top-level kwarg

Generated REST clients typically expect update_filter inside PointsList/PointsBatch; passing it as a method kwarg can raise “unexpected keyword argument”. Also convert update_filter before constructing the body.

Apply:

@@
-            if isinstance(points, list):
+            if isinstance(update_filter, grpc.Filter):
+                update_filter = GrpcToRest.convert_filter(model=update_filter)
+
+            if isinstance(points, list):
                 points = [
                     (
                         GrpcToRest.convert_point_struct(point)
                         if isinstance(point, grpc.PointStruct)
                         else point
                     )
                     for point in points
                 ]
-
-                points = models.PointsList(points=points, shard_key=shard_key_selector)
+                points = models.PointsList(
+                    points=points,
+                    shard_key=shard_key_selector,
+                    update_filter=update_filter,
+                )
 
-            if isinstance(points, models.Batch):
-                points = models.PointsBatch(batch=points, shard_key=shard_key_selector)
-
-            if isinstance(update_filter, grpc.Filter):
-                update_filter = GrpcToRest.convert_filter(model=update_filter)
+            if isinstance(points, models.Batch):
+                points = models.PointsBatch(
+                    batch=points,
+                    shard_key=shard_key_selector,
+                    update_filter=update_filter,
+                )
@@
             http_result = self.openapi_client.points_api.upsert_points(
                 collection_name=collection_name,
                 wait=wait,
                 point_insert_operations=points,
                 ordering=ordering,
-                update_filter=update_filter,
             ).result

Verify method signature in generated client:

#!/bin/bash
fd -a 'points_api.py' qdrant_client/http/api | while read -r f; do
  echo "== $f =="; rg -nP -C2 'def\s+upsert_points\s*\(' "$f"; rg -n 'update_filter' "$f" || true
done

2661-2663: Bug: invalid isinstance check for metadata (Payload); conversion skipped

models.Payload is a typing alias; get_args(models.Payload) yields type arguments (e.g., (str, Any)), not a runtime class tuple. This path will skip conversion and send raw JSON to gRPC, causing type errors.

Apply:

-            if isinstance(metadata, get_args(models.Payload)):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if isinstance(metadata, Mapping):
+                # If already a mapping of grpc.Value, keep as-is; otherwise convert JSON -> grpc.Value
+                first_val = next(iter(metadata.values()), None)
+                if not isinstance(first_val, grpc.Value):
+                    metadata = RestToGrpc.convert_payload(metadata)

2790-2792: Same metadata isinstance bug in create_collection (gRPC path)

Repeat the safe Mapping + grpc.Value-aware conversion here.

-            if isinstance(metadata, get_args(models.Payload)):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if isinstance(metadata, Mapping):
+                first_val = next(iter(metadata.values()), None)
+                if not isinstance(first_val, grpc.Value):
+                    metadata = RestToGrpc.convert_payload(metadata)

1965-1975: REST update_vectors should accept grpc.PointVectors — normalize before sending

If callers pass gRPC PointVectors, JSON encoding fails. Convert to REST models before building the request.

Apply:

             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
 
+            # Normalize gRPC PointVectors to REST model if provided
+            if isinstance(points, list):
+                points = [
+                    (GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
+                    for p in points
+                ]
+
             return self.openapi_client.points_api.update_vectors(
                 collection_name=collection_name,
                 wait=wait,
                 update_vectors=models.UpdateVectors(
                     points=points,
                     shard_key=shard_key_selector,
                     update_filter=update_filter,
                 ),
                 ordering=ordering,
             ).result
🧹 Nitpick comments (1)
qdrant_client/qdrant_remote.py (1)

1961-1963: Nit: wrong assertion message

This is update_vectors, but the message says “Upsert returned None result”.

-            assert grpc_result is not None, "Upsert returned None result"
+            assert grpc_result is not None, "Update vectors returned None result"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a24cc5 and 1eddb54.

📒 Files selected for processing (1)
  • qdrant_client/qdrant_remote.py (15 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
qdrant_client/qdrant_remote.py (6)
qdrant_client/http/models/models.py (2)
  • Filter (877-887)
  • UpdateVectors (3300-3303)
qdrant_client/grpc/points_pb2.pyi (3)
  • models (5151-5151)
  • UpsertPoints (638-686)
  • UpdatePointVectors (784-828)
qdrant_client/conversions/conversion.py (8)
  • RestToGrpc (2492-4794)
  • convert_filter (166-182)
  • convert_filter (2494-2514)
  • GrpcToRest (140-2482)
  • convert_update_result (465-469)
  • convert_update_result (2678-2682)
  • convert_payload (581-582)
  • convert_payload (2796-2797)
qdrant_client/async_qdrant_client.py (2)
  • grpc_points (169-177)
  • update_vectors (1592-1648)
qdrant_client/qdrant_client.py (2)
  • grpc_points (187-196)
  • update_vectors (1646-1704)
qdrant_client/async_qdrant_remote.py (2)
  • grpc_points (276-284)
  • update_vectors (1694-1739)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_qdrant_client.py (1)

1177-1178: Strengthen assertion with a type check (guards against unexpected model wrappers).

Verify the field is a plain dict before equality.

-    assert collection_info.config.metadata == collection_metadata
+    assert isinstance(collection_info.config.metadata, dict)
+    assert collection_info.config.metadata == collection_metadata
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dace96c and dc0821c.

📒 Files selected for processing (1)
  • tests/test_qdrant_client.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_qdrant_client.py (7)
qdrant_client/async_qdrant_remote.py (2)
  • create_collection (2467-2574)
  • get_collection (2340-2354)
qdrant_client/qdrant_remote.py (2)
  • create_collection (2730-2855)
  • get_collection (2597-2609)
qdrant_client/async_qdrant_client.py (2)
  • create_collection (2245-2335)
  • get_collection (2149-2159)
qdrant_client/qdrant_client.py (2)
  • create_collection (2323-2414)
  • get_collection (2221-2232)
qdrant_client/local/async_qdrant_local.py (2)
  • create_collection (916-960)
  • get_collection (870-872)
qdrant_client/local/qdrant_local.py (2)
  • create_collection (988-1039)
  • get_collection (939-941)
qdrant_client/grpc/collections_pb2.pyi (1)
  • config (2228-2229)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
  • GitHub Check: Python 3.12.x on ubuntu-latest test
  • GitHub Check: Python 3.13.x on ubuntu-latest test
  • GitHub Check: Python 3.10.x on ubuntu-latest test
  • GitHub Check: Python 3.9.x on ubuntu-latest test
  • GitHub Check: Python 3.11.x on ubuntu-latest test

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
qdrant_client/grpc/collections_pb2.pyi (1)

11-11: Use package-relative import for sibling proto module.

import json_with_int_pb2 will try to resolve a top-level module and breaks once the client is installed as qdrant_client.grpc. Generated stubs need relative imports to load their siblings reliably. Switch to from . import json_with_int_pb2 (same fix was called out earlier).

-import json_with_int_pb2
+from . import json_with_int_pb2
qdrant_client/conversions/conversion.py (3)

3656-3658: Preserve optional k field: avoid setting to None.

Passing model.rrf.k directly to grpc.Rrf(k=...) will set the protobuf optional field even when k is None. Only include k when it's present.

Apply this diff:

-        if isinstance(model, rest.RrfQuery):
-            return grpc.Query(rrf=grpc.Rrf(k=model.rrf.k))
+        if isinstance(model, rest.RrfQuery):
+            rrf = grpc.Rrf()
+            if model.rrf.k is not None:
+                rrf.k = model.rrf.k
+            return grpc.Query(rrf=rrf)

267-267: Use emptiness check instead of is not None for protobuf map field.

Protobuf map fields like model.metadata are always present (never None), so model.metadata is not None is always True. Check emptiness instead.

Apply this diff:

-            metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
+            metadata=cls.convert_payload(model.metadata) if model.metadata else None,

794-794: Use emptiness check for protobuf map field.

Same issue as line 267: model.metadata is a protobuf map that's never None. The check should test emptiness.

Apply this diff:

-            metadata=(cls.convert_payload(model.metadata) if model.metadata is not None else None),
+            metadata=cls.convert_payload(model.metadata) if model.metadata else None,
🧹 Nitpick comments (4)
qdrant_client/http/models/models.py (2)

1078-1081: Fix docstring: field name mismatch (full_scan_threshold_kbfull_scan_threshold).

Both HnswConfig and HnswConfigDiff reference full_scan_threshold_kb in text, but the field is full_scan_threshold.

-        description="Minimal size threshold (in KiloBytes) below which full-scan is preferred over HNSW search. This measures the total size of vectors being queried against. When the maximum estimated amount of points that a condition satisfies is smaller than `full_scan_threshold_kb`, the query planner will use full-scan search instead of HNSW index traversal for better performance. Note: 1Kb = 1 vector of size 256",
+        description="Minimal size threshold (in KiloBytes) below which full-scan is preferred over HNSW search. This measures the total size of vectors being queried against. When the maximum estimated amount of points that a condition satisfies is smaller than `full_scan_threshold`, the query planner will use full-scan search instead of HNSW index traversal for better performance. Note: 1Kb = 1 vector of size 256",

Also applies to: 1109-1112


3326-3329: Add missing description for UpdateVectors.update_filter.

Align with PointsBatch/PointsList and gRPC doc.

 class UpdateVectors(BaseModel, extra="forbid"):
     points: List["PointVectors"] = Field(..., description="Points with named vectors")
     shard_key: Optional["ShardKeySelector"] = Field(default=None, description="")
-    update_filter: Optional["Filter"] = Field(default=None, description="")
+    update_filter: Optional["Filter"] = Field(
+        default=None,
+        description="If specified, only points that match this filter will be updated",
+    )
qdrant_client/conversions/conversion.py (2)

2816-2816: Consider None check for optional field consistency.

copy_vectors is assigned directly without checking if it's None. While this may work, other optional fields in the same function (e.g., ef_construct, m, on_disk) use explicit None checks for consistency.

Apply this diff if you want consistency:

-            copy_vectors=model.copy_vectors,
+            copy_vectors=model.copy_vectors if model.copy_vectors is not None else None,

Also applies to line 2891.


2879-2879: Check for empty dict, not just None.

When model.metadata is an empty dict {}, it will still pass the is not None check and call convert_payload({}). Consider checking truthiness to skip empty dicts.

Apply this diff:

-            metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
+            metadata=cls.convert_payload(model.metadata) if model.metadata else None,

Also applies to line 3020.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a6ef1a and 556202e.

📒 Files selected for processing (6)
  • qdrant_client/conversions/conversion.py (21 hunks)
  • qdrant_client/grpc/collections_pb2.py (7 hunks)
  • qdrant_client/grpc/collections_pb2.pyi (27 hunks)
  • qdrant_client/http/models/models.py (31 hunks)
  • qdrant_client/proto/collections.proto (11 hunks)
  • tests/conversions/fixtures.py (11 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
tests/conversions/fixtures.py (4)
qdrant_client/grpc/points_pb2.pyi (8)
  • Match (4718-4771)
  • FieldCondition (4656-4714)
  • match (4671-4672)
  • Condition (4537-4574)
  • field (4548-4548)
  • Query (2772-2829)
  • rrf (2811-2812)
  • Rrf (2753-2768)
qdrant_client/http/models/models.py (4)
  • FieldCondition (860-881)
  • StrictModeConfig (3091-3136)
  • CollectionConfig (222-242)
  • Rrf (2444-2449)
qdrant_client/grpc/collections_pb2.pyi (8)
  • StrictModeConfig (1227-1351)
  • metadata (1527-1528)
  • metadata (1638-1639)
  • metadata (1855-1856)
  • CollectionConfig (1809-1873)
  • strict_mode_config (1524-1525)
  • strict_mode_config (1635-1636)
  • strict_mode_config (1852-1853)
qdrant_client/grpc/json_with_int_pb2.pyi (1)
  • Value (84-131)
qdrant_client/grpc/collections_pb2.py (2)
qdrant_client/grpc/collections_pb2.pyi (7)
  • CollectionWarning (773-783)
  • CreateCollection (1453-1578)
  • MetadataEntry (1456-1471)
  • MetadataEntry (1585-1600)
  • MetadataEntry (1812-1827)
  • UpdateCollection (1582-1671)
  • CollectionConfig (1809-1873)
qdrant_client/http/models/models.py (4)
  • CollectionWarning (359-360)
  • CreateCollection (476-527)
  • UpdateCollection (3270-3301)
  • CollectionConfig (222-242)
qdrant_client/conversions/conversion.py (4)
qdrant_client/grpc/collections_pb2.pyi (24)
  • metadata (1527-1528)
  • metadata (1638-1639)
  • metadata (1855-1856)
  • HasField (375-375)
  • HasField (411-411)
  • HasField (439-439)
  • HasField (471-471)
  • HasField (501-501)
  • HasField (522-522)
  • HasField (544-544)
  • HasField (570-570)
  • HasField (658-658)
  • HasField (702-702)
  • HasField (750-750)
  • HasField (847-847)
  • HasField (892-892)
  • HasField (922-922)
  • HasField (1025-1025)
  • HasField (1067-1067)
  • shard_key (2520-2521)
  • shard_key (2550-2551)
  • shard_key (2604-2604)
  • shard_key (2803-2804)
  • shard_key (2834-2835)
qdrant_client/async_qdrant_remote.py (2)
  • upsert (1616-1695)
  • update_vectors (1697-1742)
qdrant_client/http/models/models.py (5)
  • MatchTextAny (1577-1582)
  • RrfQuery (2452-2453)
  • Rrf (2444-2449)
  • PointsList (1992-1998)
  • UpdateVectors (3325-3328)
qdrant_client/grpc/points_pb2.pyi (41)
  • rrf (2811-2812)
  • Rrf (2753-2768)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • upsert (3659-3659)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • shard_key (3842-3843)
  • shard_key (4206-4207)
  • shard_key_selector (662-663)
  • shard_key_selector (709-710)
  • shard_key_selector (756-757)
  • shard_key_selector (804-805)
  • shard_key_selector (877-878)
  • shard_key_selector (941-942)
  • shard_key_selector (994-995)
  • shard_key_selector (1038-1039)
  • shard_key_selector (1478-1479)
  • shard_key_selector (1639-1640)
  • shard_key_selector (1771-1772)
  • shard_key_selector (1821-1822)
  • shard_key_selector (1905-1906)
  • shard_key_selector (2058-2059)
  • shard_key_selector (2217-2218)
  • shard_key_selector (2309-2310)
  • update_vectors (3671-3671)
  • UpdateVectors (3551-3578)
  • Match (4718-4771)
  • Query (2772-2829)
  • PointsUpdateOperation (3390-3694)
  • PointStructList (3393-3419)
qdrant_client/grpc/collections_pb2.pyi (2)
qdrant_client/http/models/models.py (1)
  • CollectionWarning (359-360)
qdrant_client/grpc/json_with_int_pb2.pyi (2)
  • value (61-61)
  • Value (84-131)
qdrant_client/http/models/models.py (2)
qdrant_client/grpc/points_pb2.pyi (20)
  • b (3343-3344)
  • options (318-319)
  • options (360-361)
  • options (403-404)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • Rrf (2753-2768)
  • rrf (2811-2812)
qdrant_client/grpc/collections_pb2.pyi (8)
  • stopwords (2021-2022)
  • metadata (1527-1528)
  • metadata (1638-1639)
  • metadata (1855-1856)
  • CollectionWarning (773-783)
  • languages (1984-1985)
  • custom (1987-1988)
  • WalConfigDiff (903-929)
🪛 Buf (1.57.2)
qdrant_client/proto/collections.proto

6-6: import "json_with_int.proto": file does not exist

(COMPILE)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
🔇 Additional comments (2)
qdrant_client/grpc/collections_pb2.py (1)

16-16: json_with_int_pb2 confirmed present Verified that json_with_int_pb2.py and its stub exist under qdrant_client/grpc and are tracked by git.

qdrant_client/proto/collections.proto (1)

6-7: json_with_int.proto import is correct. The file exists at proto/json_with_int.proto and stubs in grpc/json_with_int_pb2.py confirm successful code generation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 556202e and 6e0bb9a.

📒 Files selected for processing (1)
  • qdrant_client/embed/_inspection_cache.py (24 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
  • GitHub Check: Python 3.12.x on ubuntu-latest test
  • GitHub Check: Python 3.13.x on ubuntu-latest test
  • GitHub Check: Python 3.11.x on ubuntu-latest test
  • GitHub Check: Python 3.9.x on ubuntu-latest test
  • GitHub Check: Python 3.10.x on ubuntu-latest test
🔇 Additional comments (10)
qdrant_client/embed/_inspection_cache.py (10)

9-9: LGTM: New BM25 and RRF entries added to CACHE_STR_PATH.

The additions of Bm25Config, MatchTextAny, Rrf, and RrfQuery entries are consistent with the existing pattern and support the new features being introduced.

Also applies to: 83-83, 209-210


590-590: LGTM: MatchTextAny correctly integrated.

The new MatchTextAny type is properly defined and integrated into FieldCondition.match. The structure and description are consistent with existing match types like MatchText.

Also applies to: 1061-1074


1452-1521: LGTM: Comprehensive BM25 configuration.

The Bm25Config definition is well-structured with:

  • Standard BM25 parameters (k, b, avg_len) with sensible defaults
  • Complete text preprocessing options (tokenizer, language, stopwords, stemmer)
  • Token length filtering capabilities
  • Proper optional field handling with null types

1537-1545: LGTM: Document.options enhanced with BM25 support.

The options field now explicitly accepts Bm25Config while maintaining backward compatibility with arbitrary objects. This improves type safety and makes BM25 configuration a first-class citizen.


1599-1719: LGTM: Comprehensive language and text processing definitions.

The new definitions provide robust text processing support:

  • Language enum with 31 languages for general text processing
  • SnowballLanguage enum with 19 languages (Snowball stemmer subset)
  • SnowballParams for stemmer configuration
  • StopwordsSet allowing both predefined and custom stopwords
  • TokenizerType covering common tokenization strategies

All definitions are well-structured and consistent with the schema patterns.


1974-1979: LGTM: HnswConfigDiff.copy_vectors addition.

The new copy_vectors field is well-documented with clear explanations of:

  • The performance trade-off (search speed vs. disk usage)
  • Requirements (quantized vectors enabled)
  • Limitations (multi-vectors not supported)

1953-1953: LGTM: StrictModeConfig improvements.

The updates improve documentation consistency and add new functionality:

  • Enhanced full_scan_threshold description with clearer explanation
  • Consistent punctuation across field descriptions
  • Clearer descriptions for multivector_config and sparse_config
  • New max_payload_index_count field for limiting payload indexes

Also applies to: 2213-2214, 2219-2220, 2285-2286, 2297-2298, 2300-2305


2397-2402: LGTM: WalConfigDiff.wal_retain_closed addition.

The new wal_retain_closed field for controlling WAL segment retention is properly defined with clear description and consistent structure.


3047-3047: LGTM: RRF (Reciprocal Rank Fusion) support added.

The RRF implementation is clean and consistent:

  • Rrf type with optional k parameter for RRF algorithm
  • RrfQuery wrapper following the query type pattern
  • Proper integration into Prefetch and QueryRequest query unions
  • Updated Fusion description accurately reflects RRF support

Also applies to: 3447-3447, 3570-3590, 3677-3677


4245-4249: LGTM: update_filter feature for conditional updates.

The update_filter field is consistently added to PointsBatch, PointsList, and UpdateVectors, enabling conditional update/insert behavior. The implementation is clean with:

  • Uniform structure across all three types
  • Clear description explaining the conditional semantics
  • Proper optional field handling with Filter type

Also applies to: 4278-4282, 4318-4322

"anyOf": [{"type": "number"}, {"type": "null"}],
"default": None,
"description": "The midpoint of the decay. Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
"description": "The midpoint of the decay. Should be between 0 and 1.Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typographical error in description.

Missing space before "Defaults" in the description text.

Apply this diff to fix the typo:

-                "description": "The midpoint of the decay. Should be between 0 and 1.Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
+                "description": "The midpoint of the decay. Should be between 0 and 1. Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"description": "The midpoint of the decay. Should be between 0 and 1.Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
"description": "The midpoint of the decay. Should be between 0 and 1. Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
🤖 Prompt for AI Agents
In qdrant_client/embed/_inspection_cache.py around line 444, the description
string for "The midpoint of the decay..." is missing a space before "Defaults";
update the string to insert a space so it reads "...Should be between 0 and 1.
Defaults to 0.5. Output will be this value when `|x - target| == scale`." to
correct the typographical error.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
qdrant_client/async_qdrant_remote.py (1)

1416-1423: Bug: Missing read_consistency conversion in DiscoverBatch (gRPC path).

Passes REST-type consistency directly into gRPC. Convert before the call to avoid type errors.

             requests = [
                 RestToGrpc.convert_discover_request(r, collection_name)
                 if isinstance(r, models.DiscoverRequest)
                 else r
                 for r in requests
             ]
+            if isinstance(consistency, get_args_subscribed(models.ReadConsistency)):
+                consistency = RestToGrpc.convert_read_consistency(consistency)
             grpc_res: grpc.SearchBatchResponse = await self.grpc_points.DiscoverBatch(
                 grpc.DiscoverBatchPoints(
                     collection_name=collection_name,
                     discover_points=requests,
                     read_consistency=consistency,
                     timeout=timeout,
                 ),
qdrant_client/qdrant_remote.py (1)

1618-1626: Bug: Missing read_consistency conversion in DiscoverBatch (gRPC path).

Convert REST consistency to gRPC before use.

         if self._prefer_grpc:
             requests = [
                 (
                     RestToGrpc.convert_discover_request(r, collection_name)
                     if isinstance(r, models.DiscoverRequest)
                     else r
                 )
                 for r in requests
             ]
+            if isinstance(consistency, get_args_subscribed(models.ReadConsistency)):
+                consistency = RestToGrpc.convert_read_consistency(consistency)
 
             grpc_res: grpc.SearchBatchResponse = self.grpc_points.DiscoverBatch(
                 grpc.DiscoverBatchPoints(
                     collection_name=collection_name,
                     discover_points=requests,
                     read_consistency=consistency,
♻️ Duplicate comments (3)
qdrant_client/qdrant_remote.py (3)

1974-1987: REST update_vectors: accept grpc.PointVectors (normalize before JSON).

Convert grpc.PointVectors to REST to avoid serialization errors. This mirrors the earlier suggestion.

         else:
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
+            if isinstance(points, list):
+                points = [
+                    (GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
+                    for p in points
+                ]
 
             return self.openapi_client.points_api.update_vectors(
                 collection_name=collection_name,
                 wait=wait,
                 update_vectors=models.UpdateVectors(
                     points=points,
                     shard_key=shard_key_selector,
                     update_filter=update_filter,

2670-2685: UpdateCollection (gRPC): convert metadata whenever provided.

dict-only check can skip valid payloads. Convert if not None.

-            if isinstance(metadata, dict):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)
 
             return self.grpc_collections.Update(
                 grpc.UpdateCollection(
                     collection_name=collection_name,
                     ...
                     timeout=timeout,
-                    metadata=metadata,
+                    metadata=metadata,
                 ),

2799-2819: CreateCollection (gRPC): same metadata conversion issue.

Convert when provided.

-            if isinstance(metadata, dict):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)
 
             create_collection = grpc.CreateCollection(
                 collection_name=collection_name,
                 ...
-                metadata=metadata,
+                metadata=metadata,
             )
🧹 Nitpick comments (4)
qdrant_client/async_qdrant_remote.py (4)

1736-1744: REST update_vectors: convert grpc.PointVectors before JSON.

If callers pass grpc.PointVectors, current path will fail serialization. Normalize to REST models.

         else:
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
+            if isinstance(points, list):
+                points = [
+                    (GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
+                    for p in points
+                ]
             return (
                 await self.openapi_client.points_api.update_vectors(
                     collection_name=collection_name,
                     wait=wait,
                     update_vectors=models.UpdateVectors(
                         points=points, shard_key=shard_key_selector, update_filter=update_filter
                     ),

2408-2410: Metadata conversion guard too narrow.

Use “not None” (or Mapping) instead of dict-only to ensure conversion always happens for provided metadata.

-            if isinstance(metadata, dict):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)

2522-2524: CreateCollection: same metadata conversion issue.

Always convert when provided.

-            if isinstance(metadata, dict):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)

1529-1529: Lint: unused kwargs.

Rename to _kwargs or add “# noqa: ARG002” in the signature to silence Ruff.

-        **kwargs: Any,
+        **_kwargs: Any,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e0bb9a and 977fcf4.

📒 Files selected for processing (2)
  • qdrant_client/async_qdrant_remote.py (19 hunks)
  • qdrant_client/qdrant_remote.py (18 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
qdrant_client/async_qdrant_remote.py (3)
qdrant_client/grpc/points_pb2.pyi (3)
  • ReadConsistency (228-245)
  • models (5151-5151)
  • ShardKeySelector (617-634)
qdrant_client/http/models/models.py (6)
  • Filter (884-894)
  • PointStruct (1972-1975)
  • PointsList (1992-1998)
  • Batch (61-64)
  • PointsBatch (1983-1989)
  • UpdateVectors (3325-3328)
qdrant_client/conversions/conversion.py (11)
  • RestToGrpc (2500-4806)
  • convert_filter (166-182)
  • convert_filter (2502-2522)
  • convert_shard_key_selector (2193-2196)
  • convert_shard_key_selector (4618-4625)
  • convert_read_consistency (1736-1745)
  • convert_read_consistency (4090-4100)
  • convert_point_struct (603-608)
  • convert_point_struct (2796-2801)
  • convert_payload (583-584)
  • convert_payload (2804-2805)
qdrant_client/qdrant_remote.py (4)
qdrant_client/grpc/points_pb2.pyi (5)
  • ReadConsistency (228-245)
  • models (5151-5151)
  • CountPoints (2289-2332)
  • UpsertPoints (638-686)
  • UpdatePointVectors (784-828)
qdrant_client/conversions/conversion.py (10)
  • RestToGrpc (2500-4806)
  • convert_read_consistency (1736-1745)
  • convert_read_consistency (4090-4100)
  • convert_filter (166-182)
  • convert_filter (2502-2522)
  • GrpcToRest (140-2490)
  • convert_update_result (467-471)
  • convert_update_result (2686-2690)
  • convert_payload (583-584)
  • convert_payload (2804-2805)
qdrant_client/async_qdrant_remote.py (2)
  • grpc_points (276-284)
  • update_vectors (1702-1747)
qdrant_client/http/models/models.py (5)
  • Filter (884-894)
  • PointsList (1992-1998)
  • Batch (61-64)
  • PointsBatch (1983-1989)
  • UpdateVectors (3325-3328)
🪛 Ruff (0.14.0)
qdrant_client/async_qdrant_remote.py

1529-1529: Unused method argument: kwargs

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
  • GitHub Check: Python 3.13.x on ubuntu-latest test
  • GitHub Check: Python 3.9.x on ubuntu-latest test
  • GitHub Check: Python 3.10.x on ubuntu-latest test
  • GitHub Check: Python 3.12.x on ubuntu-latest test
  • GitHub Check: Python 3.11.x on ubuntu-latest test

@joein joein force-pushed the update-models-1.16 branch from 977fcf4 to 4a4d40e Compare October 29, 2025 09:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
qdrant_client/grpc/points_pb2.pyi (1)

651-687: Test coverage missing for update_filter on upsert and update_vectors operations.

The .pyi stub, pb2.py runtime (reflection-based), client methods, and HTTP converters are correctly wired for update_filter. However, no integration tests exist for the two critical use cases:

  • upsert() with update_filter: matching points are updated, others inserted
  • update_vectors() with update_filter: only matching points' vectors are updated

Existing tests only cover set_payload() with filters, not these operations. Add tests demonstrating both behaviors and edge cases (e.g., filter excludes all provided points).

♻️ Duplicate comments (12)
qdrant_client/proto/points.proto (1)

69-76: vectors_count still present; align with “remove vectors_count” objective

The legacy vectors_count field persists (even if deprecated). To fully enforce the removal, reserve the field and migrate tests/fixtures to MultiDenseVector.

Apply in proto:

 message Vector {
   repeated float data = 1; // Vector data (flatten for multi vectors), deprecated
   optional SparseIndices indices = 2; // Sparse indices for sparse vectors, deprecated
-  optional uint32 vectors_count = 3; // Number of vectors per multi vector, deprecated
+  reserved 3; // vectors_count removed (legacy multi-vector), do not reuse
   oneof vector {
 message VectorOutput {
   repeated float data = 1; // Vector data (flatten for multi vectors), deprecated
   optional SparseIndices indices = 2; // Sparse indices for sparse vectors, deprecated
-  optional uint32 vectors_count = 3; // Number of vectors per multi vector, deprecated
+  reserved 3; // vectors_count removed (legacy multi-vector), do not reuse
   oneof vector {

Then regenerate stubs and update fixtures to MultiDenseVector (see fixture suggestions below).

#!/bin/bash
rg -nP '\bvectors_count\b' -S

Also applies to: 82-91

qdrant_client/proto/collections.proto (1)

425-425: Collection metadata: add end-to-end coverage with mixed types

Great addition. Ensure REST↔gRPC conversions round-trip ints, floats, bools, nulls, lists, nested maps. Current e2e test uses a simple string only.

See test diff suggestion in tests/test_qdrant_client.py comment.

Also applies to: 438-438, 478-478

tests/test_qdrant_client.py (1)

1170-1185: Widen metadata payload and add update_filter coverage

Expand metadata to mixed types to catch conversion edge cases, and add a small test for update_filter propagation.

Apply:

-        collection_metadata = {"ownership": "Bart Simpson's property"}
+        collection_metadata = {
+            "ownership": "Bart Simpson's property",
+            "year": 1990,
+            "rating": 4.2,
+            "published": True,
+            "notes": None,
+            "tags": ["simpson", "animated"],
+            "nested": {"creator": "Matt Groening", "ids": [1, 2, 3]},
+        }

Optionally, add:

# verify update_filter is accepted and serialized
client.upsert(
    collection_name=COLLECTION_NAME,
    points=[PointStruct(id=999, vector=np.random.rand(DIM).tolist())],
    update_filter=Filter(must=[FieldCondition(key="test", match=MatchValue(value="value"))]),
)
tests/conversions/fixtures.py (1)

404-408: Migrate away from legacy vectors_count in fixtures

Replace flattened data + vectors_count with MultiDenseVector to align with deprecation/removal.

Apply:

-multi_vector = grpc.Vectors(
-    vector=grpc.Vector(data=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], vectors_count=2)
-)
+multi_vector = grpc.Vectors(
+    vector=grpc.Vector(
+        multi_dense=grpc.MultiDenseVector(
+            vectors=[
+                grpc.DenseVector(data=[1.0, 2.0, 3.0, 4.0]),
+                grpc.DenseVector(data=[5.0, 6.0, 7.0, 8.0]),
+            ]
+        )
+    )
+)
-multi_vector_output = grpc.VectorsOutput(
-    vector=grpc.VectorOutput(data=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], vectors_count=2)
-)
+multi_vector_output = grpc.VectorsOutput(
+    vector=grpc.VectorOutput(
+        multi_dense=grpc.MultiDenseVector(
+            vectors=[
+                grpc.DenseVector(data=[1.0, 2.0, 3.0, 4.0]),
+                grpc.DenseVector(data=[5.0, 6.0, 7.0, 8.0]),
+            ]
+        )
+    )
+)
-            "multi": grpc.Vector(data=[1.0, 2.0, 3.0, 4.0], vectors_count=2),
+            "multi": grpc.Vector(
+                multi_dense=grpc.MultiDenseVector(
+                    vectors=[
+                        grpc.DenseVector(data=[1.0, 2.0, 3.0, 4.0]),
+                        grpc.DenseVector(data=[5.0, 6.0, 7.0, 8.0]),
+                    ]
+                )
+            ),
#!/bin/bash
rg -nP '\bvectors_count\b' tests/conversions/fixtures.py

Also applies to: 445-446, 891-895

qdrant_client/conversions/conversion.py (3)

267-267: Critical: incorrect None-check on protobuf map field

Protobuf map fields are never None; they're always initialized. The check model.metadata is not None will always evaluate to True. Use an emptiness check instead.

Apply this diff:

-            metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
+            metadata=cls.convert_payload(model.metadata) if len(model.metadata) > 0 else None,

Based on past review comments.


794-794: Critical: incorrect None-check on protobuf map field

Same issue as Line 267. Protobuf maps are never None. Use an emptiness check.

Apply this diff:

-            metadata=(cls.convert_payload(model.metadata) if model.metadata is not None else None),
+            metadata=cls.convert_payload(model.metadata) if len(model.metadata) > 0 else None,

Based on past review comments.


3656-3658: Preserve optional k when None

When model.rrf.k is None, avoid setting the protobuf optional scalar to None. Only populate the field when a value is present.

Apply this diff:

         if isinstance(model, rest.RrfQuery):
-            return grpc.Query(rrf=grpc.Rrf(k=model.rrf.k))
+            rrf = grpc.Rrf()
+            if model.rrf.k is not None:
+                rrf.k = model.rrf.k
+            return grpc.Query(rrf=rrf)

Based on past review comments.

qdrant_client/async_qdrant_remote.py (1)

2438-2439: Convert metadata for gRPC whenever provided (guard too narrow).

Use a presence check, not isinstance(dict), to ensure all Mapping-like inputs (or alias types) get converted.

Apply:

@@ def update_collection(...):
-            if isinstance(metadata, dict):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)

@@ def create_collection(...):
-            if isinstance(metadata, dict):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)

This mirrors the sync client fix and avoids silent skips when metadata isn’t a plain dict.

Also applies to: 2461-2462, 2474-2475, 2543-2544, 2575-2576, 2592-2593

qdrant_client/async_qdrant_client.py (1)

1536-1536: update_filter must be Filter (not Payload) in async client signatures.

Public typing mismatch vs behavior and other clients. Keep API consistent with AsyncQdrantRemote/HTTP models.

Apply:

@@ async def upsert(...):
-        update_filter: Optional[types.Payload] = None,
+        update_filter: Optional[types.Filter] = None,

@@ async def update_vectors(...):
-        update_filter: Optional[types.Payload] = None,
+        update_filter: Optional[types.Filter] = None,
#!/bin/bash
# Verify no async/sync client still uses Payload for update_filter
rg -nP -C2 $'update_filter:\s*Optional\[types\.(Payload|Filter)\]' qdrant_client | sed -n '1,200p'

Also applies to: 1611-1611, 1562-1562, 1640-1642, 1600-1600, 1659-1659

qdrant_client/qdrant_remote.py (1)

2034-2047: Handle gRPC PointVectors in REST path

The REST path will fail JSON serialization if callers pass gRPC PointVectors objects. Add conversion logic similar to the upsert REST path.

Apply this diff to handle gRPC types:

         else:
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
+            
+            if isinstance(points, list):
+                points = [
+                    (
+                        GrpcToRest.convert_point_vectors(point)
+                        if isinstance(point, grpc.PointVectors)
+                        else point
+                    )
+                    for point in points
+                ]
 
             return self.openapi_client.points_api.update_vectors(
qdrant_client/embed/_inspection_cache.py (1)

444-444: Fix typographical error in description.

Missing space before "Defaults" in the description text. This issue was previously identified but remains unfixed.

Apply this diff to fix the typo:

-                "description": "The midpoint of the decay. Should be between 0 and 1.Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
+                "description": "The midpoint of the decay. Should be between 0 and 1. Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
qdrant_client/grpc/collections_pb2.pyi (1)

11-11: Regression: Import issue reappeared after file regeneration.

The past review comment on this line was marked as addressed in commit 556202e, but the absolute import import json_with_int_pb2 has reappeared. Since this is a generated file (@generated by mypy-protobuf), the previous fix was likely lost during regeneration.

To make this fix permanent, apply it at the generator level (in the mypy-protobuf configuration or proto definitions) rather than manually editing this file.

Expected:

from . import json_with_int_pb2
🧹 Nitpick comments (10)
qdrant_client/http/api/distributed_api.py (1)

156-174: Timeout plumbed correctly; please document units and edge cases

Looks good. Consider clarifying in docstring/API docs that timeout is in seconds and must be non-negative. Add a small test covering force-only, timeout-only, and both together. If this module is generated, put the note in user-facing docs/changelog instead of editing here.

qdrant_client/proto/points.proto (2)

138-139: Specify exact semantics for update_filter (ID exists but doesn’t match?)

Ambiguity: when an ID exists but doesn’t satisfy update_filter, is it a no-op, error, or reinsert attempt (which would conflict on ID)? Please clarify in comments and server behavior; align across UpsertPoints, UpdatePointVectors, and PointsUpdateOperation variants.

Also applies to: 166-167, 805-806, 827-828


568-571: Two RRF paths (Fusion.RRF vs Query.rrf). Deprecate one for clarity.

Having both enum Fusion.RRF (implicit defaults) and Query.rrf (parameterized) invites confusion. Recommend deprecating Fusion.RRF in comments and favor Query.rrf, keeping enum for backward compatibility.

Also applies to: 672-676, 688-689

qdrant_client/proto/collections.proto (1)

202-209: HNSW copy_vectors: document validation

Field requires quantization enabled and disallows multi-vectors. Suggest adding “Requires quantization; not supported with MultiDenseVector” to the comment to avoid misconfig.

tests/conversions/fixtures.py (1)

38-39: Include text_any in Match fixtures for conversion coverage

You added match_text_any but it’s not in fixtures["Match"]. Add it so conversion tests cover the new variant.

-    "Match": [match_keyword, match_integer, match_bool, match_text],
+    "Match": [match_keyword, match_integer, match_bool, match_text, match_phrase, match_text_any],

Also applies to: 52-53, 129-130, 1638-1643

qdrant_client/http/models/models.py (2)

359-361: Consider adding extra="forbid" for consistency.

The CollectionWarning model is correctly defined, but unlike most other models in this file (e.g., Bm25Config, FieldCondition, Filter), it doesn't use extra="forbid". For consistency and to prevent unexpected fields, consider:

-class CollectionWarning(BaseModel):
+class CollectionWarning(BaseModel, extra="forbid"):
     message: str = Field(..., description="Warning message")

1941-1947: Consider adding extra="forbid" for consistency.

The PeerMetadata model is correctly defined with the version field, but like CollectionWarning, it lacks extra="forbid". For consistency with most models in this file and to prevent unexpected fields:

-class PeerMetadata(BaseModel):
+class PeerMetadata(BaseModel, extra="forbid"):
     """
     Metadata describing extra properties for each peer
     """
     
     version: str = Field(..., description="Peer Qdrant version")
qdrant_client/async_qdrant_remote.py (1)

1580-1599: Added read consistency to count: LGTM.

Correctly converts and forwards consistency to both gRPC and REST.

Consider dropping unused **kwargs or assert no extras for parity with other methods.

Also applies to: 1612-1613

qdrant_client/qdrant_remote.py (2)

2691-2780: Consider using Mapping for metadata type check

The current isinstance(metadata, dict) check works but could be more robust.

Since Mapping is already imported (line 11), consider using it for broader type coverage:

-            if isinstance(metadata, dict):
+            if isinstance(metadata, Mapping):
                 metadata = RestToGrpc.convert_payload(metadata)

Alternatively, if metadata is always expected to be a dict when provided (per the type hint), you could simplify to:

-            if isinstance(metadata, dict):
+            if metadata is not None:
                 metadata = RestToGrpc.convert_payload(metadata)

2797-2922: Consider using Mapping for metadata type check (same pattern)

Same suggestion as in update_collection for consistency:

-            if isinstance(metadata, dict):
+            if isinstance(metadata, Mapping):
                 metadata = RestToGrpc.convert_payload(metadata)

Or simplify if metadata is always a dict when provided:

-            if isinstance(metadata, dict):
+            if metadata is not None:
                 metadata = RestToGrpc.convert_payload(metadata)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 977fcf4 and 4a4d40e.

📒 Files selected for processing (18)
  • qdrant_client/async_qdrant_client.py (15 hunks)
  • qdrant_client/async_qdrant_remote.py (19 hunks)
  • qdrant_client/conversions/conversion.py (21 hunks)
  • qdrant_client/embed/_inspection_cache.py (24 hunks)
  • qdrant_client/grpc/collections_pb2.py (7 hunks)
  • qdrant_client/grpc/collections_pb2.pyi (27 hunks)
  • qdrant_client/grpc/points_pb2.pyi (18 hunks)
  • qdrant_client/http/api/distributed_api.py (4 hunks)
  • qdrant_client/http/api/points_api.py (10 hunks)
  • qdrant_client/http/api/search_api.py (3 hunks)
  • qdrant_client/http/models/models.py (31 hunks)
  • qdrant_client/local/local_collection.py (0 hunks)
  • qdrant_client/proto/collections.proto (11 hunks)
  • qdrant_client/proto/points.proto (10 hunks)
  • qdrant_client/qdrant_client.py (15 hunks)
  • qdrant_client/qdrant_remote.py (18 hunks)
  • tests/conversions/fixtures.py (11 hunks)
  • tests/test_qdrant_client.py (1 hunks)
💤 Files with no reviewable changes (1)
  • qdrant_client/local/local_collection.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • qdrant_client/grpc/collections_pb2.py
  • qdrant_client/http/api/points_api.py
  • qdrant_client/http/api/search_api.py
  • qdrant_client/qdrant_client.py
🧰 Additional context used
🧬 Code graph analysis (9)
tests/test_qdrant_client.py (3)
qdrant_client/http/models/models.py (2)
  • VectorParams (3412-3437)
  • Distance (728-739)
tests/utils.py (1)
  • read_version (9-25)
qdrant_client/qdrant_client.py (2)
  • create_collection (2333-2424)
  • get_collection (2231-2242)
qdrant_client/http/api/distributed_api.py (1)
qdrant_client/http/models/models.py (1)
  • InlineResponse200 (1215-1219)
qdrant_client/conversions/conversion.py (3)
qdrant_client/grpc/collections_pb2.pyi (24)
  • metadata (1527-1528)
  • metadata (1638-1639)
  • metadata (1855-1856)
  • HasField (375-375)
  • HasField (411-411)
  • HasField (439-439)
  • HasField (471-471)
  • HasField (501-501)
  • HasField (522-522)
  • HasField (544-544)
  • HasField (570-570)
  • HasField (658-658)
  • HasField (702-702)
  • HasField (750-750)
  • HasField (847-847)
  • HasField (892-892)
  • HasField (922-922)
  • HasField (1025-1025)
  • HasField (1067-1067)
  • shard_key (2520-2521)
  • shard_key (2550-2551)
  • shard_key (2604-2604)
  • shard_key (2803-2804)
  • shard_key (2834-2835)
qdrant_client/http/models/models.py (5)
  • MatchTextAny (1577-1582)
  • RrfQuery (2452-2453)
  • Rrf (2444-2449)
  • PointsList (1992-1998)
  • UpdateVectors (3325-3328)
qdrant_client/grpc/points_pb2.pyi (39)
  • rrf (2811-2812)
  • Rrf (2753-2768)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • shard_key (3842-3843)
  • shard_key (4206-4207)
  • shard_key_selector (662-663)
  • shard_key_selector (709-710)
  • shard_key_selector (756-757)
  • shard_key_selector (804-805)
  • shard_key_selector (877-878)
  • shard_key_selector (941-942)
  • shard_key_selector (994-995)
  • shard_key_selector (1038-1039)
  • shard_key_selector (1478-1479)
  • shard_key_selector (1639-1640)
  • shard_key_selector (1771-1772)
  • shard_key_selector (1821-1822)
  • shard_key_selector (1905-1906)
  • shard_key_selector (2058-2059)
  • shard_key_selector (2217-2218)
  • shard_key_selector (2309-2310)
  • UpdateVectors (3551-3578)
  • Match (4718-4771)
  • Query (2772-2829)
  • PointsUpdateOperation (3390-3694)
  • PointStructList (3393-3419)
qdrant_client/grpc/points_pb2.pyi (1)
qdrant_client/http/models/models.py (2)
  • Fusion (920-929)
  • Rrf (2444-2449)
tests/conversions/fixtures.py (4)
qdrant_client/grpc/points_pb2.pyi (16)
  • Match (4718-4771)
  • FieldCondition (4656-4714)
  • match (4671-4672)
  • Condition (4537-4574)
  • field (4548-4548)
  • params (1461-1462)
  • params (1617-1618)
  • params (1877-1878)
  • params (2025-2026)
  • params (2199-2200)
  • params (2856-2857)
  • params (2928-2929)
  • params (3069-3070)
  • Query (2772-2829)
  • rrf (2811-2812)
  • Rrf (2753-2768)
qdrant_client/http/models/models.py (4)
  • FieldCondition (860-881)
  • StrictModeConfig (3091-3136)
  • CollectionConfig (222-242)
  • Rrf (2444-2449)
qdrant_client/grpc/collections_pb2.pyi (21)
  • StrictModeConfig (1227-1351)
  • metadata (1527-1528)
  • metadata (1638-1639)
  • metadata (1855-1856)
  • CollectionConfig (1809-1873)
  • params (492-492)
  • params (513-513)
  • params (1620-1621)
  • params (1837-1838)
  • params (2219-2220)
  • hnsw_config (352-353)
  • hnsw_config (397-398)
  • hnsw_config (1492-1493)
  • hnsw_config (1623-1624)
  • hnsw_config (1840-1841)
  • optimizer_config (1843-1844)
  • wal_config (1495-1496)
  • wal_config (1846-1847)
  • strict_mode_config (1524-1525)
  • strict_mode_config (1635-1636)
  • strict_mode_config (1852-1853)
qdrant_client/grpc/json_with_int_pb2.pyi (1)
  • Value (84-131)
qdrant_client/http/models/models.py (2)
qdrant_client/grpc/points_pb2.pyi (20)
  • b (3343-3344)
  • options (318-319)
  • options (360-361)
  • options (403-404)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • Rrf (2753-2768)
  • rrf (2811-2812)
qdrant_client/grpc/collections_pb2.pyi (9)
  • stopwords (2021-2022)
  • metadata (1527-1528)
  • metadata (1638-1639)
  • metadata (1855-1856)
  • warnings (2285-2286)
  • CollectionWarning (773-783)
  • languages (1984-1985)
  • custom (1987-1988)
  • WalConfigDiff (903-929)
qdrant_client/async_qdrant_remote.py (5)
qdrant_client/http/models/models.py (6)
  • Filter (884-894)
  • PointStruct (1972-1975)
  • PointsList (1992-1998)
  • Batch (61-64)
  • PointsBatch (1983-1989)
  • UpdateVectors (3325-3328)
qdrant_client/conversions/conversion.py (10)
  • RestToGrpc (2500-4806)
  • convert_filter (166-182)
  • convert_filter (2502-2522)
  • convert_read_consistency (1736-1745)
  • convert_read_consistency (4090-4100)
  • GrpcToRest (140-2490)
  • convert_point_struct (603-608)
  • convert_point_struct (2796-2801)
  • convert_payload (583-584)
  • convert_payload (2804-2805)
qdrant_client/conversions/common_types.py (1)
  • get_args_subscribed (33-45)
qdrant_client/qdrant_remote.py (1)
  • update_vectors (1999-2047)
qdrant_client/async_qdrant_client.py (1)
  • update_vectors (1604-1660)
qdrant_client/qdrant_remote.py (5)
qdrant_client/grpc/points_pb2.pyi (5)
  • ReadConsistency (228-245)
  • models (5151-5151)
  • CountPoints (2289-2332)
  • UpsertPoints (638-686)
  • UpdatePointVectors (784-828)
qdrant_client/conversions/conversion.py (10)
  • RestToGrpc (2500-4806)
  • convert_read_consistency (1736-1745)
  • convert_read_consistency (4090-4100)
  • convert_filter (166-182)
  • convert_filter (2502-2522)
  • GrpcToRest (140-2490)
  • convert_update_result (467-471)
  • convert_update_result (2686-2690)
  • convert_payload (583-584)
  • convert_payload (2804-2805)
qdrant_client/async_qdrant_remote.py (2)
  • grpc_points (325-334)
  • update_vectors (1754-1799)
qdrant_client/qdrant_client.py (2)
  • grpc_points (197-206)
  • update_vectors (1656-1714)
qdrant_client/http/models/models.py (5)
  • Filter (884-894)
  • PointsList (1992-1998)
  • Batch (61-64)
  • PointsBatch (1983-1989)
  • UpdateVectors (3325-3328)
qdrant_client/grpc/collections_pb2.pyi (2)
qdrant_client/http/models/models.py (1)
  • CollectionWarning (359-360)
qdrant_client/grpc/json_with_int_pb2.pyi (2)
  • value (61-61)
  • Value (84-131)
🪛 Buf (1.59.0)
qdrant_client/proto/collections.proto

6-6: import "json_with_int.proto": file does not exist

(COMPILE)

🪛 Ruff (0.14.2)
qdrant_client/async_qdrant_remote.py

1581-1581: Unused method argument: kwargs

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
  • GitHub Check: Python 3.10.x on ubuntu-latest test
  • GitHub Check: Python 3.11.x on ubuntu-latest test
  • GitHub Check: Python 3.13.x on ubuntu-latest test
  • GitHub Check: Python 3.12.x on ubuntu-latest test
  • GitHub Check: Python 3.9.x on ubuntu-latest test
🔇 Additional comments (51)
qdrant_client/http/api/distributed_api.py (2)

263-276: Async API propagation LGTM
Parameter is forwarded correctly to the builder.


340-353: Sync API propagation LGTM
Parameter is forwarded correctly to the builder.

qdrant_client/grpc/points_pb2.pyi (3)

4411-4428: FacetResponse usage field aligns with REST models

Addition of usage mirrors most other response messages; REST InlineResponse types similarly expose usage.


4730-4772: Based on the verification of the provided search results, the review comment can be resolved:

All text_any end-to-end support is correctly implemented

The evidence confirms:

  1. HTTP/OpenAPI models: HTTP model at qdrant_client/http/models/models.py includes text_any field with description "Full-text match of at least one token of the string"

  2. Converters mapping (bidirectional):

    • REST to gRPC conversion at conversion.py lines 681-682 handles text_any via rest.MatchTextAny
    • gRPC to REST conversion at conversion.py lines 3056-3057 converts rest.MatchTextAny models
  3. Oneof enforcement: The WhichOneof method signature shows text_any is correctly part of the match_value oneof, with the return type including all variants including "text_any", which protobuf enforces at the protocol level—only one variant can be set at a time, ensuring field exclusivity regardless of whether both text and text_any are supplied.


2753-2830: Review comment is incorrect — no changes needed.

The proto definition shows fusion and rrf are both part of the same oneof variant group, making them mutually exclusive. They cannot both be set simultaneously, so the review's concern about documenting "how both should be used together" or establishing "precedence if both set" is based on a false premise.

The semantic distinction is clear and correct:

  • Fusion.RRF (enum): RRF with default parameters
  • Query.rrf (Rrf message): RRF with custom k parameter

This design is sound—when users need RRF with a custom k, they use Query.rrf; otherwise, they use Fusion.RRF. HTTP models, gRPC types, and conversion logic are all properly aligned. No documentation or alignment issues exist.

qdrant_client/proto/points.proto (3)

1036-1037: FacetResponse.usage addition: LGTM

Useful for telemetry/billing; no concerns.


1124-1125: Match.text_any addition: LGTM

Good expansion of text matching. Ensure HTTP models and conversions handle it consistently (tokenization vs Match.text/phrase).


8-9: ****

The file json_with_int.proto is present in the repository at qdrant_client/proto/json_with_int.proto, and the import path in points.proto is correct. Buf and CI will resolve this import successfully—there is no blocking issue.

Likely an incorrect or invalid review comment.

tests/conversions/fixtures.py (1)

371-388: New fixtures (metadata, RRF, copy_vectors): LGTM

Good coverage of metadata map, parameterized RRF (k and default), and HNSW copy_vectors flag.

Also applies to: 1512-1515, 1741-1744, 297-297

qdrant_client/proto/collections.proto (1)

6-7: The file exists and the import is valid—this review comment is incorrect.

The json_with_int.proto file is present at qdrant_client/proto/json_with_int.proto with proper content. The import statement in collections.proto resolves correctly. No blocking issue exists.

Likely an incorrect or invalid review comment.

qdrant_client/http/models/models.py (19)

93-133: LGTM: Well-structured BM25 configuration model.

The Bm25Config class is well-defined with appropriate optional fields, sensible defaults, and clear documentation. The field types align with expected text search parameters.


205-205: LGTM: Appropriate peer metadata field addition.

The peer_metadata field correctly uses Optional[Dict[str, "PeerMetadata"]] to represent metadata for cluster peers, consistent with the pattern used elsewhere in the file.


239-242: LGTM: Collection metadata field properly added.

The metadata field is correctly typed as Optional["Payload"] (which is Dict[str, Any]), allowing arbitrary JSON metadata for collections. This aligns with the gRPC definition and provides good flexibility for storing application-specific data.


277-279: LGTM: Warnings field appropriately integrated.

The warnings field correctly represents collection warnings as Optional[List["CollectionWarning"]], matching the gRPC definition and allowing collections to report issues when present.


757-767: LGTM: Document options enhanced with structured BM25 config.

The options field type change from Optional[Dict[str, Any]] to Optional["DocumentOptions"] (which is Union[Dict[str, Any], Bm25Config]) maintains backward compatibility while allowing structured BM25 configuration. The union includes the original dict type, ensuring existing code continues to work.


855-857: LGTM: Feature flag properly added.

The appendable_quantization feature flag is correctly defined with an appropriate default value of False and clear documentation.


920-930: LGTM: Fusion documentation accurately updated.

The docstring correctly documents the available fusion algorithms including the newly added RRF (Reciprocal Rank Fusion) option.


1078-1081: LGTM: Enhanced HNSW threshold documentation.

The updated full_scan_threshold description provides clearer guidance on when the query planner switches between full-scan and HNSW index traversal, improving developer understanding.


1094-1097: LGTM: Copy vectors optimization properly documented.

The copy_vectors field is well-defined with clear documentation explaining the performance tradeoffs, prerequisites (quantization), and limitations (no multi-vector support). The optional boolean type is appropriate.


1577-1583: LGTM: Text-any match variant properly implemented.

The MatchTextAny class follows the established pattern for match conditions and clearly documents its behavior (matching at least one token). The extra="forbid" setting ensures strict validation.


1986-1989: LGTM: Update filter enables conditional upsert.

The update_filter field in PointsBatch correctly implements conditional upsert functionality, allowing points to be updated only if they match the filter, with non-matching points being inserted. This aligns with the gRPC definition.


2444-2454: LGTM: RRF query models properly implemented.

The Rrf and RrfQuery classes correctly implement Reciprocal Rank Fusion functionality, matching the gRPC definition. The structure follows the established pattern for query types, with RrfQuery wrapping the Rrf parameters.


3082-3088: LGTM: Stopwords configuration properly defined.

The StopwordsSet fields correctly allow combining predefined language stopwords with custom stopwords. The type annotations and descriptions clearly explain the merging behavior.


3103-3136: LGTM: Strict mode constraints properly expanded.

The additional strict mode configuration fields (search_max_hnsw_ef, search_allow_exact, multivector_config, sparse_config, max_payload_index_count) are correctly typed and documented, providing fine-grained control over collection behavior and resource limits.


3298-3301: LGTM: Collection metadata update properly documented.

The metadata field in UpdateCollection correctly allows merging metadata with existing values. The description helpfully explains both update and removal semantics.


3518-3518: LGTM: WAL retention configuration properly added.

The wal_retain_closed field is correctly added to both WalConfig (with default=1) and WalConfigDiff (with default=None), allowing control over closed WAL segment retention. The default value of 1 is sensible for maintaining recent history.


3602-3605: LGTM: Document options type alias properly defined.

The DocumentOptions type alias correctly provides flexibility between unstructured dictionary options and structured Bm25Config, maintaining backward compatibility while enabling better type safety.


3623-3630: LGTM: Match union properly updated.

The Match union correctly includes MatchTextAny, completing the integration of the text-any matching functionality. The ordering groups related match types logically.


3691-3701: LGTM: Query union properly updated.

The Query union correctly includes RrfQuery, completing the integration of Reciprocal Rank Fusion query functionality. The type is appropriately positioned in the union.

qdrant_client/async_qdrant_client.py (1)

2196-2196: Metadata parameter propagation: LGTM.

Signatures/docs/pass-through for metadata are consistent. No changes needed here.

If you also updated the async/sync remotes for gRPC conversion, ensure conversion occurs whenever metadata is provided (see related comment in async_qdrant_remote.py).

Also applies to: 2213-2213, 2234-2235, 2276-2276, 2345-2346, 2366-2367

qdrant_client/async_qdrant_remote.py (3)

1761-1762: update_filter in update_vectors: wiring looks correct across gRPC/REST.

Consistent with models.UpdateVectors.update_filter and server expectations.

Also applies to: 1771-1772, 1780-1781, 1788-1790, 1795-1796


2650-2651: recreate_collection: metadata pass-through is consistent.

Delegation to create_collection preserves metadata; good.

Also applies to: 2670-2671


1680-1681: update_filter wiring is correct across gRPC/REST boundaries.

Verification confirms:

  • gRPC path: update_filter converted and passed directly to grpc.UpsertPoints (lines 1709–1720)
  • REST path: update_filter injected into PointsList/PointsBatch models before calling upsert_points (lines 1727–1742)
  • Body models carry the field; type conversions work bidirectionally. No issues found.
qdrant_client/qdrant_remote.py (4)

1806-1846: LGTM: consistency parameter properly integrated

The consistency parameter is correctly added to the count method with proper REST↔gRPC conversions on both paths.


1907-1997: LGTM: update_filter properly integrated in upsert

The update_filter parameter is correctly added with proper conversions and propagation through both gRPC and REST paths to PointsList and PointsBatch.


2006-2033: LGTM: update_filter properly integrated in gRPC path

The update_filter parameter is correctly added with proper conversions and propagation through the gRPC path.


2924-2963: LGTM: metadata parameter correctly forwarded

The recreate_collection method properly forwards the metadata parameter to create_collection.

qdrant_client/embed/_inspection_cache.py (6)

9-9: LGTM! New cache entries align with added types.

The additions to CACHE_STR_PATH for Bm25Config, MatchTextAny, Rrf, and RrfQuery are consistent with the new type definitions added later in the file.

Also applies to: 83-83, 209-210


590-590: LGTM! MatchTextAny properly integrated.

The new MatchTextAny type is correctly added to the FieldCondition.match union and follows the established pattern for match types. The description clearly explains its behavior as matching at least one token.

Also applies to: 1061-1074


1452-1521: LGTM! BM25 configuration is well-structured.

The BM25 support additions are comprehensive and well-designed:

  • Bm25Config includes all necessary parameters with sensible defaults
  • Supporting types (Language, Snowball*, StopwordsSet, TokenizerType) are properly defined
  • Integration into Document.options maintains backward compatibility via anyOf
  • Field descriptions clearly explain parameters and their impact

Also applies to: 1537-1541, 1599-1719


3047-3050: LGTM! Reciprocal Rank Fusion properly implemented.

The RRF (Reciprocal Rank Fusion) additions are well-integrated:

  • Fusion enum description now documents both RRF and DBSF algorithms
  • Rrf and RrfQuery types follow established patterns
  • RrfQuery is correctly added to query unions in Prefetch and QueryRequest
  • The k parameter description is clear

Also applies to: 3447-3447, 3570-3590, 3677-3677


1974-1979: LGTM! Configuration updates are clear and well-documented.

The configuration additions and updates are properly implemented:

  • HnswConfigDiff.copy_vectors clearly explains the search speed vs. disk usage trade-off
  • StrictModeConfig descriptions for search_max_hnsw_ef and search_allow_exact are more concise
  • max_payload_index_count is properly added with clear description
  • WalConfigDiff.wal_retain_closed has clear description of its purpose

Also applies to: 2213-2220, 2285-2298, 2300-2305, 2397-2402


4245-4249: LGTM! update_filter consistently added across point operations.

The update_filter field is correctly and consistently added to PointsBatch, PointsList, and UpdateVectors:

  • Uniform type definition (Filter | null)
  • Clear descriptions explaining conditional update/insert behavior
  • Proper default value (None)
  • Enables filtering which points should be updated vs. inserted

Also applies to: 4278-4282, 4318-4322

qdrant_client/grpc/collections_pb2.pyi (8)

773-785: LGTM! CollectionWarning class added correctly.

The new CollectionWarning message type is well-formed and consistent with the protobuf patterns used throughout this file. It aligns with the corresponding HTTP model definition.


796-850: LGTM! HnswConfigDiff enhancements are well-documented.

The addition of the copy_vectors field and the clarified full_scan_threshold documentation improve the API. The implementation correctly follows the optional field pattern used throughout the file.


908-929: LGTM! WAL retention configuration added correctly.

The wal_retain_closed field addition is consistent with the existing WAL configuration pattern and properly typed as an optional integer.


1248-1351: LGTM! StrictModeConfig enhancements improve clarity.

The addition of max_payload_index_count and the improved docstrings make the strict mode configuration more understandable. The deprecation note for max_segment_size is helpful for users.


1456-1550: LGTM! Collection metadata support added correctly.

The nested MetadataEntry class and metadata field on CreateCollection follow the established protobuf map pattern. The MessageMap[str, json_with_int_pb2.Value] type correctly represents arbitrary JSON metadata.


1585-1655: LGTM! UpdateCollection metadata follows the same pattern.

The metadata implementation is consistent with CreateCollection. The docstring correctly notes that metadata will be merged, which is important for update semantics.


1812-1869: LGTM! CollectionConfig metadata completes the metadata feature.

The metadata field is consistently implemented across CreateCollection, UpdateCollection, and CollectionConfig, providing a complete metadata API.


2266-2300: LGTM! Collection warnings properly integrated.

The warnings field on CollectionInfo correctly uses RepeatedCompositeFieldContainer[CollectionWarning] for a list of warnings. The method signatures properly handle repeated fields (excluded from HasField, included in ClearField).

class UpdateVectors(BaseModel, extra="forbid"):
points: List["PointVectors"] = Field(..., description="Points with named vectors")
shard_key: Optional["ShardKeySelector"] = Field(default=None, description="")
update_filter: Optional["Filter"] = Field(default=None, description="")
Copy link

@coderabbitai coderabbitai bot Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add description for update_filter field.

The update_filter field is correctly typed but lacks a description. For consistency with PointsBatch.update_filter and PointsList.update_filter, add a description:

-    update_filter: Optional["Filter"] = Field(default=None, description="")
+    update_filter: Optional["Filter"] = Field(
+        default=None, 
+        description="If specified, only points that match this filter will be updated"
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
update_filter: Optional["Filter"] = Field(default=None, description="")
update_filter: Optional["Filter"] = Field(
default=None,
description="If specified, only points that match this filter will be updated"
)
🤖 Prompt for AI Agents
In qdrant_client/http/models/models.py around line 3328, the Field for
update_filter currently has an empty description; add a descriptive string
matching the style used by PointsBatch.update_filter and
PointsList.update_filter that explains this optional Filter narrows which points
are updated (e.g., "Optional filter to select which points the update applies
to"). Update the Field(...) call to include that description text.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

models.py is autogenerated, I can't fix it

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joein, understood! Since models.py is autogenerated, this change would need to be made in the source (likely the OpenAPI spec or proto definitions in the qdrant core) rather than here. No action needed on your end for this PR. Thanks for clarifying!

@joein joein requested a review from tbung October 29, 2025 10:04
joein added 4 commits October 31, 2025 18:06
* new: direct uuid support

* tests: add uuid tests

* fix: update inspection cache
* new: add collection metadata and tests to local mode

* fix: regen async client
* new: implement parametrized rrf in local mode

* refactoring: use a variable for a magic value

* fix: adjust conversion according to AI
* new: add missing update_filter, implement it in local mode

* fix: fix type hint, fix update operation, fix rest uploader, add tests

* fix: fix update filter is None case

* fix: mypy was not a good boy
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
qdrant_client/async_qdrant_client.py (1)

1536-1563: Fix update_filter type annotation in upsert methods

The update_filter parameter on upsert methods is incorrectly typed as Optional[types.Payload] but should be Optional[types.Filter] to match the update_vectors implementation and all remote client implementations.

  • qdrant_client/qdrant_client.py line 1582
  • qdrant_client/async_qdrant_client.py line 1536
qdrant_client/local/async_qdrant_local.py (1)

894-911: Guard against None when merging collection metadata

Async local collections have the same problem: _collection.config.metadata defaults to None, so calling .update(...) directly crashes on the first metadata write. Mirror the sync fix by instantiating the dict when missing before merging new values.

qdrant_client/qdrant_remote.py (2)

1908-1998: Bug: update_filter lost when points is already PointsList/PointsBatch (REST path).

If callers pass models.PointsList/PointsBatch and also provide update_filter to this method, the filter isn’t injected into the body. Inject it when missing.

         else:
+            # If caller already built a Points* container, inject update_filter when provided
+            if isinstance(points, models.PointsList) and update_filter is not None and points.update_filter is None:
+                points.update_filter = update_filter
+            if isinstance(points, models.PointsBatch) and update_filter is not None and points.update_filter is None:
+                points.update_filter = update_filter
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
 
             if isinstance(points, list):
                 points = [
                     (
                         GrpcToRest.convert_point_struct(point)
                         if isinstance(point, grpc.PointStruct)
                         else point
                     )
                     for point in points
                 ]
 
                 points = models.PointsList(
                     points=points, shard_key=shard_key_selector, update_filter=update_filter
                 )
 
             if isinstance(points, models.Batch):
                 points = models.PointsBatch(
                     batch=points, shard_key=shard_key_selector, update_filter=update_filter
                 )

2982-3016: Uploader kwargs now include update_filter — update_filter is passed but never used.

The issue is confirmed. Both upload_batch() and upload_batch_grpc() functions do NOT accept update_filter. Unlike shard_key_selector which is explicitly named in constructors and forwarded to these functions, update_filter would be bundled into **kwargs and silently ignored.

To fix:

  1. Add update_filter: Optional[UpdateFilter] = None as explicit parameter in RestBatchUploader.__init__() and GrpcBatchUploader.__init__()
  2. Extract and store it (e.g., self._update_filter = update_filter)
  3. Add update_filter parameter to upload_batch() and upload_batch_grpc() function signatures
  4. Forward it to the actual upsert API calls: openapi_client.points_api.upsert_points(...) and points_client.Upsert(...)
qdrant_client/async_qdrant_remote.py (3)

1674-1752: Bug: update_filter lost when points is already PointsList/PointsBatch (REST path).

Same as sync: inject update_filter into existing models.PointsList/PointsBatch before calling REST.

         else:
+            # If caller already built a Points* container, inject update_filter when provided
+            if isinstance(points, models.PointsList) and update_filter is not None and points.update_filter is None:
+                points.update_filter = update_filter
+            if isinstance(points, models.PointsBatch) and update_filter is not None and points.update_filter is None:
+                points.update_filter = update_filter
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
             if isinstance(points, list):
                 points = [
                     GrpcToRest.convert_point_struct(point)
                     if isinstance(point, grpc.PointStruct)
                     else point
                     for point in points
                 ]
                 points = models.PointsList(
                     points=points, shard_key=shard_key_selector, update_filter=update_filter
                 )
             if isinstance(points, models.Batch):
                 points = models.PointsBatch(
                     batch=points, shard_key=shard_key_selector, update_filter=update_filter
                 )

1755-1800: Async REST update_vectors should normalize grpc.PointVectors.

Convert grpc.PointVectors to REST equivalents before building models.UpdateVectors.

         else:
+            # Normalize possible gRPC PointVectors to REST models for JSON payload
+            if isinstance(points, list):
+                points = [
+                    (GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
+                    for p in points
+                ]
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
             return (
                 await self.openapi_client.points_api.update_vectors(
                     collection_name=collection_name,
                     wait=wait,
                     update_vectors=models.UpdateVectors(
-                        points=points, shard_key=shard_key_selector, update_filter=update_filter
+                        points=points, shard_key=shard_key_selector, update_filter=update_filter
                     ),
                     ordering=ordering,
                 )
             ).result

2680-2724: Update_filter is passed to uploaders but not implemented — requires refactoring.

Verification confirms the issue: update_filter is passed to RestBatchUploader and GrpcBatchUploader constructors, but:

  • Neither uploader stores it (no self.update_filter assignments)
  • The batch upload functions (upload_batch, upload_batch_grpc) don't accept update_filter parameters
  • update_filter is never propagated to actual upsert operations

The feature is incomplete. Either:

  1. Add update_filter parameter to upload_batch() and upload_batch_grpc() signatures, store it in uploaders, and pass to API calls
  2. Remove update_filter from updater_kwargs if not yet supported
♻️ Duplicate comments (4)
qdrant_client/conversions/conversion.py (2)

268-268: Critical: Incorrect presence check on protobuf map field.

Protobuf map fields are never None in Python—they're always present (even when empty). The check model.metadata is not None will always evaluate to True, causing convert_payload to be called even for empty maps. This means an empty protobuf map {} will be converted to an empty dict instead of None, which may not align with the REST model's semantics (metadata: Optional[Dict[str, Any]] defaulting to None).

Apply this diff to check for non-empty maps:

-            metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
+            metadata=cls.convert_payload(model.metadata) if model.metadata else None,

Alternatively, be explicit with length:

-            metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
+            metadata=cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None,

795-795: Critical: Same protobuf map presence check issue.

Same issue as line 268: model.metadata is not None always evaluates to True for protobuf maps. The parentheses are correct (no tuple), but the condition needs fixing.

Apply this diff:

-            metadata=(cls.convert_payload(model.metadata) if model.metadata is not None else None),
+            metadata=cls.convert_payload(model.metadata) if model.metadata else None,
qdrant_client/embed/_inspection_cache.py (1)

444-446: Restore the missing space in the midpoint description.

The string still renders as “1.Defaults”, which reads poorly in the generated docs. Please insert the space so it becomes “1. Defaults”.

-                "description": "The midpoint of the decay. Should be between 0 and 1.Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
+                "description": "The midpoint of the decay. Should be between 0 and 1. Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
qdrant_client/qdrant_remote.py (1)

1999-2047: REST update_vectors should normalize grpc.PointVectors; otherwise JSON encoding fails.

Convert grpc.PointVectors to REST model before constructing models.UpdateVectors.

         else:
+            # Normalize possible gRPC PointVectors to REST models for JSON payload
+            if isinstance(points, list):
+                points = [
+                    (GrpcToRest.convert_point_vectors(p) if isinstance(p, grpc.PointVectors) else p)
+                    for p in points
+                ]
             if isinstance(update_filter, grpc.Filter):
                 update_filter = GrpcToRest.convert_filter(model=update_filter)
 
             return self.openapi_client.points_api.update_vectors(
                 collection_name=collection_name,
                 wait=wait,
                 update_vectors=models.UpdateVectors(
                     points=points,
                     shard_key=shard_key_selector,
                     update_filter=update_filter,
                 ),
                 ordering=ordering,
             ).result
🧹 Nitpick comments (8)
tests/test_local_persistence.py (1)

155-203: Rename typo: “persisence” → “persistence”

Purely cosmetic but improves discoverability. Consider renaming test and collection name for consistency.

-def test_update_persisence():
-    collection_name = "update_persisence"
+def test_update_persistence():
+    collection_name = "update_persistence"
tests/congruence_tests/test_updates.py (1)

508-709: Avoid flakiness: add wait=True on remote mutating calls

Several remote writes (upsert/update_vectors/upload_points/upload_collection/batch_update_points) run without wait=True. Under load this can race comparisons. Recommend setting wait=True for remote_client in this test.

Example:

-remote_client.upsert(COLLECTION_NAME, points=original_points)
+remote_client.upsert(COLLECTION_NAME, points=original_points, wait=True)

-remote_client.update_vectors(COLLECTION_NAME, points=new_point_vectors, update_filter=update_filter)
+remote_client.update_vectors(COLLECTION_NAME, points=new_point_vectors, update_filter=update_filter, wait=True)

Apply similarly to other remote writes in this test.

qdrant_client/hybrid/fusion.py (1)

10-18: Harden input and avoid side‑effects (optional)

  • Validate ranking_constant_k > 0; fall back to default if invalid.
  • Consider copying ScoredPoint before overwriting score to avoid mutating upstream responses.
 def reciprocal_rank_fusion(
     responses: list[list[models.ScoredPoint]],
     limit: int = 10,
     ranking_constant_k: Optional[int] = None,
 ) -> list[models.ScoredPoint]:
-    def compute_score(pos: int) -> float:
-        ranking_constant = (
-            ranking_constant_k if ranking_constant_k is not None else DEFAULT_RANKING_CONSTANT_K
-        )  # mitigates the impact of high rankings by outlier systems
+    def compute_score(pos: int) -> float:
+        k = ranking_constant_k if (ranking_constant_k and ranking_constant_k > 0) else DEFAULT_RANKING_CONSTANT_K
         return 1 / (ranking_constant + pos)
@@
-    for point_id, score in sorted_scores[:limit]:
-        point = point_pile[point_id]
-        point.score = score
-        sorted_points.append(point)
+    for point_id, score in sorted_scores[:limit]:
+        original = point_pile[point_id]
+        point = models.ScoredPoint(**original.model_dump())  # shallow clone to avoid side-effects
+        point.score = score
+        sorted_points.append(point)

Also applies to: 30-36

qdrant_client/qdrant_remote.py (2)

2702-2746: Broaden metadata type check for gRPC conversion.

Use Mapping instead of dict to accept any dict-like payloads.

-            if isinstance(metadata, dict):
+            if isinstance(metadata, Mapping):
                 metadata = RestToGrpc.convert_payload(metadata)

2816-2881: Same metadata type-check improvement for create_collection (gRPC path).

-            if isinstance(metadata, dict):
+            if isinstance(metadata, Mapping):
                 metadata = RestToGrpc.convert_payload(metadata)
qdrant_client/async_qdrant_remote.py (2)

2428-2478: Broaden metadata check to Mapping for async update_collection (gRPC).

-            if isinstance(metadata, dict):
+            if isinstance(metadata, Mapping):
                 metadata = RestToGrpc.convert_payload(metadata)

2524-2596: Same metadata check improvement for async create_collection (gRPC).

-            if isinstance(metadata, dict):
+            if isinstance(metadata, Mapping):
                 metadata = RestToGrpc.convert_payload(metadata)
qdrant_client/qdrant_client.py (1)

2492-2492: Minor docstring inconsistency for metadata parameter

For consistency with create_collection and update_collection, consider using "JSON-like metadata" instead of "JSON metadata" in the docstring.

Apply this diff:

-            metadata: Arbitrary JSON metadata for the collection
+            metadata: Arbitrary JSON-like metadata for the collection
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4d40e and cee3770.

📒 Files selected for processing (20)
  • qdrant_client/async_qdrant_client.py (21 hunks)
  • qdrant_client/async_qdrant_remote.py (26 hunks)
  • qdrant_client/conversions/common_types.py (2 hunks)
  • qdrant_client/conversions/conversion.py (23 hunks)
  • qdrant_client/embed/_inspection_cache.py (45 hunks)
  • qdrant_client/http/models/models.py (31 hunks)
  • qdrant_client/hybrid/fusion.py (1 hunks)
  • qdrant_client/local/async_qdrant_local.py (11 hunks)
  • qdrant_client/local/local_collection.py (14 hunks)
  • qdrant_client/local/payload_filters.py (2 hunks)
  • qdrant_client/local/qdrant_local.py (13 hunks)
  • qdrant_client/qdrant_client.py (21 hunks)
  • qdrant_client/qdrant_remote.py (25 hunks)
  • qdrant_client/uploader/grpc_uploader.py (5 hunks)
  • qdrant_client/uploader/rest_uploader.py (5 hunks)
  • tests/congruence_tests/test_query.py (2 hunks)
  • tests/congruence_tests/test_updates.py (2 hunks)
  • tests/congruence_tests/test_uuids.py (1 hunks)
  • tests/test_local_persistence.py (1 hunks)
  • tests/test_qdrant_client.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (15)
tests/congruence_tests/test_query.py (3)
qdrant_client/client_base.py (1)
  • query_points (93-120)
qdrant_client/http/models/models.py (2)
  • QueryResponse (2147-2148)
  • Prefetch (2010-2034)
qdrant_client/local/local_collection.py (1)
  • query_points (705-760)
qdrant_client/hybrid/fusion.py (1)
qdrant_client/http/models/models.py (1)
  • ScoredPoint (2496-2507)
tests/test_qdrant_client.py (5)
tests/utils.py (1)
  • read_version (9-25)
qdrant_client/local/async_qdrant_local.py (3)
  • create_collection (925-972)
  • get_collection (873-875)
  • update_collection (884-901)
qdrant_client/local/qdrant_local.py (3)
  • create_collection (997-1050)
  • get_collection (942-944)
  • update_collection (953-972)
qdrant_client/qdrant_client.py (3)
  • create_collection (2333-2424)
  • get_collection (2231-2242)
  • update_collection (2257-2311)
qdrant_client/qdrant_remote.py (3)
  • create_collection (2797-2922)
  • get_collection (2664-2676)
  • update_collection (2691-2780)
qdrant_client/conversions/common_types.py (1)
qdrant_client/grpc/points_pb2.pyi (1)
  • PointId (249-271)
tests/congruence_tests/test_uuids.py (5)
qdrant_client/qdrant_client.py (23)
  • QdrantClient (27-3191)
  • collection_exists (2244-2255)
  • delete_collection (2313-2331)
  • create_collection (2333-2424)
  • create_payload_index (2726-2767)
  • upsert (1575-1654)
  • query_points (447-620)
  • query_batch_points (403-445)
  • query_points_groups (622-788)
  • search_matrix_pairs (1058-1104)
  • search_matrix_offsets (1106-1152)
  • scroll (1413-1481)
  • facet (1524-1573)
  • retrieve (1766-1822)
  • update_vectors (1656-1714)
  • delete_vectors (1716-1764)
  • delete (1824-1869)
  • set_payload (1871-1965)
  • overwrite_payload (1967-2032)
  • delete_payload (2034-2081)
  • clear_payload (2083-2125)
  • upload_collection (2648-2724)
  • batch_update_points (2127-2167)
tests/congruence_tests/test_common.py (5)
  • init_local (399-404)
  • init_remote (407-409)
  • generate_fixtures (117-132)
  • compare_client_results (296-380)
  • compare_collections (175-197)
tests/fixtures/payload.py (1)
  • one_random_payload_please (190-245)
qdrant_client/http/models/models.py (19)
  • VectorParams (3412-3437)
  • Distance (728-739)
  • PointStruct (1972-1975)
  • PayloadSchemaType (1883-1898)
  • Filter (884-894)
  • HasIdCondition (1049-1054)
  • QueryRequest (2108-2140)
  • PointVectors (1978-1980)
  • PointIdsList (1955-1957)
  • FilterSelector (897-899)
  • PointsBatch (1983-1989)
  • Batch (61-64)
  • PointsList (1992-1998)
  • SetPayload (2716-2735)
  • DeletePayload (628-642)
  • ClearPayloadOperation (155-156)
  • UpdateVectors (3325-3328)
  • DeleteVectors (649-657)
  • DeleteOperation (624-625)
qdrant_client/local/local_collection.py (15)
  • upsert (2473-2509)
  • query_points (705-760)
  • search_matrix_pairs (1586-1607)
  • search_matrix_offsets (1552-1584)
  • scroll (1886-1920)
  • facet (1162-1211)
  • retrieve (1213-1237)
  • update_vectors (2543-2553)
  • delete_vectors (2555-2570)
  • delete (2608-2618)
  • set_payload (2630-2654)
  • overwrite_payload (2656-2670)
  • delete_payload (2672-2688)
  • clear_payload (2690-2703)
  • batch_update_points (2705-2744)
qdrant_client/async_qdrant_client.py (1)
qdrant_client/http/models/models.py (1)
  • Filter (884-894)
qdrant_client/http/models/models.py (2)
qdrant_client/grpc/points_pb2.pyi (17)
  • b (3343-3344)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • Rrf (2753-2768)
  • rrf (2811-2812)
qdrant_client/grpc/collections_pb2.pyi (11)
  • stopwords (2021-2022)
  • metadata (1527-1528)
  • metadata (1638-1639)
  • metadata (1855-1856)
  • warnings (2285-2286)
  • CollectionWarning (773-783)
  • languages (1984-1985)
  • custom (1987-1988)
  • sparse_config (1283-1284)
  • sparse_config (1377-1377)
  • WalConfigDiff (903-929)
qdrant_client/async_qdrant_remote.py (4)
qdrant_client/http/models/models.py (6)
  • Filter (884-894)
  • PointStruct (1972-1975)
  • PointsList (1992-1998)
  • Batch (61-64)
  • PointsBatch (1983-1989)
  • UpdateVectors (3325-3328)
qdrant_client/conversions/conversion.py (12)
  • RestToGrpc (2501-4809)
  • convert_filter (167-183)
  • convert_filter (2503-2523)
  • convert_shard_key_selector (2194-2197)
  • convert_shard_key_selector (4621-4628)
  • convert_read_consistency (1737-1746)
  • convert_read_consistency (4093-4103)
  • GrpcToRest (141-2491)
  • convert_update_result (468-472)
  • convert_update_result (2687-2691)
  • convert_payload (584-585)
  • convert_payload (2805-2806)
qdrant_client/conversions/common_types.py (1)
  • get_args_subscribed (34-46)
qdrant_client/qdrant_remote.py (1)
  • update_vectors (1999-2047)
qdrant_client/local/local_collection.py (2)
qdrant_client/local/payload_filters.py (1)
  • calculate_payload_mask (317-335)
qdrant_client/hybrid/fusion.py (2)
  • reciprocal_rank_fusion (4-29)
  • distribution_based_score_fusion (32-72)
qdrant_client/uploader/grpc_uploader.py (2)
qdrant_client/conversions/conversion.py (4)
  • RestToGrpc (2501-4809)
  • payload_to_grpc (91-92)
  • convert_extended_point_id (3159-3166)
  • convert_vector_struct (3388-3428)
qdrant_client/grpc/points_service_pb2_grpc.py (1)
  • PointsStub (8-156)
qdrant_client/conversions/conversion.py (5)
qdrant_client/grpc/collections_pb2.pyi (24)
  • metadata (1527-1528)
  • metadata (1638-1639)
  • metadata (1855-1856)
  • HasField (375-375)
  • HasField (411-411)
  • HasField (439-439)
  • HasField (471-471)
  • HasField (501-501)
  • HasField (522-522)
  • HasField (544-544)
  • HasField (570-570)
  • HasField (658-658)
  • HasField (702-702)
  • HasField (750-750)
  • HasField (847-847)
  • HasField (892-892)
  • HasField (922-922)
  • HasField (1025-1025)
  • HasField (1067-1067)
  • shard_key (2520-2521)
  • shard_key (2550-2551)
  • shard_key (2604-2604)
  • shard_key (2803-2804)
  • shard_key (2834-2835)
qdrant_client/async_qdrant_remote.py (2)
  • upsert (1673-1752)
  • update_vectors (1754-1799)
qdrant_client/qdrant_remote.py (2)
  • upsert (1907-1997)
  • update_vectors (1999-2047)
qdrant_client/http/models/models.py (5)
  • MatchTextAny (1577-1582)
  • RrfQuery (2452-2453)
  • Rrf (2444-2449)
  • PointsList (1992-1998)
  • UpdateVectors (3325-3328)
qdrant_client/grpc/points_pb2.pyi (51)
  • rrf (2811-2812)
  • Rrf (2753-2768)
  • update_filter (665-666)
  • update_filter (807-808)
  • update_filter (3405-3406)
  • update_filter (3564-3565)
  • upsert (3659-3659)
  • points (657-657)
  • points (703-704)
  • points (798-799)
  • points (1032-1033)
  • points (3400-3400)
  • points (3558-3559)
  • points (3612-3613)
  • points (3633-3634)
  • points (4921-4922)
  • points (5001-5001)
  • shard_key (3842-3843)
  • shard_key (4206-4207)
  • shard_key_selector (662-663)
  • shard_key_selector (709-710)
  • shard_key_selector (756-757)
  • shard_key_selector (804-805)
  • shard_key_selector (877-878)
  • shard_key_selector (941-942)
  • shard_key_selector (994-995)
  • shard_key_selector (1038-1039)
  • shard_key_selector (1478-1479)
  • shard_key_selector (1639-1640)
  • shard_key_selector (1771-1772)
  • shard_key_selector (1821-1822)
  • shard_key_selector (1905-1906)
  • shard_key_selector (2058-2059)
  • shard_key_selector (2217-2218)
  • shard_key_selector (2309-2310)
  • update_vectors (3671-3671)
  • UpdateVectors (3551-3578)
  • Match (4718-4771)
  • Query (2772-2829)
  • vectors (564-564)
  • vectors (841-842)
  • vectors (871-872)
  • vectors (1218-1218)
  • vectors (1250-1250)
  • vectors (1268-1268)
  • vectors (1289-1289)
  • vectors (3590-3591)
  • vectors (3839-3840)
  • vectors (4204-4204)
  • vectors (5064-5064)
  • PointStructList (3393-3419)
qdrant_client/uploader/rest_uploader.py (4)
qdrant_client/http/api_client.py (1)
  • SyncApis (42-57)
qdrant_client/grpc/points_pb2.pyi (2)
  • models (5151-5151)
  • ShardKeySelector (617-634)
qdrant_client/http/models/models.py (3)
  • Batch (61-64)
  • PointsList (1992-1998)
  • PointStruct (1972-1975)
qdrant_client/uploader/uploader.py (1)
  • BaseUploader (26-94)
tests/test_local_persistence.py (6)
qdrant_client/qdrant_client.py (8)
  • QdrantClient (27-3191)
  • collection_exists (2244-2255)
  • delete_collection (2313-2331)
  • create_collection (2333-2424)
  • rest (209-223)
  • get_collection (2231-2242)
  • update_collection (2257-2311)
  • close (175-182)
qdrant_client/local/async_qdrant_local.py (6)
  • collection_exists (877-882)
  • delete_collection (909-923)
  • create_collection (925-972)
  • get_collection (873-875)
  • update_collection (884-901)
  • close (74-90)
qdrant_client/local/qdrant_local.py (6)
  • collection_exists (946-951)
  • delete_collection (980-995)
  • create_collection (997-1050)
  • get_collection (942-944)
  • update_collection (953-972)
  • close (74-93)
qdrant_client/http/models/models.py (4)
  • VectorParams (3412-3437)
  • Distance (728-739)
  • SparseVectorParams (3018-3028)
  • Modifier (1641-1650)
qdrant_client/local/local_collection.py (1)
  • close (173-175)
qdrant_client/local/persistence.py (1)
  • close (100-101)
qdrant_client/qdrant_client.py (1)
qdrant_client/http/models/models.py (1)
  • Filter (884-894)
qdrant_client/local/async_qdrant_local.py (4)
qdrant_client/local/qdrant_local.py (10)
  • _get_collection (165-173)
  • upsert (776-781)
  • _default_update_result (816-820)
  • update_vectors (783-791)
  • delete_vectors (793-802)
  • _save (144-163)
  • _collection_path (974-978)
  • delete_collection (980-995)
  • closed (71-72)
  • scroll (733-752)
qdrant_client/local/local_collection.py (4)
  • upsert (2473-2509)
  • update_vectors (2543-2553)
  • delete_vectors (2555-2570)
  • scroll (1886-1920)
qdrant_client/async_qdrant_client.py (5)
  • upsert (1529-1602)
  • update_vectors (1604-1660)
  • delete_vectors (1662-1709)
  • delete_collection (2238-2255)
  • scroll (1370-1437)
qdrant_client/qdrant_client.py (5)
  • upsert (1575-1654)
  • update_vectors (1656-1714)
  • delete_vectors (1716-1764)
  • delete_collection (2313-2331)
  • scroll (1413-1481)
🪛 Ruff (0.14.2)
qdrant_client/async_qdrant_remote.py

1581-1581: Unused method argument: kwargs

(ARG002)

qdrant_client/local/local_collection.py

821-821: Avoid specifying long messages outside the exception class

(TRY003)


828-828: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


2441-2441: Avoid specifying long messages outside the exception class

(TRY003)


2499-2499: Prefer TypeError exception for invalid type

(TRY004)


2499-2499: Avoid specifying long messages outside the exception class

(TRY003)

qdrant_client/uploader/grpc_uploader.py

36-36: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


106-106: Avoid specifying long messages outside the exception class

(TRY003)

qdrant_client/conversions/conversion.py

3662-3662: Avoid specifying long messages outside the exception class

(TRY003)

qdrant_client/uploader/rest_uploader.py

93-93: Avoid specifying long messages outside the exception class

(TRY003)

qdrant_client/local/qdrant_local.py

787-787: Unused method argument: kwargs

(ARG002)


798-798: Unused method argument: kwargs

(ARG002)


980-980: Unused method argument: kwargs

(ARG002)


1139-1139: Unused method argument: field_name

(ARG002)

qdrant_client/local/async_qdrant_local.py

728-728: Unused method argument: kwargs

(ARG002)


739-739: Unused method argument: kwargs

(ARG002)


909-909: Unused method argument: kwargs

(ARG002)


911-911: Avoid specifying long messages outside the exception class

(TRY003)


1066-1066: Unused method argument: collection_name

(ARG002)


1066-1066: Unused method argument: field_name

(ARG002)


1066-1066: Unused method argument: kwargs

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
🔇 Additional comments (46)
qdrant_client/local/local_collection.py (11)

59-59: LGTM!

The import of check_filter is necessary to support the new update_filter feature in upsert/update operations.


980-986: LGTM!

The error handling correctly raises ValueError (instead of AssertionError) for fusion, formula, and RRF queries when prefetches are missing. The error messages are clear and user-friendly.


1229-1229: LGTM!

UUID-to-string normalization ensures consistent ID lookups. This aligns with the internal representation where UUIDs are stored as strings in self.ids.


2280-2282: LGTM with a note on input mutation.

UUID normalization is correctly applied. Note that this mutates the input point.id in-place, which is acceptable for an internal method but should be kept consistent across the codebase.


2339-2343: LGTM!

UUID normalization is consistently applied before adding points to internal dictionaries.


2496-2527: LGTM!

The update_filter parameter is correctly propagated to _upsert_point for both list and batch upsert operations.


2571-2593: Verify that the early return is intentional.

The update_filter logic mirrors _upsert_point, correctly applying conditional updates. When the filter excludes a point, the function returns None early (line 2591).

However, the early return applies to the entire function rather than just the current point in the loop. This means if any point is filtered out, the function stops processing remaining points in the points sequence.

Is this intended, or should it use continue to skip only the filtered point?

If only the current point should be skipped:

             if not check_filter(
                 update_filter, self.payload[idx], self.ids_inv[idx], has_vector
             ):
-                return None
+                continue

2637-2642: LGTM!

UUID normalization is consistently applied for both list and PointIdsList selectors, ensuring uniform string-based ID handling.


2750-2781: LGTM!

The update_filter is correctly extracted and propagated for both UpsertOperation (with PointsBatch and PointsList variants) and UpdateVectorsOperation.


2798-2835: LGTM!

The metadata field is correctly propagated from self.config.metadata to the CollectionInfo response. This aligns with the PR objective to add collection-level metadata support.

Also note: vectors_count has been correctly removed from CollectionInfo, as indicated in the PR objectives.


2438-2494: The update_filter concern is moot—the early return is unreachable with current code.

The update_filter parameter was added to _upsert_point, but no caller ever passes it. Both call sites (lines 2476 and 2491) are in the public upsert() method, which itself doesn't accept update_filter. This means update_filter always defaults to None, making the condition at line 2483 (if not self.deleted[idx] and update_filter is not None:) always false. The early return on line 2488 is dead code and will never execute.

Either the feature is incomplete (the public upsert() method should accept and pass update_filter), or this parameter was added defensively for future expansion. Either way, callers don't need to handle None returns because they'll never receive them.

Likely an incorrect or invalid review comment.

qdrant_client/conversions/conversion.py (11)

1-1: LGTM: UUID import for ExtendedPointId support.

The uuid import is correctly added to support UUID-to-string conversion in convert_extended_point_id at line 3162.


284-284: LGTM: copy_vectors field added to HNSW config conversions.

The copy_vectors field is consistently handled in both gRPC→REST and REST→gRPC directions with proper optional field checking using HasField.

Also applies to: 300-300


682-683: LGTM: text_any match variant added.

The text_any match handling follows the same pattern as other match variants (text, keyword, phrase, etc.) and correctly maps to rest.MatchTextAny.


1359-1362: LGTM: RRF query conversion correctly implemented.

The RRF conversion correctly:

  • Reads k from the rrf object (not model.k)
  • Respects optional field semantics using HasField("k")
  • Passes None when k is not set

3659-3664: LGTM: REST→gRPC RRF conversion avoids setting None on optional scalar.

The conversion correctly creates an empty grpc.Rrf() and only sets k when it's not None, avoiding the protobuf anti-pattern of setting optional scalars to None.


1983-1990: LGTM: update_filter propagation in gRPC→REST conversions.

The update_filter field is correctly extracted from gRPC upsert and update_vectors operations using HasField("update_filter") and passed to the REST models.

Also applies to: 2092-2099


2384-2386: LGTM: max_payload_index_count added to strict mode configs.

The max_payload_index_count field is consistently handled in both convert_strict_mode_config and convert_strict_mode_config_output with proper optional field checking.

Also applies to: 2454-2456


2880-2880: LGTM: REST→gRPC metadata conversions are correct.

These conversions check model.metadata is not None, which is correct for REST models where metadata is typed as Optional[Dict[str, Any]] and can actually be None (unlike protobuf maps in the gRPC→REST direction).

Also applies to: 3021-3021


3162-3163: LGTM: UUID support for ExtendedPointId.

Converting uuid.UUID instances to strings before creating grpc.PointId is a clean approach to support Python UUID objects in the API.


4401-4410: LGTM: update_filter propagation in REST→gRPC conversions.

The update_filter is consistently extracted from REST models and converted to gRPC filters in both upsert and update_vectors operations.

Also applies to: 4516-4527


4740-4740: LGTM: max_payload_index_count in REST→gRPC strict mode config conversions.

Consistent handling of max_payload_index_count in both convert_strict_mode_config and convert_strict_mode_config_output methods.

Also applies to: 4774-4774

qdrant_client/async_qdrant_client.py (5)

1611-1642: update_filter on update_vectors is consistent

Signature, docs, and forwarding are aligned with Filter semantics.

Also applies to: 1653-1660


2506-2531: upload_points gains update_filter: OK

Matches upsert semantics; forwarded to underlying client.

Also applies to: 2551-2561


2577-2604: upload_collection gains update_filter: OK

Signature/docstring/forwarding consistent with other write paths.

Also applies to: 2625-2637


2196-2214: metadata on update_collection: merge semantics documented

Optional metadata forwarded; pairs with async_local implementation merging dicts.

Also applies to: 2233-2235


2276-2324: metadata on create/recreate_collection: OK

Signature/docs/forwarding are consistent.

Also applies to: 2344-2346, 2366-2414, 2439-2441

tests/congruence_tests/test_query.py (2)

366-378: Parametrized RRF helper looks good

Method builds RrfQuery with k=10 and mirrors existing RRF paths.


1293-1295: Good to cover parametrized RRF in fusion tests

Adding this comparison strengthens cross-backend parity.

qdrant_client/hybrid/fusion.py (1)

6-6: Verify RRF default k aligns with server/local behavior

DEFAULT_RANKING_CONSTANT_K = 2 significantly affects fusion. If the server’s default differs (historically 60 in many RRF impls), local vs remote results may diverge when no k is provided. Please confirm the intended default and align both sides or gate in tests.

Also applies to: 10-18

tests/test_qdrant_client.py (1)

1170-1189: Metadata coverage looks solid

Thanks for exercising both the initial metadata persistence and the merge-on-update path when the server supports it. The version gating keeps older deployments unaffected.

qdrant_client/uploader/grpc_uploader.py (1)

85-142: Nice job plumbing update_filter through gRPC uploads

The single conversion in __init__ and reuse in process_upload avoids redundant work, and the PointId normalization keeps generated IDs valid.

qdrant_client/qdrant_remote.py (4)

2941-2963: Recreate: metadata propagation LGTM.


3063-3078: upload_points: parameter and plumbing LGTM.


3094-3113: upload_collection: parameter and plumbing LGTM.


1806-1846: REST client signature verified—consistency parameter is properly supported.

The REST PointsApi.count_points method accepts consistency: m.ReadConsistency = None as a parameter in both async and sync variants, confirming the code change is correct.

qdrant_client/async_qdrant_remote.py (4)

2633-2672: Async recreate_collection: metadata propagation LGTM.


2768-2783: upload_points (async): LGTM.


2799-2814: upload_collection (async): LGTM.


1574-1617: Async count consistency wiring verified—code is correct.

The async REST client's count_points method accepts a consistency parameter (line 564, points_api.py) and properly propagates it. The code correctly passes consistency to both gRPC and REST paths.

qdrant_client/qdrant_client.py (6)

1575-1654: LGTM — upsert method correctly implements update_filter

The update_filter parameter is correctly typed as types.Filter, properly documented, and forwarded to the underlying client. The previous type annotation issue has been resolved.


1656-1714: LGTM — update_vectors method correctly implements update_filter

The update_filter parameter is correctly typed as types.Filter, properly documented, and forwarded to the underlying client. The docstring appropriately describes the filtering behavior for vector updates.


2257-2311: LGTM — update_collection method correctly implements metadata parameter

The metadata parameter is properly typed as types.Payload for arbitrary JSON-like data, clearly documented with merge semantics, and correctly forwarded to the underlying client.


2333-2424: LGTM — create_collection method correctly implements metadata parameter

The metadata parameter is properly typed, documented, and forwarded to the underlying client implementation.


2580-2648: LGTM — upload_points method correctly implements update_filter

The update_filter parameter is correctly typed as types.Filter, properly documented, and forwarded to the underlying client.


2650-2729: LGTM — upload_collection method correctly implements update_filter

The update_filter parameter is correctly typed as types.Filter, properly documented, and forwarded to the underlying client.

Comment on lines +1481 to +1486
"anyOf": [{"$ref": "#/$defs/TokenizerType"}, {"type": "null"}],
"default": None,
"description": "Configuration of the local bm25 models.",
},
"language": {
"anyOf": [{"type": "string"}, {"type": "null"}],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify the BM25 tokenizer field.

The description currently repeats the overall config text, so downstream docs don’t explain what this field does. Please describe that it selects the tokenizer (and add the missing title for consistency with sibling properties).

             "tokenizer": {
                 "anyOf": [{"$ref": "#/$defs/TokenizerType"}, {"type": "null"}],
                 "default": None,
-                "description": "Configuration of the local bm25 models.",
+                "description": "Tokenizer to apply before scoring; leave unset to inherit the collection-level default.",
+                "title": "Tokenizer",
             },
🤖 Prompt for AI Agents
In qdrant_client/embed/_inspection_cache.py around lines 1481 to 1486, the
"language" property is missing a title and its description repeats the overall
config; update it to include a title (e.g., "Tokenizer language" or "language")
and replace the description with a concise explanation that this field selects
which BM25 tokenizer to use (or null to use the default tokenizer), so the docs
clearly state the purpose and allowed values.

Comment on lines +814 to +817
if isinstance(query, models.RrfQuery):
fused = reciprocal_rank_fusion(
responses=sources, limit=limit + offset, ranking_constant_k=query.rrf.k
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Parameter name mismatch in reciprocal_rank_fusion call.

The call passes ranking_constant_k=query.rrf.k, but based on the function definition in qdrant_client/hybrid/fusion.py (lines 3-28), reciprocal_rank_fusion does not accept a ranking_constant_k parameter. The function currently hardcodes ranking_constant = 2 inside compute_score.

This will raise a TypeError at runtime when RrfQuery is used.

Apply this fix if reciprocal_rank_fusion should accept a configurable constant:

Option 1: Update the fusion function signature (in qdrant_client/hybrid/fusion.py):

def reciprocal_rank_fusion(
    responses: list[list[models.ScoredPoint]], 
    limit: int = 10,
    ranking_constant_k: int = 2
) -> list[models.ScoredPoint]:
    def compute_score(pos: int) -> float:
        return 1 / (ranking_constant_k + pos)
    # ... rest of the function

Option 2: If the constant should remain fixed, remove the parameter from this call:

-            if isinstance(query, models.RrfQuery):
-                fused = reciprocal_rank_fusion(
-                    responses=sources, limit=limit + offset, ranking_constant_k=query.rrf.k
-                )
+            if isinstance(query, models.RrfQuery):
+                fused = reciprocal_rank_fusion(
+                    responses=sources, limit=limit + offset
+                )
🤖 Prompt for AI Agents
In qdrant_client/local/local_collection.py around lines 814-817 the call to
reciprocal_rank_fusion passes a keyword argument ranking_constant_k=query.rrf.k
which does not exist on the function signature and will raise a TypeError; fix
by either (A) updating qdrant_client/hybrid/fusion.py to accept
ranking_constant_k (add it to the reciprocal_rank_fusion parameters and use it
inside compute_score) so the call remains as-is, or (B) if the constant should
remain fixed, remove the ranking_constant_k argument from this call and let the
fusion function use its internal hardcoded value.


compare_client_results(local_client, remote_client, search_matrix_offsets_uuid_filter)

cl.scroll(COLLECTION_NAME, scroll_filter=id_filter)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix NameError: cl is undefined here

This stray call will crash the test. Remove it (the next compare already exercises scroll).

-    cl.scroll(COLLECTION_NAME, scroll_filter=id_filter)
+    # removed stray call; scroll is exercised below via compare_client_results
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cl.scroll(COLLECTION_NAME, scroll_filter=id_filter)
# removed stray call; scroll is exercised below via compare_client_results
🤖 Prompt for AI Agents
In tests/congruence_tests/test_uuids.py around line 111, there is a stray call
to cl.scroll(COLLECTION_NAME, scroll_filter=id_filter) where cl is undefined;
remove this line so the test no longer raises NameError (the subsequent compare
already covers scrolling).

joein and others added 3 commits November 2, 2025 17:29
* new: add match text any local mode

* tests: add match text any tests
* new: update models, remove init_from and locks

* deprecate: remove init from tests

* deprecate: remove lock tests

* new: convert ascii_folding

* fix: fix type stub

* new: convert acorn

* new: convert shard key with fallback

* new: update grpcio and grpcio tools in generator (#1106)

* new: update grpcio and grpcio tools in generator

* fix: bind grpcio and tools versions to 1.62.0 in generator

* Remove deprecated methods (#1103)

* deprecate: remove old api methods

* deprecate: remove type stub for removed methods

* deprecate: remove old api methods from test_qdrant_client

* deprecate: replace search with query points in test_in_memory

* deprecate: replace search methods in fastembed mixin with query points

* deprecate: replace old api methods in test async qdrant client

* deprecate: replace search with query points in test delete points

* deprecate: replace discover and context with query points in test_discovery

* deprecate: replace recommend_groups with query_points_groups in test_group_recommend

* deprecate: replace search_groups in test_group_search

* deprecate: replace recommend with query points in test_recommendation

* deprecate: replace search with query points in test search

* deprecate: replace context and discover with query points in test sparse discovery

* deprecate: replace search with query points in test sparse idf search

* deprecate: replace recommend with query points in test sparse recommend

* deprecate: replace search with query points in test sparse search

* deprecate: replace missing search request with query request in qdrant_fastembed

* deprecate: replace search with query points in test multivector search queries

* deprecate: replace upload records with upload points in test_updates

* deprecate: remove redundant structs (#1104)

* deprecate: remove redundant structs

* fix: do not use removed conversions in local mode

* fix: remove redundant conversions, simplify types.QueryRequest

* deprecate: replace old style grpc vector conversion to a new one (#1105)

* deprecate: replace old style grpc vector conversion to a new one

* fix: ignore union attr in conversion

* review fixes

---------

Co-authored-by: generall <[email protected]>

---------

Co-authored-by: generall <[email protected]>

---------

Co-authored-by: generall <[email protected]>

* new: deprecate add, query, query_batch in fastembed mixin (#1102)

* new: deprecate add, query, query_batch in fastembed mixin

* 1.16 -> 1.17

---------

Co-authored-by: generall <[email protected]>

---------

Co-authored-by: generall <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
qdrant_client/async_qdrant_remote.py (2)

1696-1764: Convert metadata when provided, not only when dict.

The current isinstance(metadata, dict) checks are too restrictive and may skip conversion for Mapping types or pydantic-validated aliases. Change all four instances to if metadata is not None.

Locations to fix:

  • qdrant_client/async_qdrant_remote.py:1718 (update_collection)
  • qdrant_client/async_qdrant_remote.py:1822 (create_collection)
  • qdrant_client/qdrant_remote.py:1872 (update_collection)
  • qdrant_client/qdrant_remote.py:1989 (create_collection)
-            if isinstance(metadata, dict):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)

Note: recreate_collection delegates to create_collection, so no separate fix is needed there.


556-615: Bug: shard_key on lookup_from silently dropped during REST-to-gRPC conversion.

The RestToGrpc.convert_lookup_location method (qdrant_client/conversions/conversion.py:3776-3780) only converts collection_name and vector_name, but omits the shard_key field. Since rest.LookupLocation has shard_key: Optional[ShardKeySelector] and grpc.LookupLocation supports shard_key_selector, any user-provided lookup_from.shard_key is silently discarded, causing queries to search unintended shards.

Fix: Update RestToGrpc.convert_lookup_location to extract and convert model.shard_key (if present) to shard_key_selector in the gRPC message.

qdrant_client/http/models/models.py (1)

1537-1550: Convert REST LookupLocation.shard_key field to gRPC LookupLocation.shard_key_selector

The shard_key field in rest.LookupLocation is silently dropped during REST→gRPC conversion. Proto definition confirms grpc.LookupLocation accepts optional ShardKeySelector shard_key_selector, but convert_lookup_location (line 3776) only maps collection_name and vector_name. This causes shard routing directives to be ignored at runtime.

Apply the fix to qdrant_client/conversions/conversion.py line 3776:

 def convert_lookup_location(cls, model: rest.LookupLocation) -> grpc.LookupLocation:
     return grpc.LookupLocation(
         collection_name=model.collection,
         vector_name=model.vector,
+        shard_key_selector=(
+            cls.convert_shard_key_selector(model.shard_key)
+            if model.shard_key is not None
+            else None
+        ),
     )

The convert_shard_key_selector function (line 4304) already correctly handles ShardKeyWithFallback with fallback mapping, so no additional changes are needed there.

tests/congruence_tests/test_sparse_recommend.py (1)

253-284: Fix return type to match other searcher methods.

The return type list[models.QueryResponse] is incompatible with the method signature expected by compare_client_results. All other searcher methods (lines 82, 215, 230, 241) return list[models.ScoredPoint], but recommend_batch returns list[models.QueryResponse]. Since QueryResponse contains a points field (list of ScoredPoint) rather than individual scored points, this breaks the comparison logic. Change recommend_batch to return list[models.ScoredPoint] by extracting points from the responses returned by query_batch_points.

qdrant_client/async_qdrant_fastembed.py (1)

346-370: Guard against missing payload to avoid AttributeError

scored_point.payload can be None when with_payload=False or server omits it. Accessing payload.get(...) will crash.

Apply:

-            response.append(
+            payload = scored_point.payload or {}
+            response.append(
                 QueryResponse(
                     id=scored_point.id,
                     embedding=embedding,
                     sparse_embedding=sparse_embedding,
-                    metadata=scored_point.payload,
-                    document=scored_point.payload.get("document", ""),
+                    metadata=payload,
+                    document=payload.get("document", ""),
                     score=scored_point.score,
                 )
             )
tests/congruence_tests/test_sparse_discovery.py (1)

9-9: Remove stale import (ContextExamplePair is no longer used)

Cleanup to avoid confusion and linter noise.

-from qdrant_client.http.models import ContextExamplePair
qdrant_client/conversions/conversion.py (1)

2033-2040: Fix the HasField guard for sparse index conversion.
model.HasField("index") is not None is always true because HasField already yields a boolean. As written we now materialize an index config even when the field is unset, which mutates the REST view and can surface default/empty configs unexpectedly. Drop the is not None so we only convert when the field is actually present.

Apply this diff:

-        if model.HasField("index") is not None
+        if model.HasField("index")
qdrant_client/qdrant_remote.py (1)

1141-1163: Don't reconvert already-gRPC PointVectors

In the gRPC path we now blanket-call RestToGrpc.convert_point_vectors on every element. That helper expects REST models.PointVectors, so feeding it an object that is already a grpc.PointVectors will either blow up (missing REST-only attributes) or silently produce a malformed message. Prior to this change we only converted when the element was a REST model, so prefer_grpc=True callers could pass native gRPC structures. This regression will break those call sites.

Please guard the conversion, mirroring the upsert logic:

-        points = [RestToGrpc.convert_point_vectors(point) for point in points]
+        points = [
+            RestToGrpc.convert_point_vectors(point)
+            if isinstance(point, models.PointVectors)
+            else point
+            for point in points
+        ]
♻️ Duplicate comments (7)
qdrant_client/local/local_collection.py (1)

814-817: Verify parameter compatibility after prior review concern.

A past review flagged that reciprocal_rank_fusion doesn't accept ranking_constant_k. However, the relevant code snippet from qdrant_client/hybrid/fusion.py (lines 8-35) indicates:

"Optional parameter ranking_constant_k (if provided) overrides default ranking constant"

This suggests the function signature was updated to accept this parameter. Please confirm that reciprocal_rank_fusion now supports ranking_constant_k to resolve the earlier concern.

#!/bin/bash
# Verify that reciprocal_rank_fusion accepts ranking_constant_k parameter
ast-grep --pattern $'def reciprocal_rank_fusion($$$, ranking_constant_k$$$)$$$'
qdrant_client/embed/_inspection_cache.py (2)

446-446: Fix the missing space in the midpoint description.

The generated text still omits the space before “Defaults”, exactly as the earlier review noted. Please apply the previously suggested fix so the sentence reads “…between 0 and 1. Defaults to 0.5…”.


1483-1489: Clarify the BM25 tokenizer field docs.

This property still reuses the generic config blurb and lacks a title, so the docs aren’t telling users what the field actually does. Please add an explicit title and update the description per the earlier feedback, e.g.:

             "tokenizer": {
                 "anyOf": [{"$ref": "#/$defs/TokenizerType"}, {"type": "null"}],
                 "default": None,
-                "description": "Configuration of the local bm25 models.",
+                "description": "Select the tokenizer to run before scoring; leave unset to inherit the collection default.",
+                "title": "Tokenizer",
             },
qdrant_client/http/models/models.py (1)

3359-3361: Fill missing description for UpdateVectors.update_filter.

The Field lacks a description, unlike PointsBatch/PointsList. Please add a description consistent with them in the generator/spec. I know this file is autogen. Raising upstream is sufficient.

qdrant_client/grpc/collections_pb2.pyi (1)

11-11: Use package-relative import for sibling proto module.

Absolute import json_with_int_pb2 may fail when installed as qdrant_client.grpc. Switch to a relative import.

Apply:

-import json_with_int_pb2
+from . import json_with_int_pb2

Verify remaining occurrences:

#!/usr/bin/env bash
rg -nC2 '^\s*import\s+json_with_int_pb2\b' qdrant_client/grpc/*.pyi
tests/test_qdrant_client.py (1)

1009-1019: Widen collection metadata test to cover mixed types (dup of prior review)

Broader types catch REST↔gRPC round‑trip issues.

-        collection_metadata = {"ownership": "Bart Simpson's property"}
+        collection_metadata = {
+            "ownership": "Bart Simpson's property",
+            "year": 1990,
+            "rating": 4.2,
+            "published": True,
+            "notes": None,
+            "tags": ["simpson", "animated"],
+            "nested": {"creator": "Matt Groening", "ids": [1, 2, 3]},
+        }
qdrant_client/local/qdrant_local.py (1)

739-744: Handle None metadata before calling .update.

If a collection has never stored metadata (default is None), update_collection(..., metadata=...) will raise AttributeError at Line 740 because _collection.config.metadata is None. Initialize the metadata dictionary before updating.

Apply this diff:

 if metadata is not None:
+    if _collection.config.metadata is None:
+        _collection.config.metadata = {}
     _collection.config.metadata.update(metadata)
     updated = True

Based on past review comments.

🧹 Nitpick comments (11)
tests/type_stub.py (1)

32-103: Consider using keyword arguments for improved maintainability.

While positional arguments may be intentional for type checking, using keyword arguments would make these test calls more resilient to API signature changes and improve readability. For example:

qdrant_client.create_collection(
    collection_name="collection",
    vectors_config=types.VectorParams(size=128, distance=rest_models.Distance.COSINE),
    # ... other parameters with explicit names
    strict_mode_config=rest_models.StrictModeConfig(),
    metadata={},
)

This is especially helpful when signatures evolve, as evidenced by this PR's changes.

qdrant_client/async_qdrant_remote.py (1)

649-661: Nit: unused kwargs in several methods.

kwargs is unused; consider removing or prefixing with underscore to appease linters.

Also applies to: 701-712, 831-841

qdrant_client/grpc/points_pb2.pyi (1)

2723-2724: Decay midpoint doc tightened; consider validating range upstream.

Optional: add client-side validation (HTTP/Pydantic) to keep midpoint in [0,1] as per doc to fail fast.

qdrant_client/grpc/collections_service_pb2.py (1)

23-23: Ruff E712: prefer not over == False in generated guard.

Tiny, no-behavior-change tweak to quiet linters; or ignore generated dirs in lint config.

Apply:

-if _descriptor._USE_C_DESCRIPTORS == False:
+if not _descriptor._USE_C_DESCRIPTORS:

Alternatively, exclude qdrant_client/grpc/*.py from E712 in your linter config to avoid churn in generated code.

qdrant_client/grpc/points_service_pb2.py (1)

23-23: Ruff E712: prefer not over == False in generated guard.

Same small linter fix as collections service; or exclude generated files from lint.

Apply:

-if _descriptor._USE_C_DESCRIPTORS == False:
+if not _descriptor._USE_C_DESCRIPTORS:
qdrant_client/async_qdrant_fastembed.py (2)

537-539: Use DeprecationWarning and proper stacklevel in deprecation notices

These should surface in user code, not inside the library, and use the right category.

-        show_warning_once(
-            "`add` method has been deprecated and will be removed in 1.17. Instead, inference can be done internally within regular methods like `upsert` by wrapping data into `models.Document` or `models.Image`."
-        )
+        show_warning_once(
+            "`add` method has been deprecated and will be removed in 1.17. Instead, inference can be done internally within regular methods like `upsert` by wrapping data into `models.Document` or `models.Image`.",
+            category=DeprecationWarning,
+            stacklevel=3,
+        )

Repeat the same change for query and query_batch deprecation calls.

Also applies to: 611-614, 693-696


592-596: Docstring still references ‘search’; should point to ‘query_points’

Update the guidance to reflect the new API.

-        If you want to use your own query vector, use `search` method instead.
+        If you want to use your own query vector, use `query_points` instead.
tests/congruence_tests/test_sparse_discovery.py (1)

79-85: Return .points for consistency with other tests

compare_client_results can unwrap, but keeping return types consistent reduces surprises.

-        return client.query_points(
+        return client.query_points(
             collection_name=COLLECTION_NAME,
             query=models.ContextQuery(context=[models.ContextPair(positive=10, negative=19)]),
             with_payload=True,
             limit=200,
             using="sparse-image",
-        )
+        ).points
tests/test_async_qdrant_client.py (1)

216-219: Avoid blocking the event loop in async tests

Use asyncio.sleep in @pytest.mark.asyncio tests.

-    time.sleep(
-        0.5
-    )  # wait param is not propagated https://github.com/qdrant/qdrant-client/issues/254
+    await asyncio.sleep(
+        0.5
+    )  # wait param is not propagated https://github.com/qdrant/qdrant-client/issues/254
qdrant_client/http/api/distributed_api.py (1)

4-6: Duplicate BaseModel import

Keep a single import to avoid shadowing and confusion.

-from pydantic import BaseModel
-from pydantic.main import BaseModel
+from pydantic import BaseModel
tests/conversions/fixtures.py (1)

1520-1526: Include text_any in Match fixtures to exercise new variant

Ensures conversions cover the newly added proto field.

-    "Match": [match_keyword, match_integer, match_bool, match_text],
+    "Match": [match_keyword, match_integer, match_bool, match_text, match_text_any],

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: collections_service.proto
# Protobuf Python Version: 4.25.1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Protobuf runtime compatibility check.

Header says “Protobuf Python Version: 4.25.1”. Ensure your declared protobuf dependency matches this generator/runtime to avoid import/runtime issues on older 3.x.

Run:


🏁 Script executed:

#!/usr/bin/env bash
# Find declared protobuf requirements
rg -nC2 '(protobuf|google-protobuf)' -g '!**/dist/**' -g '!**/build/**'
# List generated files noting declared version headers
rg -n 'Protobuf Python Version' qdrant_client/grpc/*_service_pb2.py qdrant_client/grpc/*_pb2.py

Length of output: 258914


Update protobuf minimum version to match generated code requirements.

The pyproject.toml currently declares protobuf = ">=3.20.0", but all generated _pb2.py files show # Protobuf Python Version: 4.25.1 headers. Code generated by protobuf 4.25.1 requires protobuf 4.x at runtime—protobuf 3.x versions (including 3.20.x) are incompatible and will cause import/runtime failures.

Fix: Update pyproject.toml to declare protobuf = ">=4.25.1" (or minimally >=4.0.0) to prevent users from installing incompatible protobuf 3.x versions. The lock file already pins protobuf 6.31.1, but the declared minimum must reflect the actual runtime requirement.

🤖 Prompt for AI Agents
pyproject.toml (root) lines 1-1: The generated _pb2.py files require protobuf
4.x (header shows 4.25.1) but pyproject currently allows protobuf >=3.20.0;
update the dependency spec in pyproject.toml to protobuf = ">=4.25.1" (or at
least ">=4.0.0") so runtime uses a compatible protobuf version, then
regenerate/update the lockfile if needed and run tests to ensure no
import/runtime protobuf errors.

Comment on lines +4462 to 4479
USAGE_FIELD_NUMBER: builtins.int
@property
def hits(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___FacetHit]: ...
time: builtins.float
"""Time spent to process"""
@property
def usage(self) -> global___Usage: ...
def __init__(
self,
*,
hits: collections.abc.Iterable[global___FacetHit] | None = ...,
time: builtins.float = ...,
usage: global___Usage | None = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["hits", b"hits", "time", b"time"]) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["_usage", b"_usage", "usage", b"usage"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["_usage", b"_usage", "hits", b"hits", "time", b"time", "usage", b"usage"]) -> None: ...
def WhichOneof(self, oneof_group: typing_extensions.Literal["_usage", b"_usage"]) -> typing_extensions.Literal["usage"] | None: ...

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

FacetResponse.usage: align HTTP surfaces and observability.

Good addition. Verify HTTP responses expose usage consistently and that metrics are documented.


🏁 Script executed:

#!/bin/bash
# HTTP models and API surfaces for usage
rg -nP -C3 --type=py '\bFacet(Response)?\b|usage\b' qdrant_client/http
# Tests asserting usage presence
rg -nP -C2 --type=py 'usage' tests

Length of output: 22868



---

🏁 Script executed:

```shell
#!/bin/bash
# Get full FacetResponse definition from HTTP models
rg -nA30 'class FacetResponse' qdrant_client/http/models/models.py

Length of output: 1603


🏁 Script executed:

#!/bin/bash
# Check if FacetResponse in HTTP has usage field
rg -nP 'class FacetResponse|usage.*Usage' qdrant_client/http/models/models.py | head -20

Length of output: 1403


🏁 Script executed:

#!/bin/bash
# Verify InlineResponse20020 (wrapper for FacetResponse endpoint)
rg -nA10 'class InlineResponse20020' qdrant_client/http/models/models.py

Length of output: 666


🏁 Script executed:

#!/bin/bash
# Check gRPC points_pb2.pyi for FacetResponse definition
rg -nA20 'class FacetResponse' qdrant_client/grpc/points_pb2.pyi

Length of output: 1053



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for test assertions on usage field in HTTP responses
rg -nP 'usage|Usage' tests/ --type=py | grep -i 'facet\|response\|assert' | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find Usage class definition and any documentation
rg -nB2 -A10 'class Usage' qdrant_client/http/models/models.py

Length of output: 481


🏁 Script executed:

#!/bin/bash
# Check if there are docstrings or comments explaining usage metric
rg -nB3 'Time spent to process|usage' qdrant_client/http/models/models.py | head -30

Length of output: 1774


🏁 Script executed:

#!/bin/bash
# Verify if HTTP FacetResponse has any other fields beyond hits
ast-grep --pattern 'class FacetResponse {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check for proto definitions that might show intended structure
fd -e proto -e protobuf | head -5

Length of output: 197



---

🏁 Script executed:

```shell
#!/bin/bash
# Check proto definition for FacetResponse to see source of truth
rg -nA15 'message FacetResponse' qdrant_client/proto/points.proto

Length of output: 501


🏁 Script executed:

#!/bin/bash
# Verify no HTTP test files exist that test FacetResponse directly
find tests -name '*facet*' -o -name '*test*http*' | head -10

Length of output: 36



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine the facet test file to understand test coverage
cat tests/congruence_tests/test_facet.py

Length of output: 6032


🏁 Script executed:

#!/bin/bash
# Verify if HTTP FacetResponse is generated or manually defined
rg -nB5 'class FacetResponse' qdrant_client/http/models/models.py | head -20

Length of output: 206


HTTP FacetResponse model is incomplete—missing time and usage fields.

The proto definition (points.proto:1060) specifies FacetResponse contains hits, time, and usage. The gRPC surface correctly exposes all three fields. The HTTP FacetResponse model (models.py:837) has only hits, causing misalignment between surfaces. The HTTP wrapper pattern (InlineResponse20020) adds these fields at the response envelope level, but FacetResponse itself should reflect the proto structure for consistency.

Add time: float and usage: Optional[Usage] fields to the HTTP FacetResponse model (qdrant_client/http/models/models.py:837) to align with the proto definition and gRPC surface.

🤖 Prompt for AI Agents
In qdrant_client/http/models/models.py around line 837, the HTTP FacetResponse
model lacks the proto-specified fields; add time: float and usage:
Optional[Usage] to the FacetResponse model definition (with from typing import
Optional and referencing the existing Usage model), give usage a default of None
and time a sensible default (e.g., 0.0) or make it Optional[float] if nullable,
and update the constructor/type hints and any model schema/serialization helpers
so the HTTP model matches the gRPC/proto structure.

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: points_service.proto
# Protobuf Python Version: 4.25.1
Copy link

@coderabbitai coderabbitai bot Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Protobuf runtime compatibility check.

Generated with 4.25.1—verify runtime/packaging requires protobuf >=4.25 to prevent subtle breakage.

Run:


🏁 Script executed:

#!/usr/bin/env bash
# Inspect declared protobuf version and cross-check all generated headers
rg -nC2 '(protobuf|google-protobuf)' -g '!**/dist/**' -g '!**/build/**'
rg -n 'Protobuf Python Version' qdrant_client/grpc/*_service_pb2.py qdrant_client/grpc/*_pb2.py

Length of output: 258914


Update pyproject.toml protobuf requirement to match generated code version.

The review comment identifies a real compatibility issue. The current dependency constraint is protobuf = ">=3.20.0", but the script output confirms that all protobuf-generated files in qdrant_client/grpc/ are explicitly marked as generated with version 4.25.1. This creates a runtime incompatibility risk: users could install protobuf 3.20.x, which is incompatible with code generated for 4.25.1.

Required action:
Update pyproject.toml line 27 to require at least protobuf 4.25:

protobuf = ">=4.25.0"
🤖 Prompt for AI Agents
In pyproject.toml at line 27, the protobuf version constraint must be raised to
match generated files (qdrant_client/grpc/* show Protobuf Python Version:
4.25.1); edit that line to require protobuf >=4.25.0 (protobuf = ">=4.25.0") and
then update your lock/install (e.g., poetry lock / pip-compile or reinstall
dependencies) so the new constraint is applied.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests still work for me

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current protobuf requirement in pyproject.toml
cat pyproject.toml | grep -A2 -B2 "protobuf"

Length of output: 345


@joein, your local tests passing is expected if you have protobuf >=4.25.0 installed in your environment. The concern is about the dependency declaration in pyproject.toml, which currently allows protobuf = ">=3.20.0".

When the protobuf compiler generates files with # Protobuf Python Version: 4.25.1, it typically means the generated code uses features or APIs from that version. While your environment works fine, users installing the package could end up with protobuf 3.20.x, which might cause issues.

Suggested verification:

# Test with minimum declared version to verify compatibility
pip install 'protobuf==3.20.0' && python -m pytest tests/

If that test passes, the current constraint is fine. If it fails, updating to protobuf = ">=4.25.0" would be safer. Since types-protobuf = "^4.21.0.5" is already in your dev dependencies, it seems the project is already using protobuf 4.x features.

The decision is ultimately yours as the maintainer—you might have insights about backward compatibility that aren't obvious from the diff alone.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
qdrant_client/embed/_inspection_cache.py (2)

759-768: Correct “lop-left” → “top-left” in GeoBoundingBox docs.

Minor typo repeated 3 times.

-        "description": "Geo filter request  Matches coordinates inside the rectangle, described by coordinates of lop-left and bottom-right edges",
+        "description": "Geo filter request. Matches coordinates inside the rectangle, described by coordinates of top-left and bottom-right edges",
@@
-                "description": "Geo filter request  Matches coordinates inside the rectangle, described by coordinates of lop-left and bottom-right edges",
+                "description": "Geo filter request. Matches coordinates inside the rectangle, described by coordinates of top-left and bottom-right edges",
@@
-                "description": "Geo filter request  Matches coordinates inside the rectangle, described by coordinates of lop-left and bottom-right edges",
+                "description": "Geo filter request. Matches coordinates inside the rectangle, described by coordinates of top-left and bottom-right edges",

2166-2171: Typo: “upto” → “up to”.

Small language fix in SparseIndexParams.full_scan_threshold.

-                "description": "We prefer a full scan search upto (excluding) this number of vectors.  Note: this is number of vectors, not KiloBytes.",
+                "description": "We prefer a full scan search up to (excluding) this number of vectors. Note: this is number of vectors, not kilobytes.",
qdrant_client/proto/points.proto (1)

281-286: Fix comment: vectors vs payload keys.

WithVectorsSelector.include refers to vector names, not payload keys.

 message WithVectorsSelector {
   oneof selector_options {
     bool enable = 1; // If `true` - return all vectors, if `false` - none
-    VectorsSelector include = 2; // List of payload keys to include into result
+    VectorsSelector include = 2; // List of vector names to include in the result
   }
 }
qdrant_client/async_qdrant_remote.py (3)

616-647: Convert grpc.WithLookup to REST before building QueryGroupsRequest (REST path).

Passing a grpc.WithLookup into REST models will fail serialization. Convert when grpc type is supplied.

         else:
             if isinstance(query_filter, grpc.Filter):
                 query_filter = GrpcToRest.convert_filter(model=query_filter)
             if isinstance(search_params, grpc.SearchParams):
                 search_params = GrpcToRest.convert_search_params(search_params)
             if isinstance(with_payload, grpc.WithPayloadSelector):
                 with_payload = GrpcToRest.convert_with_payload_selector(with_payload)
             if isinstance(lookup_from, grpc.LookupLocation):
                 lookup_from = GrpcToRest.convert_lookup_location(lookup_from)
+            if isinstance(with_lookup, grpc.WithLookup):
+                with_lookup = GrpcToRest.convert_with_lookup(with_lookup)
             query_request = models.QueryGroupsRequest(
                 shard_key=shard_key_selector,
                 prefetch=prefetch,
                 query=query,
                 using=using,
                 filter=query_filter,
                 params=search_params,
                 score_threshold=score_threshold,
                 limit=limit,
                 group_by=group_by,
                 group_size=group_size,
                 with_vector=with_vectors,
                 with_payload=with_payload,
                 with_lookup=with_lookup,
                 lookup_from=lookup_from,
             )

1719-1733: Always convert metadata for gRPC; don’t restrict to dict.

metadata can be various accepted Payload forms. Convert whenever it’s provided to avoid type mismatches at runtime.

-            if isinstance(metadata, dict):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)

1823-1840: Same fix for create_collection (gRPC path): convert metadata when provided.

-            if isinstance(metadata, dict):
-                metadata = RestToGrpc.convert_payload(metadata)
+            if metadata is not None:
+                metadata = RestToGrpc.convert_payload(metadata)
qdrant_client/grpc/collections_pb2.pyi (1)

2271-2305: Add missing field conversions to GrpcToRest and RestToGrpc converters.

The following proto fields are not being converted:

  1. CollectionInfo.warnings (repeated CollectionWarning) – missing from both convert_collection_info methods (lines 233 and 2491)
  2. WalConfigDiff.wal_retain_closed (optional uint64) – missing from both convert_wal_config_diff methods (lines 697 and 2783)
  3. CollectionInfo.indexed_vectors_count – missing only from RestToGrpc at line 2491

TextIndexParams.ascii_folding and HnswConfigDiff.inline_storage are already correctly handled.

♻️ Duplicate comments (6)
tests/test_qdrant_client.py (1)

999-1018: This segment was already reviewed in a previous commit. The past review comment suggests widening the metadata test payload to include mixed types (numbers, booleans, null, nested objects, arrays) to better exercise REST↔gRPC conversions.

qdrant_client/proto/collections.proto (1)

6-6: Critical: Fix the broken proto import.

The past review comment already identified this: import "json_with_int.proto" fails to compile (file doesn't exist per Buf static analysis). The map<string, Value> fields (lines 424, 437, 477) require google/protobuf/struct.proto instead.

This is a blocking compilation error that prevents proto generation.

qdrant_client/embed/_inspection_cache.py (2)

1482-1487: Clarify BM25 tokenizer field and add title.

Repeat of earlier suggestion; improves generated docs.

             "tokenizer": {
                 "anyOf": [{"$ref": "#/$defs/TokenizerType"}, {"type": "null"}],
                 "default": None,
-                "description": "Configuration of the local bm25 models.",
+                "description": "Tokenizer to apply before scoring; leave unset to inherit the collection-level default.",
+                "title": "Tokenizer",
             },

446-446: Fix missing space in “midpoint” description.

Same typo persists.

-                "description": "The midpoint of the decay. Should be between 0 and 1.Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
+                "description": "The midpoint of the decay. Should be between 0 and 1. Defaults to 0.5. Output will be this value when `|x - target| == scale`.",
qdrant_client/http/models/models.py (1)

3365-3365: Empty description for update_filter field.

The update_filter field has an empty description. This was already discussed in previous reviews and requires fixing in the autogeneration source.

qdrant_client/grpc/collections_pb2.pyi (1)

11-11: Use package-relative import for sibling proto module.

Absolute import can fail when installed as qdrant_client.grpc. Switch to relative to ensure resolution across environments.

-import json_with_int_pb2
+from . import json_with_int_pb2
🧹 Nitpick comments (5)
tests/test_qdrant_client.py (1)

1123-1131: Verify the shard key's initial state.

The test creates a shard key with initial_state=models.ReplicaState.ACTIVE but doesn't verify it was applied. Consider adding an assertion to confirm the shard's replica state matches the requested initial state.

             client.create_shard_key(
                 collection_name=COLLECTION_NAME,
                 shard_key=fish_shard_key,
                 initial_state=models.ReplicaState.ACTIVE,
             )
-            print("created shard key with replica state")
+            # Verify the shard was created with the expected state
+            cluster_info = client.get_collection_cluster_info(COLLECTION_NAME)
+            fish_shards = [s for s in cluster_info.local_shards if s.shard_key == fish_shard_key]
+            assert len(fish_shards) > 0, "Fish shard not found"
+            assert fish_shards[0].state == models.ReplicaState.ACTIVE
qdrant_client/embed/_inspection_cache.py (2)

2533-2537: Deduplicate on_disk default phrasing.

Remove repeated “Default…” sentence.

-                "description": "If true, store the index on disk. Default: false. Default is false.",
+                "description": "If true, store the index on disk. Default: false.",

2575-2581: Add title/description for TextIndexParams.tokenizer.

Parallels BM25 tokenizer; improves clarity.

             "tokenizer": {
                 "anyOf": [{"$ref": "#/$defs/TokenizerType"}, {"type": "null"}],
                 "default": None,
-                "description": "",
+                "description": "Tokenizer to use for indexing and queries; leave unset to use the collection default.",
+                "title": "Tokenizer",
             },
qdrant_client/proto/points.proto (1)

122-125: Document ShardKeySelector.fallback semantics.

Add a brief comment so users know when fallback applies.

 message ShardKeySelector {
   repeated ShardKey shard_keys = 1; // List of shard keys which should be used in the request
-  optional ShardKey fallback = 2;
+  optional ShardKey fallback = 2; // Optional fallback shard key used when the target shard(s) are unavailable or planner selects the fallback path
 }
qdrant_client/async_qdrant_remote.py (1)

560-560: Silence Ruff ARG002 for unused **kwargs in autogenerated client.

These are intentional for forward-compat; prefer config over code edits.

Add per-file ignore in pyproject (keeps generator output clean):

# pyproject.toml
[tool.ruff.lint.per-file-ignores]
"qdrant_client/async_qdrant_remote.py" = ["ARG002"]

Also applies to: 659-659, 711-711, 839-839, 2317-2317

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b6e7cc and 2529d21.

📒 Files selected for processing (11)
  • qdrant_client/async_qdrant_remote.py (32 hunks)
  • qdrant_client/conversions/common_types.py (4 hunks)
  • qdrant_client/conversions/conversion.py (33 hunks)
  • qdrant_client/embed/_inspection_cache.py (62 hunks)
  • qdrant_client/grpc/collections_pb2.pyi (33 hunks)
  • qdrant_client/http/models/models.py (46 hunks)
  • qdrant_client/proto/collections.proto (14 hunks)
  • qdrant_client/proto/points.proto (15 hunks)
  • qdrant_client/qdrant_remote.py (32 hunks)
  • tests/conversions/fixtures.py (22 hunks)
  • tests/test_qdrant_client.py (22 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
qdrant_client/conversions/common_types.py (3)
qdrant_client/grpc/points_pb2.pyi (1)
  • PointId (249-271)
qdrant_client/http/models/models.py (2)
  • QueryRequest (2121-2153)
  • ReplicaState (2380-2397)
qdrant_client/grpc/collections_pb2.pyi (1)
  • ReplicaState (268-268)
tests/test_qdrant_client.py (7)
qdrant_client/async_qdrant_remote.py (7)
  • query_points (377-484)
  • query_batch_points (486-528)
  • create_collection (1782-1875)
  • get_collection (1655-1669)
  • update_collection (1685-1764)
  • create_shard_key (2308-2355)
  • query_points_groups (530-647)
qdrant_client/qdrant_remote.py (7)
  • query_points (428-553)
  • query_batch_points (555-602)
  • create_collection (1938-2046)
  • get_collection (1805-1817)
  • update_collection (1832-1921)
  • create_shard_key (2495-2540)
  • query_points_groups (604-736)
qdrant_client/local/async_qdrant_local.py (7)
  • query_points (349-382)
  • query_batch_points (384-402)
  • create_collection (716-744)
  • get_collection (664-666)
  • update_collection (675-692)
  • create_shard_key (913-924)
  • query_points_groups (404-458)
qdrant_client/local/qdrant_local.py (7)
  • query_points (382-418)
  • query_batch_points (420-441)
  • create_collection (769-800)
  • get_collection (714-716)
  • update_collection (725-744)
  • create_shard_key (978-989)
  • query_points_groups (443-498)
qdrant_client/qdrant_client.py (7)
  • query_points (273-446)
  • query_batch_points (229-271)
  • create_collection (1632-1720)
  • get_collection (1530-1541)
  • update_collection (1556-1610)
  • create_shard_key (2355-2385)
  • query_points_groups (448-614)
qdrant_client/http/models/models.py (6)
  • Filter (914-924)
  • QueryRequest (2121-2153)
  • RecommendQuery (2261-2262)
  • RecommendInput (2249-2258)
  • ReplicaState (2380-2397)
  • SparseVector (3028-3034)
tests/utils.py (1)
  • read_version (9-25)
tests/conversions/fixtures.py (4)
qdrant_client/grpc/points_pb2.pyi (66)
  • Match (4769-4822)
  • match (4722-4723)
  • field (4599-4599)
  • params (1512-1513)
  • params (1668-1669)
  • params (1928-1929)
  • params (2076-2077)
  • params (2250-2251)
  • params (2907-2908)
  • params (2979-2980)
  • params (3120-3121)
  • multi_dense (444-445)
  • multi_dense (500-501)
  • multi_dense (591-591)
  • MultiDenseVector (557-568)
  • vectors (562-562)
  • vectors (845-846)
  • vectors (875-876)
  • vectors (1222-1222)
  • vectors (1254-1254)
  • vectors (1272-1272)
  • vectors (1293-1293)
  • vectors (3641-3642)
  • vectors (3890-3891)
  • vectors (4255-4255)
  • vectors (5115-5115)
  • DenseVector (523-534)
  • data (280-280)
  • data (430-431)
  • data (486-487)
  • data (528-528)
  • VectorsOutput (1285-1302)
  • vector (1270-1270)
  • vector (1291-1291)
  • vector (1501-1502)
  • vector (1657-1658)
  • vector (2183-2183)
  • VectorOutput (476-519)
  • dense (438-439)
  • dense (494-495)
  • dense (587-587)
  • NamedVectorsOutput (1232-1260)
  • sparse (441-442)
  • sparse (497-498)
  • sparse (589-589)
  • SparseVector (538-553)
  • values (544-544)
  • indices (433-434)
  • indices (489-490)
  • indices (546-546)
  • quantization (1442-1445)
  • Vectors (1264-1281)
  • Vector (417-472)
  • ShardKeySelector (615-638)
  • shard_keys (626-627)
  • fallback (629-629)
  • id (585-585)
  • id (842-843)
  • id (2181-2181)
  • id (3880-3881)
  • id (3952-3953)
  • id (4251-4251)
  • id (5111-5111)
  • Query (2823-2880)
  • rrf (2862-2863)
  • Rrf (2804-2819)
qdrant_client/http/models/models.py (7)
  • StrictModeConfig (3124-3169)
  • CollectionConfig (241-261)
  • SparseVector (3028-3034)
  • TextIndexParams (3248-3268)
  • TokenizerType (3275-3279)
  • Rrf (2468-2473)
  • ReplicaState (2380-2397)
qdrant_client/grpc/collections_pb2.pyi (25)
  • StrictModeConfig (1231-1355)
  • metadata (1528-1529)
  • metadata (1636-1637)
  • metadata (1853-1854)
  • CollectionConfig (1807-1871)
  • params (496-496)
  • params (517-517)
  • params (1618-1619)
  • params (1835-1836)
  • params (2223-2224)
  • hnsw_config (356-357)
  • hnsw_config (401-402)
  • hnsw_config (1495-1496)
  • hnsw_config (1621-1622)
  • hnsw_config (1838-1839)
  • optimizer_config (1841-1842)
  • wal_config (1498-1499)
  • wal_config (1844-1845)
  • strict_mode_config (1525-1526)
  • strict_mode_config (1633-1634)
  • strict_mode_config (1850-1851)
  • TextIndexParams (1997-2059)
  • TokenizerType (232-232)
  • ShardKey (2489-2506)
  • ReplicaState (268-268)
qdrant_client/grpc/json_with_int_pb2.pyi (1)
  • Value (84-131)
qdrant_client/conversions/conversion.py (3)
qdrant_client/grpc/collections_pb2.pyi (25)
  • metadata (1528-1529)
  • metadata (1636-1637)
  • metadata (1853-1854)
  • HasField (379-379)
  • HasField (415-415)
  • HasField (443-443)
  • HasField (475-475)
  • HasField (505-505)
  • HasField (526-526)
  • HasField (548-548)
  • HasField (574-574)
  • HasField (662-662)
  • HasField (706-706)
  • HasField (754-754)
  • HasField (851-851)
  • HasField (896-896)
  • HasField (926-926)
  • HasField (1029-1029)
  • HasField (1071-1071)
  • shard_key (2524-2525)
  • shard_key (2554-2555)
  • shard_key (2608-2608)
  • shard_key (2808-2809)
  • shard_key (2844-2845)
  • ReplicaState (268-268)
qdrant_client/grpc/points_pb2.pyi (86)
  • AcornSearchParams (1386-1420)
  • rrf (2862-2863)
  • Rrf (2804-2819)
  • update_filter (669-670)
  • update_filter (811-812)
  • update_filter (3456-3457)
  • update_filter (3615-3616)
  • points (661-661)
  • points (707-708)
  • points (802-803)
  • points (1036-1037)
  • points (3451-3451)
  • points (3609-3610)
  • points (3663-3664)
  • points (3684-3685)
  • points (4972-4973)
  • points (5052-5052)
  • shard_key (3893-3894)
  • shard_key (4257-4258)
  • shard_key_selector (666-667)
  • shard_key_selector (713-714)
  • shard_key_selector (760-761)
  • shard_key_selector (808-809)
  • shard_key_selector (881-882)
  • shard_key_selector (945-946)
  • shard_key_selector (998-999)
  • shard_key_selector (1042-1043)
  • shard_key_selector (1529-1530)
  • shard_key_selector (1690-1691)
  • shard_key_selector (1822-1823)
  • shard_key_selector (1872-1873)
  • shard_key_selector (1956-1957)
  • shard_key_selector (2109-2110)
  • shard_key_selector (2268-2269)
  • shard_key_selector (2360-2361)
  • update_vectors (3722-3722)
  • UpdateVectors (3602-3629)
  • ShardKeySelector (615-638)
  • fallback (629-629)
  • shard_keys (626-627)
  • target (2236-2237)
  • target (2442-2443)
  • target (2718-2719)
  • Match (4769-4822)
  • sparse (441-442)
  • sparse (497-498)
  • sparse (589-589)
  • SparseVector (538-553)
  • values (544-544)
  • indices (433-434)
  • indices (489-490)
  • indices (546-546)
  • VectorOutput (476-519)
  • PointId (249-271)
  • multi_dense (444-445)
  • multi_dense (500-501)
  • multi_dense (591-591)
  • MultiDenseVector (557-568)
  • vectors (562-562)
  • vectors (845-846)
  • vectors (875-876)
  • vectors (1222-1222)
  • vectors (1254-1254)
  • vectors (1272-1272)
  • vectors (1293-1293)
  • vectors (3641-3642)
  • vectors (3890-3891)
  • vectors (4255-4255)
  • vectors (5115-5115)
  • DenseVector (523-534)
  • data (280-280)
  • data (430-431)
  • data (486-487)
  • data (528-528)
  • vector (1270-1270)
  • vector (1291-1291)
  • vector (1501-1502)
  • vector (1657-1658)
  • vector (2183-2183)
  • Vector (417-472)
  • dense (438-439)
  • dense (494-495)
  • dense (587-587)
  • Query (2823-2880)
  • PointsUpdateOperation (3441-3745)
  • PointStructList (3444-3470)
qdrant_client/http/models/models.py (8)
  • AcornSearchParams (36-48)
  • MatchTextAny (1590-1595)
  • RrfQuery (2476-2477)
  • Rrf (2468-2473)
  • UpdateVectors (3362-3365)
  • ReplicaState (2380-2397)
  • ShardKeyWithFallback (2789-2791)
  • SparseVector (3028-3034)
qdrant_client/grpc/collections_pb2.pyi (3)
qdrant_client/http/models/models.py (2)
  • ReplicaState (2380-2397)
  • CollectionWarning (385-386)
qdrant_client/grpc/points_pb2.pyi (62)
  • ClearField (224-224)
  • ClearField (244-244)
  • ClearField (270-270)
  • ClearField (286-286)
  • ClearField (308-308)
  • ClearField (327-327)
  • ClearField (349-349)
  • ClearField (370-370)
  • ClearField (392-392)
  • ClearField (413-413)
  • ClearField (466-466)
  • ClearField (513-513)
  • ClearField (534-534)
  • ClearField (553-553)
  • ClearField (568-568)
  • ClearField (610-610)
  • HasField (243-243)
  • HasField (269-269)
  • HasField (307-307)
  • HasField (348-348)
  • HasField (369-369)
  • HasField (391-391)
  • HasField (412-412)
  • HasField (465-465)
  • HasField (512-512)
  • HasField (609-609)
  • HasField (636-636)
  • HasField (681-681)
  • HasField (724-724)
  • HasField (775-775)
  • HasField (823-823)
  • HasField (853-853)
  • WhichOneof (245-245)
  • WhichOneof (271-271)
  • WhichOneof (468-468)
  • WhichOneof (470-470)
  • WhichOneof (472-472)
  • WhichOneof (515-515)
  • WhichOneof (517-517)
  • WhichOneof (519-519)
  • WhichOneof (611-611)
  • WhichOneof (638-638)
  • WhichOneof (684-684)
  • WhichOneof (686-686)
  • WhichOneof (688-688)
  • WhichOneof (690-690)
  • WhichOneof (727-727)
  • WhichOneof (729-729)
  • value (300-300)
  • value (341-341)
  • value (384-384)
  • value (914-914)
  • value (1210-1210)
  • value (1242-1242)
  • value (2484-2484)
  • value (3292-3293)
  • value (3482-3482)
  • value (3533-3533)
  • value (3862-3862)
  • value (4235-4235)
  • value (5097-5097)
  • value (5190-5190)
qdrant_client/grpc/json_with_int_pb2.pyi (2)
  • value (61-61)
  • Value (84-131)
qdrant_client/http/models/models.py (2)
qdrant_client/grpc/points_pb2.pyi (33)
  • AcornSearchParams (1386-1420)
  • b (3394-3395)
  • update_filter (669-670)
  • update_filter (811-812)
  • update_filter (3456-3457)
  • update_filter (3615-3616)
  • points (661-661)
  • points (707-708)
  • points (802-803)
  • points (1036-1037)
  • points (3451-3451)
  • points (3609-3610)
  • points (3663-3664)
  • points (3684-3685)
  • points (4972-4973)
  • points (5052-5052)
  • filter (1504-1505)
  • filter (1660-1661)
  • filter (1805-1806)
  • filter (1920-1921)
  • filter (2068-2069)
  • filter (2242-2243)
  • filter (2352-2353)
  • filter (2904-2905)
  • filter (2976-2977)
  • filter (3117-3118)
  • filter (3218-3219)
  • filter (3321-3322)
  • filter (4605-4605)
  • filter (4694-4695)
  • filter (5054-5054)
  • Rrf (2804-2819)
  • fallback (629-629)
qdrant_client/grpc/collections_pb2.pyi (10)
  • stopwords (2020-2021)
  • stemmer (2025-2026)
  • metadata (1528-1529)
  • metadata (1636-1637)
  • metadata (1853-1854)
  • warnings (2289-2290)
  • CollectionWarning (777-787)
  • languages (1982-1983)
  • custom (1985-1986)
  • WalConfigDiff (907-933)
qdrant_client/qdrant_remote.py (3)
qdrant_client/http/models/models.py (15)
  • Prefetch (2023-2047)
  • Filter (914-924)
  • SearchParams (2634-2652)
  • LookupLocation (1540-1553)
  • GroupsResult (1051-1052)
  • WithLookup (3570-3577)
  • QueryGroupsRequest (2077-2118)
  • SearchMatrixPairsResponse (2614-2615)
  • SearchMatrixRequest (2618-2631)
  • SearchMatrixOffsetsResponse (2597-2601)
  • PointsList (2005-2011)
  • Batch (76-79)
  • PointsBatch (1996-2002)
  • UpdateVectors (3362-3365)
  • ReplicaState (2380-2397)
qdrant_client/conversions/conversion.py (14)
  • RestToGrpc (2417-4569)
  • convert_query (1335-1375)
  • convert_query (3493-3530)
  • convert_prefetch_query (1378-1396)
  • convert_prefetch_query (3635-3655)
  • GrpcToRest (141-2407)
  • convert_groups_result (1753-1756)
  • convert_groups_result (3981-3984)
  • convert_filter (167-183)
  • convert_filter (2419-2439)
  • convert_shard_key_selector (2094-2113)
  • convert_shard_key_selector (4372-4388)
  • convert_shard_key (2052-2057)
  • convert_shard_key (4329-4335)
qdrant_client/async_qdrant_remote.py (5)
  • grpc_points (324-333)
  • http (369-375)
  • query_points_groups (530-647)
  • search_matrix_pairs (649-699)
  • search_matrix_offsets (701-751)
🪛 Buf (1.59.0)
qdrant_client/proto/collections.proto

6-6: import "json_with_int.proto": file does not exist

(COMPILE)

🪛 Ruff (0.14.4)
qdrant_client/conversions/conversion.py

2091-2091: Avoid specifying long messages outside the exception class

(TRY003)


2109-2111: Avoid specifying long messages outside the exception class

(TRY003)


4369-4369: Avoid specifying long messages outside the exception class

(TRY003)

qdrant_client/async_qdrant_remote.py

560-560: Unused method argument: kwargs

(ARG002)


659-659: Unused method argument: kwargs

(ARG002)


711-711: Unused method argument: kwargs

(ARG002)


839-839: Unused method argument: kwargs

(ARG002)


2317-2317: Unused method argument: kwargs

(ARG002)

qdrant_client/qdrant_remote.py

634-634: Unused method argument: kwargs

(ARG002)


748-748: Unused method argument: kwargs

(ARG002)


804-804: Unused method argument: kwargs

(ARG002)


2504-2504: Unused method argument: kwargs

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
  • GitHub Check: Python 3.9.x on ubuntu-latest test
  • GitHub Check: Python 3.11.x on ubuntu-latest test
  • GitHub Check: Python 3.13.x on ubuntu-latest test
  • GitHub Check: Python 3.12.x on ubuntu-latest test
  • GitHub Check: Python 3.10.x on ubuntu-latest test
🔇 Additional comments (25)
tests/conversions/fixtures.py (10)

38-38: LGTM: text_any matching support properly wired.

The new text_any field for matching any word in text is correctly integrated through the fixture chain (Match → FieldCondition → Condition → Filter).

Also applies to: 52-52, 129-129, 150-150


297-297: LGTM: New configuration fields added.

The additions of inline_storage to HNSW config and max_payload_index_count to strict mode config are consistent with the updated protobuf definitions.

Also applies to: 363-363


371-388: LGTM: Collection metadata support added.

The metadata fixture and collection_config_w_metadata variant correctly demonstrate the new collection-level metadata feature, properly registered in the fixtures dictionary.


404-425: LGTM: Vector representation correctly migrated.

The fixtures properly migrate from the deprecated data/vectors_count fields to the new structured format (dense, sparse, multi_dense), aligning with the PR's core objective of removing vectors_count.

Also applies to: 444-467


481-487: Clarify the intent of commented-out fields.

The scored_point_order_value_float fixture has several fields commented out (payload, score, version, order_value), leaving only id and vectors. If this is intentionally testing a minimal/sparse case, consider adding a comment explaining the purpose to avoid confusion.


509-514: LGTM: New search and indexing parameters added.

The acorn search parameters and ascii_folding text index parameters are correctly integrated, providing test coverage for these new features.

Also applies to: 542-549


872-917: LGTM: Point vectors migrated to new representations.

The point_struct and many_vectors fixtures consistently use the new dense/sparse/multi_dense vector representations, providing comprehensive test coverage.


981-981: LGTM: Metadata and shard key fallback features integrated.

The metadata parameter in update_collection_3 and the new shard key selector with fallback are properly implemented and used in test fixtures.

Also applies to: 1051-1080


1114-1126: LGTM: RRF queries and updated point vectors properly registered.

The RRF query variants (with k, default, explicit None) and updated point vector fixtures provide comprehensive test coverage for the new query types and vector representations.

Also applies to: 1394-1396, 1584-1585, 1626-1628


1453-1462: LGTM: Comprehensive ReplicaState enum coverage.

All ReplicaState variants are properly defined and registered, providing complete test coverage for the expanded replica state enumeration.

Also applies to: 1636-1647

tests/test_qdrant_client.py (2)

396-402: LGTM: Consistent API migration from search to query_points.

The migration from the search API to the new query_points API is consistent throughout:

  • Correctly uses query parameter instead of query_vector
  • Properly accesses results via .points on QueryResponse
  • Batch operations correctly use QueryRequest objects
  • Recommend operations correctly wrapped in RecommendQuery

Also applies to: 408-414, 535-541, 552-558, 562-623, 626-639, 688-735, 818-837, 1097-1105, 1168-1240, 1298-1316, 1369-1396, 1888-1947


2010-2012: Clarify the REST client workaround in gRPC test.

The code creates a REST client within a gRPC test to fetch the payload schema. This suggests a potential issue with the gRPC path. Please clarify:

  • Is there a known bug with gRPC's get_collection for parametrized indexes?
  • Should this workaround be documented or is it a temporary fix?
qdrant_client/proto/collections.proto (3)

368-387: LGTM: Comprehensive strict mode configuration.

The expanded StrictModeConfig provides granular control over collection limits:

  • Query/timeout limits
  • Filtering configuration
  • Search parameters
  • Batch size limits
  • Storage limits
  • Multi-vector and sparse vector configs

All fields are well-documented and optional.


202-208: LGTM: Well-documented inline storage feature.

The new inline_storage field for HNSW configuration is clearly documented with its trade-offs and restrictions.


165-167: LGTM: Incremental proto additions align with PR objectives.

The following additions are well-structured:

  • CollectionWarning message for collection-level warnings
  • wal_retain_closed for WAL configuration
  • ascii_folding for text normalization
  • Deprecation of vectors_count (reserved field 3)
  • warnings field on CollectionInfo
  • ActiveRead replica state
  • initial_state for shard key creation

All maintain backward compatibility through optional fields.

Also applies to: 230-230, 523-523, 572-580, 640-640, 744-744

qdrant_client/conversions/common_types.py (1)

60-60: UUID conversion support is properly implemented.

The verification confirms that the conversion layer correctly handles UUID instances:

  • RestToGrpc.convert_extended_point_id (line 3027-3030) explicitly checks for isinstance(model, uuid.UUID) and converts to string before passing to grpc.PointId(uuid=model)
  • GrpcToRest.convert_point_id (line 499-500) properly extracts the uuid field from gRPC PointId

The addition of UUID to the PointId union is safe and fully supported by the existing conversion infrastructure.

qdrant_client/proto/points.proto (6)

68-79: Deprecation path for legacy Vector fields looks good.

Marking data/indices/vectors_count as deprecated while introducing oneof variants maintains wire compatibility. No issues spotted.

Also applies to: 81-90


132-139: update_filter on UpsertPoints: nice addition.

Filter-constrained upserts are valuable; wire fields/numbers look fine.

Please ensure client helpers surface this field in high-level APIs (sync/async/local) to avoid silent omission.


160-167: update_filter on UpdatePointVectors: looks good.

Consistent with UpsertPoints; no proto issues.


311-331: ACORN params integration acknowledged.

New message and SearchParams.acorn field are coherent with HTTP schema.

Confirm doc/source-of-truth for the default of max_selectivity (0.4) matches server behavior to avoid client-side surprises.

Also applies to: 356-360


594-599: RRF support wired correctly.

New Rrf message and Query.rrf variant align with fusion docs.

Also applies to: 699-716


1141-1153: Match.text_any addition is consistent with HTTP model.

No wire or naming issues observed.

qdrant_client/embed/_inspection_cache.py (1)

2860-2864: Fix schema/description mismatch in source model, not auto-generated cache.

The discrepancy exists in the source Pydantic model (qdrant_client/http/models/models.py:45-47), not _inspection_cache.py. The cache file is auto-generated by populate_inspection_cache.py from the model definition, so modifying it directly would be overwritten on regeneration.

Real issue: The model has default=None but its description states "Default is 0.4". Choose one approach:

  1. Change qdrant_client/http/models/models.py line 46 from default=None to default=0.4 (if client-side default is intended)
  2. Update the description to match default=None (if server handles the default)

Verify server-side ACORN behavior to determine which fix is correct.

qdrant_client/http/models/models.py (1)

36-49: LGTM: New model classes are well-structured.

The new classes (AcornSearchParams, Bm25Config, CollectionSnapshotTelemetry, CollectionWarning, MatchTextAny, PeerMetadata, ReplicatePoints/ReplicatePointsOperation, Rrf/RrfQuery, ShardKeyWithFallback, StopwordsSet) follow consistent patterns with proper type annotations, optional field handling, and descriptive documentation.

Also applies to: 108-152, 354-358, 385-387, 1590-1596, 1954-1960, 2400-2407, 2468-2478, 2789-2792, 3115-3121

qdrant_client/async_qdrant_remote.py (1)

1699-1737: LGTM on metadata threading to REST and new collection fields.

REST paths correctly pass metadata into models.UpdateCollection/CreateCollection. Once gRPC conversion is unconditional (above), both transports align.

Optionally run tests that exercise updating and creating collections with metadata via both transports.

Also applies to: 1803-1844

Comment on lines +268 to 269
metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve optional metadata when gRPC map is empty.

model.metadata is always a map container, so this condition is always true. As a result we now surface {} even when the server never populated metadata, which downstream REST conversions treat as “clear metadata” and will wipe existing collection metadata. Please keep this field at None when the map is empty (apply the same fix to other metadata conversions, e.g. convert_update_collection).

-            metadata=cls.convert_payload(model.metadata) if model.metadata is not None else None,
+            metadata=cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None,
🤖 Prompt for AI Agents
In qdrant_client/conversions/conversion.py around lines 268-269, the code always
preserves metadata because model.metadata (a map container) evaluates truthy
even when empty; change the assignment to set metadata to None when the map is
empty (e.g., use model.metadata if model.metadata and len(model.metadata) > 0
else None) so empty maps are not treated as "clear metadata". Apply the same
emptiness check to other metadata conversion sites mentioned (such as
convert_update_collection) so empty gRPC maps become None rather than {}.

Comment on lines +805 to 806
metadata=(cls.convert_payload(model.metadata) if model.metadata is not None else None),
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t fabricate empty metadata updates.

model.metadata is a map and never None, so we now emit {} even when no metadata diff was provided. That causes the REST model to think there’s an explicit update and will reset metadata to empty. Guard on len(model.metadata) so we only forward non-empty updates (same pattern as above).

-            metadata=(cls.convert_payload(model.metadata) if model.metadata is not None else None),
+            metadata=(
+                cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None
+            ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
metadata=(cls.convert_payload(model.metadata) if model.metadata is not None else None),
)
metadata=(
cls.convert_payload(model.metadata) if len(model.metadata) != 0 else None
),
)
🤖 Prompt for AI Agents
In qdrant_client/conversions/conversion.py around lines 805-806, the code always
emits metadata=(cls.convert_payload(model.metadata) if model.metadata is not
None else None) which produces {} when model.metadata is an empty dict and thus
signals an explicit metadata reset; change this to only forward metadata when
model.metadata contains entries (e.g., check len(model.metadata) > 0) and
otherwise set metadata to None so empty updates are not fabricated, following
the same guard pattern used elsewhere.

Comment on lines +2672 to 2676
def convert_acorn_search_params(cls, model: rest.AcornSearchParams) -> grpc.AcornSearchParams:
return grpc.AcornSearchParams(
enable=model.enable if model.enable is not None else None,
max_selectivity=model.max_selectivity if model.max_selectivity is not None else None,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid assigning None to protobuf scalars in ACORN params.

Passing enable=None / max_selectivity=None into the protobuf constructor raises TypeError because scalar fields can’t receive None. Build the message first and set each attribute only when the value exists.

-        return grpc.AcornSearchParams(
-            enable=model.enable if model.enable is not None else None,
-            max_selectivity=model.max_selectivity if model.max_selectivity is not None else None,
-        )
+        acorn = grpc.AcornSearchParams()
+        if model.enable is not None:
+            acorn.enable = model.enable
+        if model.max_selectivity is not None:
+            acorn.max_selectivity = model.max_selectivity
+        return acorn
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def convert_acorn_search_params(cls, model: rest.AcornSearchParams) -> grpc.AcornSearchParams:
return grpc.AcornSearchParams(
enable=model.enable if model.enable is not None else None,
max_selectivity=model.max_selectivity if model.max_selectivity is not None else None,
)
def convert_acorn_search_params(cls, model: rest.AcornSearchParams) -> grpc.AcornSearchParams:
acorn = grpc.AcornSearchParams()
if model.enable is not None:
acorn.enable = model.enable
if model.max_selectivity is not None:
acorn.max_selectivity = model.max_selectivity
return acorn
🤖 Prompt for AI Agents
In qdrant_client/conversions/conversion.py around lines 2672 to 2676, avoid
passing None into protobuf scalar fields; instead instantiate
grpc.AcornSearchParams() first and then set enable and max_selectivity only when
model.enable and model.max_selectivity are not None (e.g., if model.enable is
not None: msg.enable = model.enable), so that you never assign None to scalar
fields and only set attributes when values exist.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/congruence_tests/test_sparse_discovery.py (1)

78-84: Missing .points extraction from QueryResponse.

The function signature indicates it should return list[models.ScoredPoint], but query_points returns a QueryResponse object. You need to append .points to extract the points list, as done consistently in all other test functions in this file (lines 120, 142, 168, 194, 212, 238, 295, 307).

Apply this diff:

     def f(client: QdrantBase, **kwargs: dict[str, Any]) -> list[models.ScoredPoint]:
         return client.query_points(
             collection_name=COLLECTION_NAME,
             query=models.ContextQuery(context=[models.ContextPair(positive=10, negative=19)]),
             with_payload=True,
             limit=200,
             using="sparse-image",
-        )
+        ).points
🧹 Nitpick comments (2)
tests/congruence_tests/test_sparse_search.py (1)

31-69: Consider standardizing parameter order for consistency.

The methods use inconsistent parameter ordering—some place using before query, others after. While Python's named parameters make this functionally correct, standardizing the order would improve readability and maintainability.

For example, consistently use:

client.query_points(
    collection_name=COLLECTION_NAME,
    query=self.query_text,
    using="sparse-text",
    ...
)
tests/congruence_tests/test_sparse_discovery.py (1)

249-249: Consider removing unused kwargs parameter.

The kwargs parameter is declared but never used in this function. If it's not required by the compare_client_results callback interface, consider removing it for cleaner signatures. The same pattern appears at line 300.

Based on static analysis.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2529d21 and 6db5500.

📒 Files selected for processing (8)
  • qdrant_client/client_base.py (0 hunks)
  • qdrant_client/local/multi_distances.py (0 hunks)
  • qdrant_client/migrate/migrate.py (1 hunks)
  • tests/congruence_tests/test_collections.py (0 hunks)
  • tests/congruence_tests/test_delete_points.py (5 hunks)
  • tests/congruence_tests/test_sparse_discovery.py (10 hunks)
  • tests/congruence_tests/test_sparse_idf_search.py (1 hunks)
  • tests/congruence_tests/test_sparse_search.py (3 hunks)
💤 Files with no reviewable changes (3)
  • qdrant_client/local/multi_distances.py
  • tests/congruence_tests/test_collections.py
  • qdrant_client/client_base.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • qdrant_client/migrate/migrate.py
🧰 Additional context used
🧬 Code graph analysis (4)
tests/congruence_tests/test_sparse_discovery.py (7)
qdrant_client/client_base.py (3)
  • query_points (40-67)
  • QdrantBase (6-396)
  • query_batch_points (32-38)
qdrant_client/qdrant_remote.py (2)
  • query_points (428-553)
  • query_batch_points (555-602)
qdrant_client/local/qdrant_local.py (2)
  • query_points (382-418)
  • query_batch_points (420-441)
qdrant_client/local/local_collection.py (2)
  • query_points (705-760)
  • discover (1808-1870)
qdrant_client/http/models/models.py (9)
  • ContextQuery (454-455)
  • ContextPair (449-451)
  • DiscoverQuery (708-709)
  • DiscoverInput (701-705)
  • LookupLocation (1540-1553)
  • QueryResponse (2160-2161)
  • QueryRequest (2121-2153)
  • Filter (914-924)
  • ScoredPoint (2520-2531)
qdrant_client/local/distances.py (2)
  • ContextQuery (55-57)
  • ContextPair (38-44)
tests/congruence_tests/test_discovery.py (13)
  • f (64-72)
  • f (83-91)
  • f (102-109)
  • f (123-140)
  • f (151-164)
  • f (175-187)
  • f (198-209)
  • f (222-233)
  • f (246-257)
  • f (268-275)
  • f (348-376)
  • f (384-395)
  • f (403-410)
tests/congruence_tests/test_sparse_search.py (5)
qdrant_client/client_base.py (2)
  • query_points (40-67)
  • QdrantBase (6-396)
qdrant_client/local/qdrant_local.py (1)
  • query_points (382-418)
qdrant_client/qdrant_client.py (1)
  • query_points (273-446)
qdrant_client/local/local_collection.py (1)
  • query_points (705-760)
tests/congruence_tests/test_search.py (8)
  • simple_search_image (38-45)
  • simple_search_code (47-54)
  • simple_search_text_offset (56-64)
  • search_score_threshold (77-105)
  • simple_search_text_select_payload (107-114)
  • search_payload_exclude (116-123)
  • simple_search_image_select_vector (125-133)
  • filter_search_text (135-145)
tests/congruence_tests/test_delete_points.py (3)
qdrant_client/client_base.py (1)
  • query_points (40-67)
qdrant_client/qdrant_client.py (1)
  • query_points (273-446)
qdrant_client/local/local_collection.py (1)
  • query_points (705-760)
tests/congruence_tests/test_sparse_idf_search.py (3)
qdrant_client/client_base.py (1)
  • query_points (40-67)
qdrant_client/qdrant_remote.py (1)
  • query_points (428-553)
qdrant_client/qdrant_client.py (1)
  • query_points (273-446)
🪛 Ruff (0.14.4)
tests/congruence_tests/test_sparse_discovery.py

249-249: Unused function argument: kwargs

(ARG001)


300-300: Unused function argument: kwargs

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Pages changed - poetic-froyo-8baba7
  • GitHub Check: Python 3.13.x on ubuntu-latest test
  • GitHub Check: Python 3.12.x on ubuntu-latest test
  • GitHub Check: Python 3.11.x on ubuntu-latest test
  • GitHub Check: Python 3.10.x on ubuntu-latest test
  • GitHub Check: Python 3.9.x on ubuntu-latest test
🔇 Additional comments (5)
tests/congruence_tests/test_delete_points.py (2)

23-54: API migration correctly applied.

The test successfully migrates from the search-based API to query_points with consistent usage of the query and using parameters. Results are correctly accessed via the .points attribute on the QueryResponse. Test logic remains functionally equivalent, and the added comment on line 44 improves clarity.


73-96: Sparse vector test correctly migrated.

The sparse vector test follows the same migration pattern as the dense vector test, correctly using query_points with query=vector and using="sparse-image". The multi-line formatting in the lambda (lines 73-77) maintains readability.

tests/congruence_tests/test_sparse_idf_search.py (1)

27-35: LGTM! Clean migration to the query_points API.

The method correctly migrates from the legacy search API to query_points, properly using the using parameter to specify the vector field and the query parameter for the sparse vector input. The result handling via .points aligns with the QueryResponse structure, and all original test functionality is preserved.

tests/congruence_tests/test_sparse_search.py (2)

71-139: LGTM! Migration to query_points is correct.

All search methods correctly use the query_points API with proper parameter mapping and response handling via .points extraction.


343-374: LGTM! NaN test correctly updated for query_points API.

The test correctly:

  • Mutates the sparse vector values directly (no wrapper needed)
  • Passes the sparse vector to query_points as the query parameter
  • Expects appropriate exceptions from both local and remote clients when NaN values are present

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
qdrant_client/local/async_qdrant_local.py (1)

762-786: Normalize UUID ids in _upload_points to match other paths

upload_points/_upload_points now accept update_filter and delegate to LocalCollection.upsert, which is good. However, _upload_points still forwards point.id directly:

rest_models.PointStruct(
    id=point.id,
    vector=point.vector or {},
    payload=point.payload or {},
)

Elsewhere in this file (upload_collection, _resolve_query_input, update_vectors) UUID ids are normalized to str to match how LocalCollection stores and looks up ids. For local uploads that use UUID-based ids, this inconsistency can lead to mismatched behavior (e.g., points inserted under UUID keys that aren’t found via string-based lookup).

Consider normalizing here as well:

-        collection.upsert(
-            [
-                rest_models.PointStruct(
-                    id=point.id, vector=point.vector or {}, payload=point.payload or {}
-                )
-                for point in points
-            ],
-            update_filter=update_filter,
-        )
+        collection.upsert(
+            [
+                rest_models.PointStruct(
+                    id=str(point.id) if isinstance(point.id, uuid.UUID) else point.id,
+                    vector=point.vector or {},
+                    payload=point.payload or {},
+                )
+                for point in points
+            ],
+            update_filter=update_filter,
+        )

This keeps UUID handling consistent across all local mutation paths.

♻️ Duplicate comments (1)
qdrant_client/local/qdrant_local.py (1)

815-815: Forward kwargs to create_collection.

The **kwargs parameter accepted by recreate_collection (line 811) is not forwarded to create_collection, which limits the ability to pass additional configuration options through the recreate flow.

Apply this diff to forward kwargs:

         return self.create_collection(
-            collection_name, vectors_config, sparse_vectors_config, metadata=metadata
+            collection_name, vectors_config, sparse_vectors_config, metadata=metadata, **kwargs
         )

Based on learnings

🧹 Nitpick comments (3)
qdrant_client/local/qdrant_local.py (1)

746-747: Consider conditional save for efficiency.

Currently self._save() is called unconditionally even when updated is False (i.e., when no changes were made). While not incorrect, this is inefficient.

Apply this diff to only save when needed:

+        if updated:
+            self._save()
-        self._save()
         return updated
qdrant_client/local/async_qdrant_local.py (2)

503-523: update_filter plumbing for upsert/update_vectors looks correct (kwargs unused)

Threading update_filter through async upsert and update_vectors into the underlying LocalCollection matches the sync local implementation and keeps the local API aligned with the remote client.

Ruff flags **kwargs as unused here (and in similar methods). If you want to quiet ARG002 while preserving the interface, consider explicitly discarding or marking them, e.g.:

-    async def upsert(
-        self,
-        collection_name: str,
-        points: types.Points,
-        update_filter: Optional[types.Filter] = None,
-        **kwargs: Any,
-    ) -> types.UpdateResult:
+    async def upsert(
+        self,
+        collection_name: str,
+        points: types.Points,
+        update_filter: Optional[types.Filter] = None,
+        **kwargs: Any,
+    ) -> types.UpdateResult:
+        _ = kwargs  # kept for API compatibility

(and similarly for update_vectors and other methods with unused **kwargs).


796-827: UUID id normalization in upload_collection is good; consider explicit zip(strict=False)

Positives:

  • Normalizing point_id via str(point_id) when it is a uuid.UUID aligns this path with the rest of the UUID handling logic.
  • Threading update_filter through to collection.upsert matches the rest of the local API.

Ruff’s B905 note about zip() is relevant here:

for (point_id, vector, payload) in zip(
    ids or uuid_generator(), iter(vectors), payload or itertools.cycle([{}])
)

Given that uuid_generator() and itertools.cycle([{}]) can be unbounded, you likely intend the current “truncate to the shortest iterable” semantics rather than strict length equality. To both make that intent explicit and silence B905, you can add strict=False:

-                for (point_id, vector, payload) in zip(
-                    ids or uuid_generator(), iter(vectors), payload or itertools.cycle([{}])
-                )
+                for (point_id, vector, payload) in zip(
+                    ids or uuid_generator(),
+                    iter(vectors),
+                    payload or itertools.cycle([{}]),
+                    strict=False,
+                )

Please just verify that your minimum supported Python version includes zip(..., strict=...); if not, you may prefer to keep the current form and suppress B905 instead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99228ee and cd7f811.

📒 Files selected for processing (2)
  • qdrant_client/local/async_qdrant_local.py (10 hunks)
  • qdrant_client/local/qdrant_local.py (11 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
qdrant_client/local/async_qdrant_local.py (3)
qdrant_client/local/qdrant_local.py (14)
  • search (174-202)
  • _get_collection (164-172)
  • upsert (543-552)
  • _default_update_result (588-592)
  • update_vectors (554-563)
  • _save (143-162)
  • closed (70-71)
  • _collection_path (749-753)
  • recreate_collection (805-816)
  • delete_collection (755-770)
  • create_collection (772-803)
  • upload_points (818-826)
  • _upload_points (828-845)
  • uuid_generator (859-861)
qdrant_client/local/local_collection.py (5)
  • search (535-703)
  • upsert (2481-2522)
  • update_vectors (2556-2578)
  • update_sparse_vectors_config (2775-2781)
  • LocalCollection (95-2820)
qdrant_client/http/models/models.py (9)
  • RrfQuery (2476-2477)
  • Filter (914-924)
  • UpdateResult (3345-3347)
  • PointVectors (1991-1993)
  • CreateCollection (503-551)
  • VectorParams (3449-3474)
  • SparseVectorParams (3051-3061)
  • PointStruct (1985-1988)
  • Record (2323-2332)
qdrant_client/local/qdrant_local.py (3)
qdrant_client/http/models/models.py (8)
  • RrfQuery (2476-2477)
  • Filter (914-924)
  • UpdateResult (3345-3347)
  • PointVectors (1991-1993)
  • VectorParams (3449-3474)
  • SparseVectorParams (3051-3061)
  • PointStruct (1985-1988)
  • Record (2323-2332)
qdrant_client/local/async_qdrant_local.py (10)
  • _get_collection (153-160)
  • upsert (503-512)
  • _default_update_result (548-551)
  • update_vectors (514-523)
  • _save (134-151)
  • recreate_collection (749-760)
  • delete_collection (703-717)
  • create_collection (719-747)
  • upload_points (762-769)
  • _upload_points (771-786)
qdrant_client/local/local_collection.py (3)
  • upsert (2481-2522)
  • update_vectors (2556-2578)
  • update_sparse_vectors_config (2775-2781)
🪛 Ruff (0.14.4)
qdrant_client/local/async_qdrant_local.py

508-508: Unused method argument: kwargs

(ARG002)


519-519: Unused method argument: kwargs

(ARG002)


680-680: Unused method argument: kwargs

(ARG002)


727-727: Unused method argument: kwargs

(ARG002)


730-730: Avoid specifying long messages outside the exception class

(TRY003)


732-732: Avoid specifying long messages outside the exception class

(TRY003)


755-755: Unused method argument: kwargs

(ARG002)


767-767: Unused method argument: kwargs

(ARG002)


822-824: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

qdrant_client/local/qdrant_local.py

548-548: Unused method argument: kwargs

(ARG002)


559-559: Unused method argument: kwargs

(ARG002)


730-730: Unused method argument: kwargs

(ARG002)


811-811: Unused method argument: kwargs

(ARG002)


823-823: Unused method argument: kwargs

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Redirect rules - poetic-froyo-8baba7
  • GitHub Check: Header rules - poetic-froyo-8baba7
  • GitHub Check: Python 3.11.x on ubuntu-latest test
  • GitHub Check: Python 3.13.x on ubuntu-latest test
  • GitHub Check: Python 3.9.x on ubuntu-latest test
  • GitHub Check: Python 3.10.x on ubuntu-latest test
  • GitHub Check: Python 3.12.x on ubuntu-latest test
  • GitHub Check: Pages changed - poetic-froyo-8baba7
🔇 Additional comments (13)
qdrant_client/local/qdrant_local.py (8)

6-6: LGTM: UUID import added for proper type handling.

The uuid module import is correctly added to support UUID-to-string normalization in _resolve_query_input and upload_collection.


270-271: LGTM: UUID normalization for consistent ID handling.

The UUID-to-string conversion ensures that point IDs are consistently stored and compared as strings, which aligns with the internal storage format.


330-332: LGTM: RrfQuery support added consistently.

The pass-through handling for RrfQuery is appropriate and consistent with how OrderByQuery and FusionQuery are handled, as these query types don't require vector input resolution.


543-563: LGTM: update_filter propagation correctly implemented.

Both upsert and update_vectors correctly accept and forward the update_filter parameter to the underlying LocalCollection methods, matching the async implementation and the updated API surface.

The unused **kwargs parameters flagged by static analysis are intentional for API compatibility with server-mode signatures.


739-744: LGTM: Metadata None handling properly implemented.

The metadata update logic now correctly checks if _collection.config.metadata is None before calling .update(), and initializes with deepcopy(metadata) when needed. This properly addresses the past review comment about the AttributeError risk.

Based on learnings


779-795: LGTM: Metadata properly propagated to LocalCollection.

The metadata parameter is correctly accepted and forwarded to LocalCollection initialization with deepcopy() to prevent mutation issues.


818-845: LGTM: update_filter correctly propagated through upload flow.

The update_filter parameter is properly threaded from upload_points_upload_pointscollection.upsert, enabling filtered updates in the upload path. The comment on line 825 helpfully clarifies the wait=True equivalence in local mode.


879-890: LGTM: UUID normalization and update_filter propagation.

Line 879 correctly normalizes UUID point IDs to strings before constructing PointStruct objects, ensuring consistency with the internal ID storage format. The update_filter is properly forwarded to the final collection.upsert call on line 889.

qdrant_client/local/async_qdrant_local.py (5)

17-17: uuid import correctly added for UUID handling

The added import uuid is required for the isinstance(..., uuid.UUID) checks later in this file; import usage is consistent and appropriate.


162-190: Async search mirrors sync local behavior

The search implementation delegates directly to LocalCollection.search with the expected arguments and mirrors the synchronous local client, so behavior stays consistent between sync and async local modes.


249-253: Good: normalize UUID PointId inputs before lookup

Normalizing vector_input when it is a uuid.UUID to str before checking collection.ids aligns with how IDs are stored internally and avoids subtle lookup failures for UUID-based ids.


308-309: Confirm whether RrfQuery needs vector/id resolution

RrfQuery is currently passed through without calling input_into_vector on any nested parts. If rest_models.Rrf / RrfQuery can contain PointId or other vector-like inputs (similar to NearestQuery, RecommendQuery, etc.), those won’t be resolved here.

If by model design RrfQuery never embeds such inputs, this is fine; otherwise, you may need to mirror the handling used for other query types.


675-760: Collection metadata updates and creation flow look safe

The new metadata plumbing for collections looks sound:

  • update_collection now handles metadata being None vs an existing dict, either updating in place or assigning a deepcopy of the incoming payload.
  • create_collection and recreate_collection accept metadata, pass a deepcopy(metadata) into rest_models.CreateCollection, and persist it via _save.

This addresses the earlier risk of calling .update() on None and ensures metadata is both optional and correctly persisted for new and existing collections.

@joein joein merged commit 27acfd0 into dev Nov 14, 2025
14 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Nov 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants