qdrant-ruby 0.9.10 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6269ed0349b2612a9221b36a181df8c4126e4542685b313dcf839cfc5a91314a
4
- data.tar.gz: 660452185073d98c71d11ebe68b18dd5c0b16ee96085d830896529a618f473e9
3
+ metadata.gz: 6899d046fe093cba0a8f390313bba21f2febb5319984fa516b99d2d3a825af17
4
+ data.tar.gz: e99fb180f6d3adca5f91133d5b88869a11fb5b004147c3ddb0987428b479713d
5
5
  SHA512:
6
- metadata.gz: 7b4b80c0d1867ca65b7710974f6c13dd5e885b84b8ded38a2c34499ecff63ed3fdf782fa35a892ea32a110b3448a3b49b61e8f5e1dd7cd2f3ff946065c7ea80a
7
- data.tar.gz: 8c4957ff10a42198a4f0a133323c99d197eec8177e5c3daec7b1d708255ce101c1b5e4b9e3a70b0fc7e11ac7bda2da15b15a312932875ba36d556468e08527ad
6
+ metadata.gz: 619eb3d6fa73efb8bc57351418c6db3824f9b2fa416911be4555278944489d29cebf9892f26c23ff7406b631450a992059e8c84f05f64845a3d45799f9844fe5
7
+ data.tar.gz: c336aa478ac184ce7c70543bd5faaa15ce3d3340a77862ef3d0ccbbcfbe80430348ca0977f83591ace3309d18d108140e3e7e304e3d037c81711fec1939a35f9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.11.0] - 2026-10-21
4
+ - Fix `collections.restore_snapshot` to use PUT (POST always returned 404)
5
+ - Fix `clusters.remove_peer` to call `DELETE /cluster/peer/{peer_id}` instead of `POST /cluster/recover`
6
+ - `client.aliases` no longer caches the first response
7
+ - `points.list` now requires `ids:` (the API always required it)
8
+ - 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)
9
+ - README: fix inaccurate examples and remove the nonexistent `backups` section
10
+
11
+ ## [0.10.0] - 2026-08-12
12
+ - Drop Faraday dependency
13
+
14
+ ## [0.9.10] - 2025-11-23
15
+
3
16
  ## [0.9.9] - 2024-04-11
4
17
  - Allow sparse_vectors to be passed to collections.create
5
18
  - Support query endpoint
@@ -14,7 +27,8 @@
14
27
  - Updated Points#delete() method: Removed the requirement to specify points: in parameters. Now generates an error if neither points: nor filters: are provided, aligning with delete_points documentation standards.
15
28
 
16
29
  ## [0.9.5] - 2024-01-12
17
- - Bugfix: ArgumentError for filter in points delete
30
+ - Bugfix: ArgumentError for filter in points delete
31
+
18
32
  ## [0.9.4] - 2023-08-31
19
33
  - Introduce `Points#get_all()` method
20
34
 
data/Gemfile CHANGED
@@ -5,7 +5,7 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in qdrant.gemspec
6
6
  gemspec
7
7
 
8
- gem "rake", "~> 13.3"
8
+ gem "rake", "~> 13.4"
9
9
 
10
10
  gem "rspec", "~> 3.13"
11
- gem "standard", "~> 1.28.5"
11
+ gem "standard", "~> 1.57.0"
data/Gemfile.lock CHANGED
@@ -1,41 +1,41 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- qdrant-ruby (0.9.10)
5
- faraday (>= 2.0.1, < 3)
4
+ qdrant-ruby (0.11.0)
5
+ logger
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- ast (2.4.2)
11
- base64 (0.2.0)
12
- byebug (11.1.3)
10
+ ast (2.4.3)
11
+ byebug (13.0.0)
12
+ reline (>= 0.6.0)
13
13
  coderay (1.1.3)
14
14
  diff-lcs (1.6.2)
15
- faraday (2.8.1)
16
- base64
17
- faraday-net_http (>= 2.0, < 3.1)
18
- ruby2_keywords (>= 0.0.4)
19
- faraday-net_http (3.0.2)
20
- json (2.7.4)
21
- language_server-protocol (3.17.0.3)
15
+ io-console (0.9.2)
16
+ json (3.1.0)
17
+ language_server-protocol (3.17.0.6)
22
18
  lint_roller (1.1.0)
23
- method_source (1.0.0)
24
- parallel (1.24.0)
25
- parser (3.3.5.0)
19
+ logger (1.7.0)
20
+ method_source (1.1.0)
21
+ parallel (2.3.0)
22
+ parser (3.3.12.0)
26
23
  ast (~> 2.4.1)
27
24
  racc
28
- pry (0.14.2)
25
+ prism (1.9.0)
26
+ pry (0.16.0)
29
27
  coderay (~> 1.1)
30
28
  method_source (~> 1.0)
31
- pry-byebug (3.10.1)
32
- byebug (~> 11.0)
33
- pry (>= 0.13, < 0.15)
29
+ reline (>= 0.6.0)
30
+ pry-byebug (3.12.0)
31
+ byebug (~> 13.0)
32
+ pry (>= 0.13, < 0.17)
34
33
  racc (1.8.1)
35
34
  rainbow (3.1.1)
