From 852f888219fd6cd40982d4f00842c0a43e178ef6 Mon Sep 17 00:00:00 2001 From: Andrei Bondarev Date: Wed, 21 Oct 2026 15:04:45 -0400 Subject: [PATCH] Fix endpoints and docs that drifted from the Qdrant API - restore_snapshot: use PUT (POST returns 404) - remove_peer: call DELETE /cluster/peer/{peer_id} instead of POST /cluster/recover - client.aliases: stop caching the first response - points.list: require ids:, as the API does - Deprecation warnings for search/recommend endpoints, the lock API and init_from: - e2e: use points.query instead of the deprecated search - README: fix inaccurate examples, document points.query, drop nonexistent backups section --- CHANGELOG.md | 6 +++ README.md | 95 ++++++++++++++++----------------- lib/qdrant/base.rb | 8 +++ lib/qdrant/client.rb | 2 +- lib/qdrant/clusters.rb | 2 +- lib/qdrant/collections.rb | 4 +- lib/qdrant/points.rb | 12 ++++- lib/qdrant/service.rb | 4 ++ script/e2e.rb | 14 ++--- spec/qdrant/aliases_spec.rb | 9 ++++ spec/qdrant/clusters_spec.rb | 16 +++++- spec/qdrant/collections_spec.rb | 8 ++- spec/qdrant/points_spec.rb | 33 ++++++++++++ spec/qdrant/service_spec.rb | 14 +++++ 14 files changed, 165 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b88748e..768bf91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ ## [Unreleased] +- Fix `collections.restore_snapshot` to use PUT (POST always returned 404) +- Fix `clusters.remove_peer` to call `DELETE /cluster/peer/{peer_id}` instead of `POST /cluster/recover` +- `client.aliases` no longer caches the first response +- `points.list` now requires `ids:` (the API always required it) +- Deprecation warnings for `points.search`, `batch_search`, `recommend`, `batch_recommend` (use `points.query`), the lock API and `init_from:` (both removed in Qdrant 1.16) +- README: fix inaccurate examples and remove the nonexistent `backups` section ## [0.10.0] - 2026-08-12 - Drop Faraday dependency diff --git a/README.md b/README.md index 96e761c..b5f97ac 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ client.collections.get(collection_name: "string") client.collections.create( collection_name: "string", # required vectors: {}, # required + sparse_vectors: nil, shard_number: nil, replication_factor: nil, write_consistency_factor: nil, @@ -59,7 +60,6 @@ client.collections.create( hnsw_config: nil, wal_config: nil, optimizers_config: nil, - init_from: nil, quantization_config: nil ) @@ -73,11 +73,14 @@ client.collections.update( # Drop collection and all associated data client.collections.delete(collection_name: "string") -# Get list of all aliases (for a collection) +# Get list of all aliases for a collection client.collections.aliases( - collection_name: "string" # optional + collection_name: "string" # required ) +# Get list of all aliases across all collections +client.aliases + # Update aliases of the collections client.collections.update_aliases( actions: [{ @@ -101,9 +104,7 @@ client.collections.create_index( # Delete field index for collection client.collections.delete_index( collection_name: "string", # required - field_name: "string", # required - wait: "boolean", - ordering: "ordering" + field_name: "string" # required ) # Get cluster information for a collection @@ -118,8 +119,7 @@ client.collections.update_cluster( shard_id: "int", to_peer_id: "int", from_peer_id: "int" - }, - timeout: "int" + } ) # Create new snapshot for a collection @@ -167,18 +167,18 @@ client.points.get( client.points.get_all( collection_name: "string", # required ids: "[int]", # required - with_payload: "boolean" - with_vector: "boolean" + with_payload: "boolean", + with_vector: "boolean", + consistency: "int/string" ) -# Lists all data objects in reverse order of creation. The data will be returned as an array of objects. +# Retrieve full information of points by ids. Same as `get_all`. client.points.list( collection_name: "string", # required ids: "[int/string]", # required with_payload: nil, with_vector: nil, consistency: nil - ) # Insert or update points. Either `points:` or `batch:` is required. @@ -278,6 +278,28 @@ client.points.scroll( consistency: "int/string" ) +# Universally query points: covers search, recommend and discover, plus hybrid and multi-stage queries +client.points.query( + collection_name: "string", # required + query: "[float]/{}", # a vector, a point ID, or a query object such as {recommend: {positive: [1]}} + using: "string", + prefetch: {}, + filter: {}, + params: {}, + score_threshold: "float", + limit: "int", + offset: "int", + with_payload: "boolean", + with_vector: "boolean", + lookup_from: {}, + shard_key: "string", + consistency: "int/string", + timeout: "int" +) + +# DEPRECATED (since Qdrant 1.13): `search`, `batch_search`, `recommend` and `batch_recommend` +# still work but are no longer documented by Qdrant and will be removed. Use `points.query` instead. + # Retrieve closest points based on vector similarity and given filtering conditions client.points.search( collection_name: "string", # required @@ -310,9 +332,9 @@ client.points.recommend( offset: "int", with_payload: "boolean", with_vector: "boolean", - score_threshold: "float" + score_threshold: "float", using: "string", - lookup_from: {}, + lookup_from: {} ) # Look for the points which are closer to stored positive examples and at the same time further to negative examples. @@ -333,58 +355,33 @@ client.points.count( ### Snapshots ```ruby # Get list of snapshots of the whole storage -client.snapshots.list( - collection_name: "string" # optional -) +client.snapshots.list # Create new snapshot of the whole storage -client.snapshots.create( - collection_name: "string" # required -) +client.snapshots.create # Delete snapshot of the whole storage client.snapshots.delete( - collection_name: "string", # required - snapshot_name: "string" # required + snapshot_name: "string" # required ) # Download specified snapshot of the whole storage as a file client.snapshots.download( - collection_name: "string", # required - snapshot_name: "string" # required - filepath: "~/Downloads/backup.txt" # required -) - - -# Get the backup -client.backups.get( - backend: "filesystem", - id: "my-first-backup" -) - -# Restore backup -client.backups.restore( - backend: "filesystem", - id: "my-first-backup" -) - -# Check the backup restore status -client.backups.restore_status( - backend: "filesystem", - id: "my-first-backup" + snapshot_name: "string", # required + filepath: "~/Downloads/backup.snapshot" # required ) ``` ### Cluster ```ruby # Get information about the current state and composition of the cluster -client.cluster.info +client.clusters.info # Tries to recover current peer Raft state. -client.cluster.recover +client.clusters.recover # Tries to remove peer from the cluster. Will return an error if peer has shards on it. -client.cluster.remove_peer( +client.clusters.remove_peer( peer_id: "int", # required force: "boolean" ) @@ -402,12 +399,14 @@ client.metrics( anonymize: "boolean" # optional ) +# DEPRECATED: the lock API was removed in Qdrant 1.16. These only work against older servers. + # Get lock options. If write is locked, all write operations and collection creation are forbidden client.locks # Set lock options. If write is locked, all write operations and collection creation are forbidden. Returns previous lock options client.set_lock( - write: "boolean" # required + write: "boolean", # required error_message: "string" ) ``` diff --git a/lib/qdrant/base.rb b/lib/qdrant/base.rb index 6dceae2..62c25ba 100644 --- a/lib/qdrant/base.rb +++ b/lib/qdrant/base.rb @@ -7,5 +7,13 @@ class Base def initialize(client:) @client = client end + + private + + # Point the warning at the caller's code, skipping frames inside the gem and Forwardable. + def deprecation_warning(message) + uplevel = caller_locations.index { |loc| !loc.path.start_with?(__dir__) && !loc.path.end_with?("/forwardable.rb") } + warn("[DEPRECATION] Qdrant: #{message}", uplevel: uplevel + 1) + end end end diff --git a/lib/qdrant/client.rb b/lib/qdrant/client.rb index ae11974..a9f5db6 100644 --- a/lib/qdrant/client.rb +++ b/lib/qdrant/client.rb @@ -37,7 +37,7 @@ def connection end def aliases - @aliases ||= Qdrant::Aliases.new(client: self).list + Qdrant::Aliases.new(client: self).list end def collections diff --git a/lib/qdrant/clusters.rb b/lib/qdrant/clusters.rb index a731eac..3e6935f 100644 --- a/lib/qdrant/clusters.rb +++ b/lib/qdrant/clusters.rb @@ -21,7 +21,7 @@ def remove_peer( peer_id:, force: nil ) - response = client.connection.post("#{PATH}/recover") do |req| + response = client.connection.delete("#{PATH}/peer/#{peer_id}") do |req| req.params["force"] = force if force end response.body diff --git a/lib/qdrant/collections.rb b/lib/qdrant/collections.rb index 1ac94c6..bf778c2 100644 --- a/lib/qdrant/collections.rb +++ b/lib/qdrant/collections.rb @@ -31,6 +31,8 @@ def create( init_from: nil, quantization_config: nil ) + deprecation_warning("`init_from:` was removed in Qdrant 1.16 and is ignored by the server.") unless init_from.nil? + response = client.connection.put("#{PATH}/#{collection_name}") do |req| req.body = {} req.body["vectors"] = vectors @@ -176,7 +178,7 @@ def restore_snapshot( priority: nil, wait: nil ) - response = client.connection.post("#{PATH}/#{collection_name}/snapshots/recover") do |req| + response = client.connection.put("#{PATH}/#{collection_name}/snapshots/recover") do |req| req.params["wait"] = wait unless wait.nil? req.body = { diff --git a/lib/qdrant/points.rb b/lib/qdrant/points.rb index 0152bb0..2d46342 100644 --- a/lib/qdrant/points.rb +++ b/lib/qdrant/points.rb @@ -4,10 +4,10 @@ module Qdrant class Points < Base PATH = "points" - # Lists all data objects in reverse order of creation. The data will be returned as an array of objects. + # Retrieve full information of points by ids. Same as `get_all`. def list( collection_name:, - ids: nil, + ids:, with_payload: nil, with_vector: nil, consistency: nil @@ -212,6 +212,8 @@ def search( score_threshold: nil, consistency: nil ) + deprecation_warning("`points.search` is deprecated since Qdrant 1.13 and will be removed from the server; use `points.query` instead.") + response = client.connection.post("collections/#{collection_name}/#{PATH}/search") do |req| req.params["consistency"] = consistency unless consistency.nil? @@ -234,6 +236,8 @@ def batch_search( searches:, consistency: nil ) + deprecation_warning("`points.batch_search` is deprecated since Qdrant 1.13 and will be removed from the server; use `points.query` instead.") + response = client.connection.post("collections/#{collection_name}/#{PATH}/search/batch") do |req| req.params["consistency"] = consistency unless consistency.nil? @@ -259,6 +263,8 @@ def recommend( lookup_from: nil, consistency: nil ) + deprecation_warning("`points.recommend` is deprecated since Qdrant 1.13 and will be removed from the server; use `points.query` instead.") + response = client.connection.post("collections/#{collection_name}/#{PATH}/recommend") do |req| req.params["consistency"] = consistency unless consistency.nil? @@ -284,6 +290,8 @@ def batch_recommend( searches:, consistency: nil ) + deprecation_warning("`points.batch_recommend` is deprecated since Qdrant 1.13 and will be removed from the server; use `points.query` instead.") + response = client.connection.post("collections/#{collection_name}/#{PATH}/recommend/batch") do |req| req.params["consistency"] = consistency unless consistency.nil? diff --git a/lib/qdrant/service.rb b/lib/qdrant/service.rb index 495c8be..244272d 100644 --- a/lib/qdrant/service.rb +++ b/lib/qdrant/service.rb @@ -27,6 +27,8 @@ def set_lock( write:, error_message: nil ) + deprecation_warning("the lock API was removed in Qdrant 1.16; `set_lock` only works against older servers.") + response = client.connection.post("locks") do |req| req.body = { write: write @@ -38,6 +40,8 @@ def set_lock( # Get lock options. If write is locked, all write operations and collection creation are forbidden def locks + deprecation_warning("the lock API was removed in Qdrant 1.16; `locks` only works against older servers.") + response = client.connection.get("locks") response.body end diff --git a/script/e2e.rb b/script/e2e.rb index d0cb248..ebf47d5 100644 --- a/script/e2e.rb +++ b/script/e2e.rb @@ -85,22 +85,22 @@ def ok_body!(body, operation) puts "e2e: point write and read round trip passed" body = ok_body!( - client.points.search( + client.points.query( collection_name: collection_name, - vector: [1.0, 0.0, 0.0], + query: [1.0, 0.0, 0.0], limit: 1, with_payload: true, with_vector: false ), - "points.search" + "points.query" ) - hit = body.fetch("result").first - assert!(hit && hit["id"] == 1, "points.search did not return point 1 first") - assert!(hit["score"].to_f > 0.99, "points.search score was #{hit["score"].inspect}") + hit = body.dig("result", "points").first + assert!(hit && hit["id"] == 1, "points.query did not return point 1 first") + assert!(hit["score"].to_f > 0.99, "points.query score was #{hit["score"].inspect}") body = ok_body!(client.points.count(collection_name: collection_name, exact: true), "points.count") assert!(body.dig("result", "count") == 2, "points.count did not return 2") - puts "e2e: search and count passed" + puts "e2e: query and count passed" body = ok_body!(client.points.delete(collection_name: collection_name, points: [1, 2], wait: true), "points.delete") assert!(body.dig("result", "status") == "completed", "points.delete did not return a completed status") diff --git a/spec/qdrant/aliases_spec.rb b/spec/qdrant/aliases_spec.rb index 20a7b9e..402818d 100644 --- a/spec/qdrant/aliases_spec.rb +++ b/spec/qdrant/aliases_spec.rb @@ -26,5 +26,14 @@ it "return the nodes info" do expect(client.aliases).to be_a(Hash) end + + it "fetches fresh data on every call" do + expect_any_instance_of(Qdrant::Client::Connection).to receive(:get) + .with(Qdrant::Aliases::PATH) + .twice + .and_return(response) + + 2.times { client.aliases } + end end end diff --git a/spec/qdrant/clusters_spec.rb b/spec/qdrant/clusters_spec.rb index 5fbd86f..f1c8754 100644 --- a/spec/qdrant/clusters_spec.rb +++ b/spec/qdrant/clusters_spec.rb @@ -40,6 +40,20 @@ end end - xdescribe "#remove_peer" do + describe "#remove_peer" do + let(:request) { Qdrant::Client::RequestData.new({}, nil) } + + before do + allow_any_instance_of(Qdrant::Client::Connection).to receive(:delete) + .with("cluster/peer/42") do |_connection, _path, &block| + block.call(request) + response + end + end + + it "deletes the peer" do + expect(client.clusters.remove_peer(peer_id: 42, force: true).dig("status")).to eq("ok") + expect(request.params).to eq("force" => true) + end end end diff --git a/spec/qdrant/collections_spec.rb b/spec/qdrant/collections_spec.rb index b228a82..727ea18 100644 --- a/spec/qdrant/collections_spec.rb +++ b/spec/qdrant/collections_spec.rb @@ -65,6 +65,12 @@ expect(response.dig("status")).to eq("ok") expect(response.dig("result")).to eq(true) end + + it "warns that init_from is no longer supported" do + allow(Warning).to receive(:warn) + collections.create(collection_name: "test_collection", vectors: {size: 4, distance: "Dot"}, init_from: {collection: "other"}) + expect(Warning).to have_received(:warn).with(/\[DEPRECATION\] Qdrant: `init_from:`/, category: nil) + end end describe "#delete" do @@ -357,7 +363,7 @@ let(:response) { Qdrant::Client::Response.new(nil, nil, status_response_fixture) } before do - allow_any_instance_of(Qdrant::Client::Connection).to receive(:post) + allow_any_instance_of(Qdrant::Client::Connection).to receive(:put) .with("collections/test_collection/snapshots/recover") .and_return(response) end diff --git a/spec/qdrant/points_spec.rb b/spec/qdrant/points_spec.rb index 1f77221..6fbf635 100644 --- a/spec/qdrant/points_spec.rb +++ b/spec/qdrant/points_spec.rb @@ -89,6 +89,13 @@ .and_return(response) end + before { allow(Warning).to receive(:warn) } + + it "warns that it is deprecated" do + client.points.search(collection_name: "test_collection", vector: [0.1], limit: 1) + expect(Warning).to have_received(:warn).with(/\[DEPRECATION\] Qdrant: `points.search`/, category: nil) + end + it "return the data" do response = client.points.search( collection_name: "test_collection", @@ -131,6 +138,13 @@ .and_return(response) end + before { allow(Warning).to receive(:warn) } + + it "warns that it is deprecated" do + client.points.batch_search(collection_name: "test_collection", searches: []) + expect(Warning).to have_received(:warn).with(/\[DEPRECATION\] Qdrant: `points.batch_search`/, category: nil) + end + it "return the data" do response = client.points.batch_search( collection_name: "test_collection", @@ -154,6 +168,13 @@ .and_return(response) end + before { allow(Warning).to receive(:warn) } + + it "warns that it is deprecated" do + client.points.recommend(collection_name: "test_collection", positive: [1], limit: 1) + expect(Warning).to have_received(:warn).with(/\[DEPRECATION\] Qdrant: `points.recommend`/, category: nil) + end + it "return the data" do response = client.points.recommend( collection_name: "test_collection", @@ -175,6 +196,13 @@ .and_return(response) end + before { allow(Warning).to receive(:warn) } + + it "warns that it is deprecated" do + client.points.batch_recommend(collection_name: "test_collection", searches: []) + expect(Warning).to have_received(:warn).with(/\[DEPRECATION\] Qdrant: `points.batch_recommend`/, category: nil) + end + it "return the data" do response = client.points.batch_recommend( collection_name: "test_collection", @@ -225,6 +253,11 @@ ) expect(response.dig("result").count).to eq(5) end + + it "requires ids" do + expect { client.points.list(collection_name: "test_collection") } + .to raise_error(ArgumentError, /ids/) + end end describe "#set_payload" do diff --git a/spec/qdrant/service_spec.rb b/spec/qdrant/service_spec.rb index d47c386..7272d47 100644 --- a/spec/qdrant/service_spec.rb +++ b/spec/qdrant/service_spec.rb @@ -59,6 +59,13 @@ .and_return(response) end + before { allow(Warning).to receive(:warn) } + + it "warns that it is deprecated" do + client.set_lock(write: true) + expect(Warning).to have_received(:warn).with(/\[DEPRECATION\] Qdrant: the lock API/, category: nil) + end + it "returns the data" do response = client.set_lock( write: true, @@ -78,6 +85,13 @@ .and_return(response) end + before { allow(Warning).to receive(:warn) } + + it "warns that it is deprecated" do + client.locks + expect(Warning).to have_received(:warn).with(/\[DEPRECATION\] Qdrant: the lock API/, category: nil) + end + it "returns the data" do response = client.locks expect(response.dig("result", "error_message")).to eq("my error msg")