36
- rake (13.3.1)
37
- regexp_parser (2.9.2)
38
- rexml (3.4.2)
35
+ rake (13.4.2)
36
+ regexp_parser (2.13.1)
37
+ reline (0.7.0)
38
+ io-console (~> 0.5)
39
39
  rspec (3.13.2)
40
40
  rspec-core (~> 3.13.0)
41
41
  rspec-expectations (~> 3.13.0)
@@ -45,40 +45,44 @@ GEM
45
45
  rspec-expectations (3.13.5)
46
46
  diff-lcs (>= 1.2.0, < 2.0)
47
47
  rspec-support (~> 3.13.0)
48
- rspec-mocks (3.13.6)
48
+ rspec-mocks (3.13.8)
49
49
  diff-lcs (>= 1.2.0, < 2.0)
50
50
  rspec-support (~> 3.13.0)
51
- rspec-support (3.13.6)
52
- rubocop (1.50.2)
53
- json (~> 2.3)
54
- parallel (~> 1.10)
55
- parser (>= 3.2.0.0)
51
+ rspec-support (3.13.7)
52
+ rubocop (1.91.0)
53
+ json (>= 2.3)
54
+ language_server-protocol (~> 3.17.0.2)
55
+ lint_roller (~> 1.1.0)
56
+ parallel (>= 1.10)
57
+ parser (>= 3.3.0.2)
56
58
  rainbow (>= 2.2.2, < 4.0)
57
- regexp_parser (>= 1.8, < 3.0)
58
- rexml (>= 3.2.5, < 4.0)
59
- rubocop-ast (>= 1.28.0, < 2.0)
59
+ regexp_parser (>= 2.9.3, < 3.0)
60
+ rubocop-ast (>= 1.49.0, < 2.0)
60
61
  ruby-progressbar (~> 1.7)
61
- unicode-display_width (>= 2.4.0, < 3.0)
62
- rubocop-ast (1.30.0)
63
- parser (>= 3.2.1.0)
64
- rubocop-performance (1.16.0)
65
- rubocop (>= 1.7.0, < 2.0)
66
- rubocop-ast (>= 0.4.0)
62
+ unicode-display_width (>= 2.4.0, < 4.0)
63
+ rubocop-ast (1.50.0)
64
+ parser (>= 3.3.7.2)
65
+ prism (~> 1.7)
66
+ rubocop-performance (1.26.1)
67
+ lint_roller (~> 1.1)
68
+ rubocop (>= 1.75.0, < 2.0)
69
+ rubocop-ast (>= 1.47.1, < 2.0)
67
70
  ruby-progressbar (1.13.0)
68
- ruby2_keywords (0.0.5)
69
- standard (1.28.5)
71
+ standard (1.57.0)
70
72
  language_server-protocol (~> 3.17.0.2)
71
73
  lint_roller (~> 1.0)
72
- rubocop (~> 1.50.2)
74
+ rubocop (~> 1.91.0)
73
75
  standard-custom (~> 1.0.0)
74
- standard-performance (~> 1.0.1)
76
+ standard-performance (~> 1.8)
75
77
  standard-custom (1.0.2)
76
78
  lint_roller (~> 1.0)
77
79
  rubocop (~> 1.50)
78
- standard-performance (1.0.1)
79
- lint_roller (~> 1.0)
80
- rubocop-performance (~> 1.16.0)
81
- unicode-display_width (2.6.0)
80
+ standard-performance (1.9.0)
81
+ lint_roller (~> 1.1)
82
+ rubocop-performance (~> 1.26.0)
83
+ unicode-display_width (3.3.0)
84
+ unicode-emoji (~> 4.3)
85
+ unicode-emoji (4.3.0)
82
86
 
83
87
  PLATFORMS
84
88
  arm64-darwin-23
@@ -90,9 +94,9 @@ PLATFORMS
90
94
  DEPENDENCIES
91
95
  pry-byebug (~> 3.9)
92
96
  qdrant-ruby!
93
- rake (~> 13.3)
97
+ rake (~> 13.4)
94
98
  rspec (~> 3.13)
95
- standard (~> 1.28.5)
99
+ standard (~> 1.57.0)
96
100
 
97
101
  BUNDLED WITH
98
- 2.4.0
102
+ 2.5.3
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Qdrant
2
2
 
3
3
  <p>
4
- <img alt='Qdrant logo' src='https://qdrant.tech/images/logo_with_text.svg' height='50' />
4
+ <img alt='Qdrant logo' src='https://qdrant.tech/img/brand-resources-logos/qdrant-brandmark-red.svg' height='50' />
5
5
  &nbsp;&nbsp;+&nbsp;&nbsp;
6
6
  <img alt='Ruby logo' src='https://user-images.githubusercontent.com/541665/230231593-43861278-4550-421d-a543-fd3553aac4f6.png' height='40' />
7
7
  </p>
@@ -13,7 +13,7 @@ Part of the [Langchain.rb](https://github.com/andreibondarev/langchainrb) stack.
13
13
  ![Tests status](https://github.com/andreibondarev/qdrant-ruby/actions/workflows/ci.yml/badge.svg)
14
14
  [![Gem Version](https://badge.fury.io/rb/qdrant-ruby.svg)](https://badge.fury.io/rb/qdrant-ruby)
15
15
  [![Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/gems/qdrant-ruby)
16
- [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/andreibondarev/qdrant-ruby/blob/main/LICENSE.txt)
16
+ [![License](https://img.shields.io/badge/license-Apache2.0-green.svg)](https://github.com/andreibondarev/qdrant-ruby/blob/main/LICENSE.txt)
17
17
  [![](https://dcbadge.vercel.app/api/server/WDARp7J2n8?compact=true&style=flat)](https://discord.gg/WDARp7J2n8)
18
18
 
19
19
  ## Installation
@@ -52,6 +52,7 @@ client.collections.get(collection_name: "string")
52
52
  client.collections.create(
53
53
  collection_name: "string", # required
54
54
  vectors: {}, # required
55
+ sparse_vectors: nil,
55
56
  shard_number: nil,
56
57
  replication_factor: nil,
57
58
  write_consistency_factor: nil,
@@ -59,7 +60,6 @@ client.collections.create(
59
60
  hnsw_config: nil,
60
61
  wal_config: nil,
61
62
  optimizers_config: nil,
62
- init_from: nil,
63
63
  quantization_config: nil
64
64
  )
65
65
 
@@ -73,11 +73,14 @@ client.collections.update(
73
73
  # Drop collection and all associated data
74
74
  client.collections.delete(collection_name: "string")
75
75
 
76
- # Get list of all aliases (for a collection)
76
+ # Get list of all aliases for a collection
77
77
  client.collections.aliases(
78
- collection_name: "string" # optional
78
+ collection_name: "string" # required
79
79
  )
80
80
 
81
+ # Get list of all aliases across all collections
82
+ client.aliases
83
+
81
84
  # Update aliases of the collections
82
85
  client.collections.update_aliases(
83
86
  actions: [{
@@ -101,9 +104,7 @@ client.collections.create_index(
101
104
  # Delete field index for collection
102
105
  client.collections.delete_index(
103
106
  collection_name: "string", # required
104
- field_name: "string", # required
105
- wait: "boolean",
106
- ordering: "ordering"
107
+ field_name: "string" # required
107
108
  )
108
109
 
109
110
  # Get cluster information for a collection
@@ -118,8 +119,7 @@ client.collections.update_cluster(
118
119
  shard_id: "int",
119
120
  to_peer_id: "int",
120
121
  from_peer_id: "int"
121
- },
122
- timeout: "int"
122
+ }
123
123
  )
124
124
 
125
125
  # Create new snapshot for a collection
@@ -167,28 +167,57 @@ client.points.get(
167
167
  client.points.get_all(
168
168
  collection_name: "string", # required
169
169
  ids: "[int]", # required
170
- with_payload: "boolean"
171
- with_vector: "boolean"
170
+ with_payload: "boolean",
171
+ with_vector: "boolean",
172
+ consistency: "int/string"
172
173
  )
173
174
 
174
- # Lists all data objects in reverse order of creation. The data will be returned as an array of objects.
175
+ # Retrieve full information of points by ids. Same as `get_all`.
175
176
  client.points.list(
176
177
  collection_name: "string", # required
177
178
  ids: "[int/string]", # required
178
179
  with_payload: nil,
179
180
  with_vector: nil,
180
181
  consistency: nil
181
-
182
182
  )
183
183
 
184
- # Get a single data object.
184
+ # Insert or update points. Either `points:` or `batch:` is required.
185
185
  client.points.upsert(
186
186
  collection_name: "string", # required
187
- batch: {}, # required
187
+ points: [], # record-oriented: array of point hashes
188
+ batch: {}, # column-oriented: hash of parallel arrays
188
189
  wait: "boolean",
189
190
  ordering: "string"
190
191
  )
191
192
 
193
+ # Record-oriented upsert: each point is a hash with its own id, vector and payload
194
+ client.points.upsert(
195
+ collection_name: "my_collection",
196
+ points: [
197
+ {id: 1, vector: [0.9, 0.1, 0.1], payload: {color: "red"}},
198
+ {id: 2, vector: [0.1, 0.9, 0.1], payload: {color: "green"}},
199
+ {id: 3, vector: [0.1, 0.1, 0.9], payload: {color: "blue"}}
200
+ ]
201
+ )
202
+
203
+ # Column-oriented (batch) upsert: ids, vectors and payloads are passed as parallel arrays
204
+ client.points.upsert(
205
+ collection_name: "my_collection",
206
+ batch: {
207
+ ids: [1, 2, 3],
208
+ vectors: [
209
+ [0.9, 0.1, 0.1],
210
+ [0.1, 0.9, 0.1],
211
+ [0.1, 0.1, 0.9]
212
+ ],
213
+ payloads: [
214
+ {color: "red"},
215
+ {color: "green"},
216
+ {color: "blue"}
217
+ ]
218
+ }
219
+ )
220
+
192
221
  # Delete points
193
222
  client.points.delete(
194
223
  collection_name: "string", # required
@@ -249,6 +278,28 @@ client.points.scroll(
249
278
  consistency: "int/string"
250
279
  )
251
280
 
281
+ # Universally query points: covers search, recommend and discover, plus hybrid and multi-stage queries
282
+ client.points.query(
283
+ collection_name: "string", # required
284
+ query: "[float]/{}", # a vector, a point ID, or a query object such as {recommend: {positive: [1]}}
285
+ using: "string",
286
+ prefetch: {},
287
+ filter: {},
288
+ params: {},
289
+ score_threshold: "float",
290
+ limit: "int",
291
+ offset: "int",
292
+ with_payload: "boolean",
293
+ with_vector: "boolean",
294
+ lookup_from: {},
295
+ shard_key: "string",
296
+ consistency: "int/string",
297
+ timeout: "int"
298
+ )
299
+
300
+ # DEPRECATED (since Qdrant 1.13): `search`, `batch_search`, `recommend` and `batch_recommend`
301
+ # still work but are no longer documented by Qdrant and will be removed. Use `points.query` instead.
302
+
252
303
  # Retrieve closest points based on vector similarity and given filtering conditions
253
304
  client.points.search(
254
305
  collection_name: "string", # required
@@ -281,9 +332,9 @@ client.points.recommend(
281
332
  offset: "int",
282
333
  with_payload: "boolean",
283
334
  with_vector: "boolean",
284
- score_threshold: "float"
335
+ score_threshold: "float",
285
336
  using: "string",
286
- lookup_from: {},
337
+ lookup_from: {}
287
338
  )
288
339
 
289
340
  # Look for the points which are closer to stored positive examples and at the same time further to negative examples.
@@ -304,58 +355,33 @@ client.points.count(
304
355
  ### Snapshots
305
356
  ```ruby
306
357
  # Get list of snapshots of the whole storage
307
- client.snapshots.list(
308
- collection_name: "string" # optional
309
- )
358
+ client.snapshots.list
310
359
 
311
360
  # Create new snapshot of the whole storage
312
- client.snapshots.create(
313
- collection_name: "string" # required
314
- )
361
+ client.snapshots.create
315
362
 
316
363
  # Delete snapshot of the whole storage
317
364
  client.snapshots.delete(
318
- collection_name: "string", # required
319
- snapshot_name: "string" # required
365
+ snapshot_name: "string" # required
320
366
  )
321
367
 
322
368
  # Download specified snapshot of the whole storage as a file
323
369
  client.snapshots.download(
324
- collection_name: "string", # required
325
- snapshot_name: "string" # required
326
- filepath: "~/Downloads/backup.txt" # required
327
- )
328
-
329
-
330
- # Get the backup
331
- client.backups.get(
332
- backend: "filesystem",
333
- id: "my-first-backup"
334
- )
335
-
336
- # Restore backup
337
- client.backups.restore(
338
- backend: "filesystem",
339
- id: "my-first-backup"
340
- )
341
-
342
- # Check the backup restore status
343
- client.backups.restore_status(
344
- backend: "filesystem",
345
- id: "my-first-backup"
370
+ snapshot_name: "string", # required
371
+ filepath: "~/Downloads/backup.snapshot" # required
346
372
  )
347
373
  ```
348
374
 
349
375
  ### Cluster
350
376
  ```ruby
351
377
  # Get information about the current state and composition of the cluster
352
- client.cluster.info
378
+ client.clusters.info
353
379
 
354
380
  # Tries to recover current peer Raft state.
355
- client.cluster.recover
381
+ client.clusters.recover
356
382
 
357
383
  # Tries to remove peer from the cluster. Will return an error if peer has shards on it.
358
- client.cluster.remove_peer(
384
+ client.clusters.remove_peer(
359
385
  peer_id: "int", # required
360
386
  force: "boolean"
361
387
  )
@@ -373,12 +399,14 @@ client.metrics(
373
399
  anonymize: "boolean" # optional
374
400
  )
375
401
 
402
+ # DEPRECATED: the lock API was removed in Qdrant 1.16. These only work against older servers.
403
+
376
404
  # Get lock options. If write is locked, all write operations and collection creation are forbidden
377
405
  client.locks
378
406
 
379
407
  # Set lock options. If write is locked, all write operations and collection creation are forbidden. Returns previous lock options
380
408
  client.set_lock(
381
- write: "boolean" # required
409
+ write: "boolean", # required
382
410
  error_message: "string"
383
411
  )
384
412
  ```
@@ -387,6 +415,16 @@ client.set_lock(
387
415
 
388
416
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
389
417
 
418
+ The default `bundle exec rake` task runs `lint` (standardrb auto-format), `spec`, and `e2e` (a hosted end-to-end smoke test against a real Qdrant deployment) in sequence. Because the E2E step needs real credentials, run it with your hosted endpoint and API key exported:
419
+
420
+ ```sh
421
+ export QDRANT_URL="https://<hosted-qdrant-endpoint>"
422
+ export QDRANT_API_KEY="<project-api-key>"
423
+ bundle exec rake
424
+ ```
425
+
426
+ The individual steps are also available as `bundle exec rake lint`, `bundle exec rake spec`, and `bundle exec rake e2e`. Run the hosted check against an isolated or disposable project, since it creates and deletes a temporary collection.
427
+
390
428
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
391
429
 
392
430
  ## Contributing
data/Rakefile CHANGED
@@ -1,8 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "rbconfig"
4
5
  require "rspec/core/rake_task"
5
6
 
6
7
  RSpec::Core::RakeTask.new(:spec)
7
8
 
8
- task default: :spec
9
+ desc "Run the standardrb linter (auto-fix)"
10
+ task :lint do
11
+ sh "bundle exec standardrb --fix"
12
+ end
13
+
14
+ desc "Run the hosted Qdrant API end-to-end smoke test"
15
+ task :e2e do
16
+ exec RbConfig.ruby, File.expand_path("script/e2e.rb", __dir__)
17
+ end
18
+
19
+ task default: %i[lint spec e2e]
data/lib/qdrant/base.rb CHANGED
@@ -7,5 +7,13 @@ module Qdrant
7
7
  def initialize(client:)
8
8
  @client = client
9
9
  end
10
+
11
+ private
12
+
13
+ # Point the warning at the caller's code, skipping frames inside the gem and Forwardable.
14
+ def deprecation_warning(message)
15
+ uplevel = caller_locations.index { |loc| !loc.path.start_with?(__dir__) && !loc.path.end_with?("/forwardable.rb") }
16
+ warn("[DEPRECATION] Qdrant: #{message}", uplevel: uplevel + 1)
17
+ end
10
18
  end
11
19
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ require_relative "request"
6
+ require_relative "response"
7
+
8
+ module Qdrant
9
+ class Client
10
+ class Connection
11
+ def initialize(url:, api_key:, raise_error:, logger:)
12
+ @uri = url
13
+ @api_key = api_key
14
+ @raise_error = raise_error
15
+ @logger = logger
16
+ end
17
+
18
+ def get(path, &block)
19
+ execute(Net::HTTP::Get, path, &block)
20
+ end
21
+
22
+ def post(path, &block)
23
+ execute(Net::HTTP::Post, path, &block)
24
+ end
25
+
26
+ def put(path, &block)
27
+ execute(Net::HTTP::Put, path, &block)
28
+ end
29
+
30
+ def patch(path, &block)
31
+ execute(Net::HTTP::Patch, path, &block)
32
+ end
33
+
34
+ def delete(path, &block)
35
+ execute(Net::HTTP::Delete, path, &block)
36
+ end
37
+
38
+ private
39
+
40
+ def execute(verb, path, &block)
41
+ response = RequestBuilder
42
+ .new(verb, @uri, path, @api_key, @logger)
43
+ .tap(&block)
44
+ .build
45
+ .perform(@raise_error)
46
+
47
+ ResponseBuilder.new(response).build
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Qdrant
8
+ class Client
9
+ RequestData = Struct.new(:params, :body)
10
+
11
+ class RequestBuilder
12
+ def initialize(verb, base_url, path, api_key, logger)
13
+ raise ArgumentError, "unsupported HTTP verb" unless verb < Net::HTTPRequest
14
+
15
+ @verb = verb
16
+ @base_url = base_url
17
+ @path = path
18
+ @api_key = api_key
19
+ @logger = logger
20
+
21
+ @request = RequestData.new({}, nil)
22
+ end
23
+
24
+ def tap
25
+ yield @request if block_given?
26
+ self
27
+ end
28
+
29
+ def build
30
+ Request.new build_uri, @verb, @request.body, @api_key, @logger
31
+ end
32
+
33
+ private
34
+
35
+ def build_uri
36
+ path, query = @path.to_s.split("?", 2)
37
+
38
+ URI.join(@base_url, path).tap do |uri|
39
+ if (query_pairs = URI.decode_www_form(query.to_s) + @request.params.to_a).any?
40
+ uri.query = URI.encode_www_form(query_pairs)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ class Request
47
+ def initialize(uri, verb, body, api_key, logger)
48
+ @logger = logger
49
+ @uri = uri
50
+ @verb = verb
51
+
52
+ @data = verb.new(uri.request_uri).tap do |request|
53
+ request["api-key"] = api_key if api_key
54
+
55
+ if body
56
+ request.body = JSON.generate(body)
57
+ request["Content-Type"] = request["Accept"] = "application/json"
58
+ end
59
+ end
60
+
61
+ logger.info("#{verb_name} #{uri}")
62
+ logger.info("Request headers: #{redacted_headers.inspect}")
63
+ logger.info("Request body: #{@data.body}") if @data.body
64
+ end
65
+
66
+ def perform(raise_error)
67
+ @logger.info("Performing Request: #{verb_name} #{@uri}")
68
+
69
+ response = Net::HTTP.new(@uri.host, @uri.port).tap do |h|
70
+ h.use_ssl = true if @uri.scheme == "https"
71
+ end.request(@data)
72
+
73
+ response.value if raise_error
74
+
75
+ @logger.info("Response status: #{response.code}")
76
+ response
77
+ rescue => e
78
+ @logger.error("#{verb_name} #{@uri} failed: #{e.class}: #{e.message}")
79
+ raise
80
+ end
81
+
82
+ private
83
+
84
+ def verb_name
85
+ @verb.name.split("::").last.upcase
86
+ end
87
+
88
+ def redacted_headers
89
+ @data.to_hash.tap do |headers|
90
+ headers["api-key"] = "[FILTERED]" if headers.key?("api-key")
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Qdrant
6
+ class Client
7
+ Response = Struct.new(:status, :headers, :body)
8
+
9
+ class ResponseBuilder
10
+ JSON_CONTENT_TYPE_REGEX = /\bjson\z/
11
+
12
+ def initialize(response)
13
+ @response = response
14
+ end
15
+
16
+ def build
17
+ Response.new(@response.code.to_i, @response.to_hash, parse_body)
18
+ end
19
+
20
+ private
21
+
22
+ def parse_body
23
+ body = @response.body
24
+ return body unless !body.nil? && !body.empty? && json_response?
25
+
26
+ JSON.parse(body)
27
+ rescue JSON::ParserError
28
+ # Fallback to raw body if JSON parsing fails unexpectedly
29
+ body
30
+ end
31
+
32
+ def json_response?
33
+ content_type = @response["Content-Type"]
34
+ return false if content_type.nil?
35
+
36
+ media_type = content_type.split(";").first.to_s.strip
37
+ media_type.match?(JSON_CONTENT_TYPE_REGEX)
38
+ end
39
+ end
40
+ end
41
+ end
data/lib/qdrant/client.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "faraday"
3
+ require "logger"
4
4
  require "forwardable"
5
5
 
6
+ require_relative "client/connection"
7
+
6
8
  module Qdrant
7
9
  class Client
8
10
  extend Forwardable
@@ -14,11 +16,11 @@ module Qdrant
14
16
  def initialize(
15
17
  url:,
16
18
  api_key: nil,
17
- adapter: Faraday.default_adapter,
18
19
  raise_error: false,
19
- logger: nil
20
+ logger: nil,
21
+ adapter: nil # Deprecated. Doesn't select the transport. Should be removed in subsequent releases
20
22
  )
21
- @url = url
23
+ @url = normalize_url(url)
22
24
  @api_key = api_key
23
25
  @adapter = adapter
24
26
  @raise_error = raise_error
@@ -26,20 +28,16 @@ module Qdrant
26
28
  end
27
29
 
28
30
  def connection
29
- @connection ||= Faraday.new(url: url) do |faraday|
30
- if api_key
31
- faraday.headers["api-key"] = api_key
32
- end
33
- faraday.request :json
34
- faraday.response :logger, @logger, {headers: true, bodies: true, errors: true}
35
- faraday.response :raise_error if raise_error
36
- faraday.response :json, content_type: /\bjson$/
37
- faraday.adapter adapter
38
- end
31
+ @connection ||= Connection.new(
32
+ url: url,
33
+ api_key: api_key,
34
+ raise_error: raise_error,
35
+ logger: logger
36
+ )
39
37
  end
40
38
 
41
39
  def aliases
42
- @aliases ||= Qdrant::Aliases.new(client: self).list
40
+ Qdrant::Aliases.new(client: self).list
43
41
  end
44
42
 
45
43
  def collections
@@ -61,5 +59,14 @@ module Qdrant
61
59
  def points
62
60
  @points ||= Qdrant::Points.new(client: self)
63
61
  end
62
+
63
+ private
64
+
65
+ def normalize_url(url)
66
+ raise ArgumentError, "url must be a String" unless url.is_a?(String)
67
+ return url if url.start_with?("http://", "https://")
68
+
69
+ "https://#{url}"
70
+ end
64
71
  end
65
72
  end
@@ -21,7 +21,7 @@ module Qdrant
21
21
  peer_id:,
22
22
  force: nil
23
23
  )
24
- response = client.connection.post("#{PATH}/recover") do |req|
24
+ response = client.connection.delete("#{PATH}/peer/#{peer_id}") do |req|
25
25
  req.params["force"] = force if force
26
26
  end
27
27
  response.body
@@ -31,6 +31,8 @@ module Qdrant
31
31
  init_from: nil,
32
32
  quantization_config: nil
33
33
  )
34
+ deprecation_warning("`init_from:` was removed in Qdrant 1.16 and is ignored by the server.") unless init_from.nil?
35
+
34
36
  response = client.connection.put("#{PATH}/#{collection_name}") do |req|
35
37
  req.body = {}
36
38
  req.body["vectors"] = vectors
@@ -176,7 +178,7 @@ module Qdrant
176
178
  priority: nil,
177
179
  wait: nil
178
180
  )
179
- response = client.connection.post("#{PATH}/#{collection_name}/snapshots/recover") do |req|
181
+ response = client.connection.put("#{PATH}/#{collection_name}/snapshots/recover") do |req|
180
182
  req.params["wait"] = wait unless wait.nil?
181
183
 
182
184
  req.body = {
data/lib/qdrant/points.rb CHANGED
@@ -4,10 +4,10 @@ module Qdrant
4
4
  class Points < Base
5
5
  PATH = "points"
6
6
 
7
- # Lists all data objects in reverse order of creation. The data will be returned as an array of objects.
7
+ # Retrieve full information of points by ids. Same as `get_all`.
8
8
  def list(
9
9
  collection_name:,
10
- ids: nil,
10
+ ids:,
11
11
  with_payload: nil,
12
12
  with_vector: nil,
13
13
  consistency: nil
@@ -51,7 +51,6 @@ module Qdrant
51
51
  ordering: nil,
52
52
  filter: nil
53
53
  )
54
-
55
54
  raise ArgumentError, "Either points or filter should be provided" if points.nil? && filter.nil?
56
55
 
57
56
  response = client.connection.post("collections/#{collection_name}/#{PATH}/delete") do |req|
@@ -213,6 +212,8 @@ module Qdrant
213
212
  score_threshold: nil,
214
213
  consistency: nil
215
214
  )
215
+ deprecation_warning("`points.search` is deprecated since Qdrant 1.13 and will be removed from the server; use `points.query` instead.")
216
+
216
217
  response = client.connection.post("collections/#{collection_name}/#{PATH}/search") do |req|
217
218
  req.params["consistency"] = consistency unless consistency.nil?
218
219
 
@@ -235,6 +236,8 @@ module Qdrant
235
236
  searches:,
236
237
  consistency: nil
237
238
  )
239
+ deprecation_warning("`points.batch_search` is deprecated since Qdrant 1.13 and will be removed from the server; use `points.query` instead.")
240
+
238
241
  response = client.connection.post("collections/#{collection_name}/#{PATH}/search/batch") do |req|
239
242
  req.params["consistency"] = consistency unless consistency.nil?
240
243
 
@@ -260,6 +263,8 @@ module Qdrant
260
263
  lookup_from: nil,
261
264
  consistency: nil
262
265
  )
266
+ deprecation_warning("`points.recommend` is deprecated since Qdrant 1.13 and will be removed from the server; use `points.query` instead.")
267
+
263
268
  response = client.connection.post("collections/#{collection_name}/#{PATH}/recommend") do |req|
264
269
  req.params["consistency"] = consistency unless consistency.nil?
265
270
 
@@ -285,6 +290,8 @@ module Qdrant
285
290
  searches:,
286
291
  consistency: nil
287
292
  )
293
+ deprecation_warning("`points.batch_recommend` is deprecated since Qdrant 1.13 and will be removed from the server; use `points.query` instead.")
294
+
288
295
  response = client.connection.post("collections/#{collection_name}/#{PATH}/recommend/batch") do |req|
289
296
  req.params["consistency"] = consistency unless consistency.nil?
290
297
 
@@ -27,6 +27,8 @@ module Qdrant
27
27
  write:,
28
28
  error_message: nil
29
29
  )
30
+ deprecation_warning("the lock API was removed in Qdrant 1.16; `set_lock` only works against older servers.")
31
+
30
32
  response = client.connection.post("locks") do |req|
31
33
  req.body = {
32
34
  write: write
@@ -38,6 +40,8 @@ module Qdrant
38
40
 
39
41
  # Get lock options. If write is locked, all write operations and collection creation are forbidden
40
42
  def locks
43
+ deprecation_warning("the lock API was removed in Qdrant 1.16; `locks` only works against older servers.")
44
+
41
45
  response = client.connection.get("locks")
42
46
  response.body
43
47
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Qdrant
4
- VERSION = "0.9.10"
4
+ VERSION = "0.11.0"
5
5
  end
data/script/e2e.rb ADDED
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "logger"
5
+ require "securerandom"
6
+ require "qdrant"
7
+
8
+ def assert!(condition, message)
9
+ raise "e2e assertion failed: #{message}" unless condition
10
+ end
11
+
12
+ def ok_body!(body, operation)
13
+ assert!(body.is_a?(Hash), "#{operation} returned #{body.class}, expected Hash")
14
+ assert!(body["status"] == "ok", "#{operation} returned status #{body["status"].inspect}")
15
+ body
16
+ end
17
+
18
+ url = ENV.fetch("QDRANT_URL", "").strip
19
+ api_key = ENV.fetch("QDRANT_API_KEY", "").strip
20
+
21
+ abort "QDRANT_URL must be set to an https:// hosted endpoint" if url.empty? || !url.start_with?("https://")
22
+ abort "QDRANT_API_KEY must be set" if api_key.empty?
23
+
24
+ logger = Logger.new($stderr)
25
+ logger.level = Logger::WARN
26
+
27
+ client = Qdrant::Client.new(
28
+ url: url,
29
+ api_key: api_key,
30
+ raise_error: true,
31
+ logger: logger
32
+ )
33
+
34
+ collection_name = "qdrant_ruby_e2e_#{Process.pid}_#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_#{SecureRandom.hex(4)}"
35
+ collection_created = false
36
+ failure = nil
37
+
38
+ begin
39
+ body = ok_body!(client.collections.list, "collections.list")
40
+ assert!(body.dig("result", "collections").is_a?(Array), "collections.list result is not an Array")
41
+ puts "e2e: connection and authentication passed"
42
+
43
+ body = client.collections.create(
44
+ collection_name: collection_name,
45
+ vectors: {size: 3, distance: "Cosine"}
46
+ )
47
+ collection_created = true
48
+ ok_body!(body, "collections.create")
49
+ assert!(body["result"] == true, "collections.create did not return result=true")
50
+ puts "e2e: collection created (#{collection_name})"
51
+
52
+ body = ok_body!(client.collections.get(collection_name: collection_name), "collections.get")
53
+ status = body.dig("result", "status")
54
+ assert!(%w[green yellow].include?(status), "collection status was #{status.inspect}")
55
+
56
+ points = [
57
+ {id: 1, vector: [1.0, 0.0, 0.0], payload: {"source" => "hosted-e2e", "rank" => 1}},
58
+ {id: 2, vector: [0.0, 1.0, 0.0], payload: {"source" => "hosted-e2e", "rank" => 2}}
59
+ ]
60
+
61
+ body = ok_body!(
62
+ client.points.upsert(collection_name: collection_name, wait: true, points: points),
63
+ "points.upsert"
64
+ )
65
+ assert!(body.dig("result", "status") == "completed", "points.upsert did not return a completed status")
66
+
67
+ body = ok_body!(client.points.get(collection_name: collection_name, id: 1), "points.get")
68
+ assert!(body.dig("result", "id") == 1, "points.get returned the wrong point")
69
+
70
+ body = ok_body!(
71
+ client.points.get_all(
72
+ collection_name: collection_name,
73
+ ids: [1, 2],
74
+ with_payload: true,
75
+ with_vector: true
76
+ ),
77
+ "points.get_all"
78
+ )
79
+ results = body.fetch("result")
80
+ assert!(results.is_a?(Array) && results.length == 2, "points.get_all did not return two points")
81
+ assert!(results.map { |point| point["id"] }.sort == [1, 2], "points.get_all returned unexpected IDs")
82
+ assert!(results.find do |point|
83
+ point["id"] == 1
84
+ end.dig("payload", "source") == "hosted-e2e", "point payload was not persisted")
85
+ puts "e2e: point write and read round trip passed"
86
+
87
+ body = ok_body!(
88
+ client.points.query(
89
+ collection_name: collection_name,
90
+ query: [1.0, 0.0, 0.0],
91
+ limit: 1,
92
+ with_payload: true,
93
+ with_vector: false
94
+ ),
95
+ "points.query"
96
+ )
97
+ hit = body.dig("result", "points").first
98
+ assert!(hit && hit["id"] == 1, "points.query did not return point 1 first")
99
+ assert!(hit["score"].to_f > 0.99, "points.query score was #{hit["score"].inspect}")
100
+
101
+ body = ok_body!(client.points.count(collection_name: collection_name, exact: true), "points.count")
102
+ assert!(body.dig("result", "count") == 2, "points.count did not return 2")
103
+ puts "e2e: query and count passed"
104
+
105
+ body = ok_body!(client.points.delete(collection_name: collection_name, points: [1, 2], wait: true), "points.delete")
106
+ assert!(body.dig("result", "status") == "completed", "points.delete did not return a completed status")
107
+ body = ok_body!(client.points.count(collection_name: collection_name, exact: true), "points.count after delete")
108
+ assert!(body.dig("result", "count") == 0, "points.count after delete did not return 0")
109
+ puts "e2e: point deletion passed"
110
+ rescue => e
111
+ failure = e
112
+ warn "e2e failed: #{e.class}: #{e.message}"
113
+ ensure
114
+ if collection_created
115
+ begin
116
+ body = ok_body!(client.collections.delete(collection_name: collection_name), "collections.delete cleanup")
117
+ assert!(body["result"] == true, "collections.delete cleanup did not return result=true")
118
+ puts "e2e: temporary collection deleted"
119
+ rescue => e
120
+ failure ||= e
121
+ warn "e2e cleanup failed: #{e.class}: #{e.message}"
122
+ end
123
+ end
124
+ end
125
+
126
+ if failure
127
+ exit 1
128
+ else
129
+ puts "e2e passed"
130
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qdrant-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.10
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Bondarev
@@ -10,25 +10,19 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: faraday
13
+ name: logger
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 2.0.1
19
- - - "<"
20
- - !ruby/object:Gem::Version
21
- version: '3'
18
+ version: '0'
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
23
  - - ">="
27
24
  - !ruby/object:Gem::Version
28
- version: 2.0.1
29
- - - "<"
30
- - !ruby/object:Gem::Version
31
- version: '3'
25
+ version: '0'
32
26
  - !ruby/object:Gem::Dependency
33
27
  name: pry-byebug
34
28
  requirement: !ruby/object:Gem::Requirement
@@ -63,6 +57,9 @@ files:
63
57
  - lib/qdrant/aliases.rb
64
58
  - lib/qdrant/base.rb
65
59
  - lib/qdrant/client.rb
60
+ - lib/qdrant/client/connection.rb
61
+ - lib/qdrant/client/request.rb
62
+ - lib/qdrant/client/response.rb
66
63
  - lib/qdrant/clusters.rb
67
64
  - lib/qdrant/collections.rb
68
65
  - lib/qdrant/error.rb
@@ -70,6 +67,7 @@ files:
70
67
  - lib/qdrant/service.rb
71
68
  - lib/qdrant/snapshots.rb
72
69
  - lib/qdrant/version.rb
70
+ - script/e2e.rb
73
71
  - sig/qdrant.rbs
74
72
  homepage: https://github.com/andreibondarev/qdrant-ruby
75
73
  licenses:
@@ -85,7 +83,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
85
83
  requirements:
86
84
  - - ">="
87
85
  - !ruby/object:Gem::Version
88
- version: 2.6.0
86
+ version: '3.3'
89
87
  required_rubygems_version: !ruby/object:Gem::Requirement
90
88
  requirements:
91
89
  - - ">="