weaviate-ruby 0.9.1 → 0.10.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.
@@ -3,4 +3,56 @@
3
3
  module Weaviate
4
4
  class Error < StandardError
5
5
  end
6
+
7
+ # Raised when Weaviate cannot be reached
8
+ class ConnectionFailed < Error
9
+ end
10
+
11
+ # Raised when a request times out
12
+ class TimeoutError < ConnectionFailed
13
+ end
14
+
15
+ # Raised when Weaviate responds with a 4xx or 5xx status
16
+ class HTTPError < Error
17
+ attr_reader :response
18
+
19
+ def initialize(message = nil, response: nil)
20
+ @response = response
21
+ super(message || "the server responded with status #{response&.status}")
22
+ end
23
+
24
+ def status
25
+ response&.status
26
+ end
27
+
28
+ def body
29
+ response&.body
30
+ end
31
+ end
32
+
33
+ class ClientError < HTTPError; end
34
+
35
+ class BadRequestError < ClientError; end
36
+
37
+ class UnauthorizedError < ClientError; end
38
+
39
+ class ForbiddenError < ClientError; end
40
+
41
+ class ResourceNotFound < ClientError; end
42
+
43
+ class ConflictError < ClientError; end
44
+
45
+ class UnprocessableEntityError < ClientError; end
46
+
47
+ class ServerError < HTTPError; end
48
+
49
+ # Raised when a GraphQL query returns errors
50
+ class GraphQLError < Error
51
+ attr_reader :errors
52
+
53
+ def initialize(errors)
54
+ @errors = errors
55
+ super(errors.map { |e| e["message"] }.join(", "))
56
+ end
57
+ end
6
58
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Weaviate
4
+ class GraphQL < Base
5
+ PATH = "graphql"
6
+
7
+ # Execute a GraphQL query and return the "data" portion of the response.
8
+ def execute(query)
9
+ response = client.connection.post(PATH) do |req|
10
+ req.body = {query: query}
11
+ end
12
+
13
+ errors = response.body["errors"]
14
+ raise Weaviate::GraphQLError.new(errors) if errors && !errors.empty?
15
+
16
+ response.body["data"]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Weaviate
4
+ # Manage roles of OIDC groups. Requires RBAC and OIDC to be enabled.
5
+ class Groups < Base
6
+ PATH = "authz/groups"
7
+ GROUP_TYPE = "oidc"
8
+
9
+ # Groups that have roles assigned.
10
+ def list
11
+ response = client.connection.get("#{PATH}/#{GROUP_TYPE}")
12
+ response.body
13
+ end
14
+
15
+ def roles(group_id:, include_full_roles: nil)
16
+ response = client.connection.get("#{PATH}/#{group_id}/roles/#{GROUP_TYPE}") do |req|
17
+ req.params["includeFullRoles"] = include_full_roles unless include_full_roles.nil?
18
+ end
19
+ response.body
20
+ end
21
+
22
+ def assign_roles(group_id:, roles:)
23
+ update_roles(group_id, "assign", roles)
24
+ end
25
+
26
+ def revoke_roles(group_id:, roles:)
27
+ update_roles(group_id, "revoke", roles)
28
+ end
29
+
30
+ private
31
+
32
+ def update_roles(group_id, action, roles)
33
+ response = client.connection.post("#{PATH}/#{group_id}/#{action}") do |req|
34
+ req.body = {roles: Array(roles), groupType: GROUP_TYPE}
35
+ end
36
+ response.success?
37
+ end
38
+ end
39
+ end
@@ -4,14 +4,23 @@ module Weaviate
4
4
  class Health < Base
5
5
  PATH = ".well-known"
6
6
 
7
+ # Whether Weaviate is alive. Returns false when it responds with an error or cannot be reached.
7
8
  def live?
8
- response = client.connection.get("#{PATH}/live")
9
- response.status == 200
9
+ check("#{PATH}/live")
10
10
  end
11
11
 
12
+ # Whether Weaviate is ready to receive traffic. Returns false when it responds with an error
13
+ # (e.g. 503 while starting up) or cannot be reached.
12
14
  def ready?
13
- response = client.connection.get("#{PATH}/ready")
14
- response.status == 200
15
+ check("#{PATH}/ready")
16
+ end
17
+
18
+ private
19
+
20
+ def check(path)
21
+ client.connection.get(path).status == 200
22
+ rescue Weaviate::HTTPError, Weaviate::ConnectionFailed
23
+ false
15
24
  end
16
25
  end
17
26
  end
@@ -4,8 +4,16 @@ module Weaviate
4
4
  class Nodes < Base
5
5
  PATH = "nodes"
6
6
 
7
- def list
8
- response = client.connection.get(PATH)
7
+ # @param output [String] "minimal" (default) or "verbose", which includes shard details
8
+ # @param class_name [String] Only return shards of this collection
9
+ # @param shard_name [String] Only return this shard, requires class_name
10
+ def list(output: nil, class_name: nil, shard_name: nil)
11
+ path = class_name.nil? ? PATH : "#{PATH}/#{class_name}"
12
+
13
+ response = client.connection.get(path) do |req|
14
+ req.params["output"] = output unless output.nil?
15
+ req.params["shardName"] = shard_name unless shard_name.nil?
16
+ end
9
17
  response.body
10
18
  end
11
19
  end
@@ -66,10 +66,12 @@ module Weaviate
66
66
  )
67
67
  validate_consistency_level!(consistency_level) unless consistency_level.nil?
68
68
 
69
+ # Weaviate expects the tenant on each object rather than on the batch
70
+ objects = objects.map { |object| {tenant: tenant}.merge(object) } unless tenant.nil?
71
+
69
72
  response = client.connection.post("batch/#{PATH}") do |req|
70
73
  req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
71
74
  req.body = {objects: objects}
72
- req.body["tenant"] = tenant unless tenant.nil?
73
75
  end
74
76
 
75
77
  response.body
@@ -109,6 +111,8 @@ module Weaviate
109
111
  end
110
112
 
111
113
  response.status == 204
114
+ rescue Weaviate::ResourceNotFound
115
+ false
112
116
  end
113
117
 
114
118
  # Update an individual data object based on its uuid.
@@ -175,11 +179,7 @@ module Weaviate
175
179
  req.params["tenant"] = tenant unless tenant.nil?
176
180
  end
177
181
 
178
- if response.success?
179
- response.body.empty?
180
- else
181
- response.body
182
- end
182
+ response.body.empty?
183
183
  end
184
184
 
185
185
  def batch_delete(
@@ -212,6 +212,92 @@ module Weaviate
212
212
  response.body
213
213
  end
214
214
 
215
+ # Add a reference to an object's reference property.
216
+ def add_reference(
217
+ class_name:,
218
+ id:,
219
+ property:,
220
+ to_class_name:,
221
+ to_id:,
222
+ tenant: nil,
223
+ consistency_level: nil
224
+ )
225
+ validate_consistency_level!(consistency_level) unless consistency_level.nil?
226
+
227
+ response = client.connection.post(reference_path(class_name, id, property)) do |req|
228
+ req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
229
+ req.params["tenant"] = tenant unless tenant.nil?
230
+ req.body = {beacon: beacon(to_class_name, to_id)}
231
+ end
232
+
233
+ response.success?
234
+ end
235
+
236
+ # Replace all references of an object's reference property.
237
+ # @param to [Array<Hash>] The objects to reference, e.g. [{ class_name: "Category", id: "..." }]
238
+ def replace_references(
239
+ class_name:,
240
+ id:,
241
+ property:,
242
+ to:,
243
+ tenant: nil,
244
+ consistency_level: nil
245
+ )
246
+ validate_consistency_level!(consistency_level) unless consistency_level.nil?
247
+
248
+ response = client.connection.put(reference_path(class_name, id, property)) do |req|
249
+ req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
250
+ req.params["tenant"] = tenant unless tenant.nil?
251
+ req.body = to.map { |ref| {beacon: beacon(ref.fetch(:class_name), ref.fetch(:id))} }
252
+ end
253
+
254
+ response.success?
255
+ end
256
+
257
+ # Delete a reference from an object's reference property.
258
+ def delete_reference(
259
+ class_name:,
260
+ id:,
261
+ property:,
262
+ to_class_name:,
263
+ to_id:,
264
+ tenant: nil,
265
+ consistency_level: nil
266
+ )
267
+ validate_consistency_level!(consistency_level) unless consistency_level.nil?
268
+
269
+ response = client.connection.delete(reference_path(class_name, id, property)) do |req|
270
+ req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
271
+ req.params["tenant"] = tenant unless tenant.nil?
272
+ req.body = {beacon: beacon(to_class_name, to_id)}
273
+ end
274
+
275
+ response.success?
276
+ end
277
+
278
+ # Batch create references.
279
+ # @param references [Array<Hash>] e.g. [{ from_class_name: "Post", from_id: "...", property: "ofCategory", to_class_name: "Category", to_id: "...", tenant: nil }]
280
+ def batch_create_references(
281
+ references:,
282
+ consistency_level: nil
283
+ )
284
+ validate_consistency_level!(consistency_level) unless consistency_level.nil?
285
+
286
+ response = client.connection.post("batch/references") do |req|
287
+ req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
288
+ req.body = references.map do |ref|
289
+ body = {
290
+ from: "#{beacon(ref.fetch(:from_class_name), ref.fetch(:from_id))}/#{ref.fetch(:property)}",
291
+ to: beacon(ref.fetch(:to_class_name), ref.fetch(:to_id))
292
+ }
293
+ body[:tenant] = ref[:tenant] unless ref[:tenant].nil?
294
+ body
295
+ end
296
+ end
297
+
298
+ response.body
299
+ end
300
+
215
301
  # Validate a data object
216
302
  def validate(
217
303
  class_name:,
@@ -225,15 +311,19 @@ module Weaviate
225
311
  req.body["id"] = id unless id.nil?
226
312
  end
227
313
 
228
- if response.success?
229
- response.body.empty?
230
- else
231
- response.body
232
- end
314
+ response.body.empty?
233
315
  end
234
316
 
235
317
  private
236
318
 
319
+ def reference_path(class_name, id, property)
320
+ "#{PATH}/#{class_name}/#{id}/references/#{property}"
321
+ end
322
+
323
+ def beacon(class_name, id)
324
+ "weaviate://localhost/#{class_name}/#{id}"
325
+ end
326
+
237
327
  def validate_consistency_level!(consistency_level)
238
328
  unless %w[ONE QUORUM ALL].include?(consistency_level.to_s.upcase)
239
329
  raise ArgumentError, 'consistency_level must be either "ONE" or "QUORUM" OR "ALL"'
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module Weaviate
4
6
  class Query < Base
7
+ # Fields whose values are GraphQL enums, so strings are sent unquoted, e.g. operator: "Equal"
8
+ ENUM_KEYS = %w[operator order fusionType].freeze
9
+
10
+ GRAPHQL_NAME = /\A[_A-Za-z][_0-9A-Za-z]*\z/
11
+
5
12
  def get(
6
13
  class_name:,
7
14
  fields:,
@@ -18,14 +25,18 @@ module Weaviate
18
25
  near_object: nil,
19
26
  with_hybrid: nil,
20
27
  bm25: nil,
21
- ask: nil
28
+ ask: nil,
29
+ group_by: nil
22
30
  )
23
- response = client.graphql.execute(
31
+ data = client.graphql.execute(
24
32
  get_query(
25
33
  class_name: class_name,
26
- tenant: tenant,
27
34
  fields: fields,
35
+ after: after,
36
+ tenant: tenant,
37
+ limit: limit,
28
38
  autocut: autocut,
39
+ offset: offset,
29
40
  sort: sort,
30
41
  where: where,
31
42
  near_text: near_text,
@@ -34,15 +45,11 @@ module Weaviate
34
45
  near_object: near_object,
35
46
  with_hybrid: with_hybrid,
36
47
  bm25: bm25,
37
- ask: ask
38
- ),
39
- after: after,
40
- limit: limit,
41
- offset: offset
48
+ ask: ask,
49
+ group_by: group_by
50
+ )
42
51
  )
43
- response.original_hash.dig("data", "Get", class_name)
44
- rescue Graphlient::Errors::ExecutionError => error
45
- raise Weaviate::Error.new(error.response.data.get.errors.messages.to_h)
52
+ data.dig("Get", class_name)
46
53
  end
47
54
 
48
55
  def aggs(
@@ -55,27 +62,30 @@ module Weaviate
55
62
  near_vector: nil,
56
63
  near_image: nil,
57
64
  near_object: nil,
58
- group_by: nil
65
+ group_by: nil,
66
+ limit: nil,
67
+ with_hybrid: nil
59
68
  )
60
- response = client.graphql.execute(
69
+ data = client.graphql.execute(
61
70
  aggs_query(
62
71
  class_name: class_name,
63
- tenant: tenant,
64
72
  fields: fields,
73
+ tenant: tenant,
65
74
  where: where,
75
+ object_limit: object_limit,
66
76
  near_text: near_text,
67
77
  near_vector: near_vector,
68
78
  near_image: near_image,
69
- near_object: near_object
70
- ),
71
- group_by: group_by,
72
- object_limit: object_limit
79
+ near_object: near_object,
80
+ group_by: group_by,
81
+ limit: limit,
82
+ with_hybrid: with_hybrid
83
+ )
73
84
  )
74
- response.original_hash.dig("data", "Aggregate", class_name)
75
- rescue Graphlient::Errors::ExecutionError => error
76
- raise Weaviate::Error.new(error.response.data.aggregate.errors.messages.to_h)
85
+ data.dig("Aggregate", class_name)
77
86
  end
78
87
 
88
+ # @deprecated Weaviate disables Explore when more than one vectorizer module is enabled.
79
89
  def explore(
80
90
  fields:,
81
91
  after: nil,
@@ -88,29 +98,35 @@ module Weaviate
88
98
  near_image: nil,
89
99
  near_object: nil
90
100
  )
91
- response = client.graphql.execute(
101
+ unless @explore_deprecation_warned
102
+ warn "[weaviate-ruby] query.explore is deprecated: Weaviate disables Explore when more than one vectorizer module is enabled, " \
103
+ "which is the default on recent versions and on Weaviate Cloud. Use query.get on each collection instead."
104
+ @explore_deprecation_warned = true
105
+ end
106
+
107
+ # `after` is accepted for backwards compatibility, but Explore does not support it
108
+ data = client.graphql.execute(
92
109
  explore_query(
93
110
  fields: fields,
111
+ limit: limit,
112
+ offset: offset,
94
113
  sort: sort,
95
114
  where: where,
96
115
  near_text: near_text,
97
116
  near_vector: near_vector,
98
117
  near_image: near_image,
99
118
  near_object: near_object
100
- ),
101
- after: after,
102
- limit: limit,
103
- offset: offset
119
+ )
104
120
  )
105
- response.original_hash.dig("data", "Explore")
106
- rescue Graphlient::Errors::ExecutionError => error
107
- raise Weaviate::Error.new(error.to_s)
121
+ data["Explore"]
108
122
  end
109
123
 
110
124
  private
111
125
 
112
126
  def explore_query(
113
127
  fields:,
128
+ limit: nil,
129
+ offset: nil,
114
130
  where: nil,
115
131
  near_text: nil,
116
132
  near_vector: nil,
@@ -118,21 +134,20 @@ module Weaviate
118
134
  near_object: nil,
119
135
  sort: nil
120
136
  )
121
- client.graphql.parse <<~GRAPHQL
122
- query(
123
- $limit: Int,
124
- $offset: Int
125
- ) {
126
- Explore (
127
- limit: $limit,
128
- offset: $offset,
129
- #{(!near_text.nil?) ? "nearText: #{near_text}" : ""},
130
- #{(!near_vector.nil?) ? "nearVector: #{near_vector}" : ""},
131
- #{(!near_image.nil?) ? "nearImage: #{near_image}" : ""},
132
- #{(!near_object.nil?) ? "nearObject: #{near_object}" : ""},
133
- #{(!where.nil?) ? "where: #{where}" : ""},
134
- #{(!sort.nil?) ? "sort: #{sort}" : ""}
135
- ) {
137
+ arguments = build_arguments(
138
+ limit: integer_value(limit),
139
+ offset: integer_value(offset),
140
+ nearText: near_text,
141
+ nearVector: near_vector,
142
+ nearImage: near_image,
143
+ nearObject: near_object,
144
+ where: where,
145
+ sort: sort
146
+ )
147
+
148
+ <<~GRAPHQL
149
+ {
150
+ Explore#{arguments} {
136
151
  #{fields}
137
152
  }
138
153
  }
@@ -142,8 +157,11 @@ module Weaviate
142
157
  def get_query(
143
158
  class_name:,
144
159
  fields:,
145
- autocut: nil,
160
+ after: nil,
146
161
  tenant: nil,
162
+ limit: nil,
163
+ autocut: nil,
164
+ offset: nil,
147
165
  where: nil,
148
166
  near_text: nil,
149
167
  near_vector: nil,
@@ -152,31 +170,31 @@ module Weaviate
152
170
  with_hybrid: nil,
153
171
  bm25: nil,
154
172
  ask: nil,
155
- sort: nil
173
+ sort: nil,
174
+ group_by: nil
156
175
  )
157
- client.graphql.parse <<~GRAPHQL
158
- query(
159
- $after: String,
160
- $limit: Int,
161
- $offset: Int,
162
- ) {
176
+ arguments = build_arguments(
177
+ after: string_value(after),
178
+ limit: integer_value(limit),
179
+ offset: integer_value(offset),
180
+ autocut: integer_value(autocut),
181
+ tenant: string_value(tenant),
182
+ nearText: near_text,
183
+ nearVector: near_vector,
184
+ nearImage: near_image,
185
+ nearObject: near_object,
186
+ hybrid: with_hybrid,
187
+ bm25: bm25,
188
+ ask: ask,
189
+ where: where,
190
+ sort: sort,
191
+ groupBy: group_by
192
+ )
193
+
194
+ <<~GRAPHQL
195
+ {
163
196
  Get {
164
- #{class_name}(
165
- after: $after,
166
- limit: $limit,
167
- offset: $offset,
168
- #{(!autocut.nil?) ? "autocut: #{autocut}" : ""},
169
- #{(!tenant.nil?) ? "tenant: \"#{tenant}\"" : ""},
170
- #{(!near_text.nil?) ? "nearText: #{near_text}" : ""},
171
- #{(!near_vector.nil?) ? "nearVector: #{near_vector}" : ""},
172
- #{(!near_image.nil?) ? "nearImage: #{near_image}" : ""},
173
- #{(!near_object.nil?) ? "nearObject: #{near_object}" : ""},
174
- #{(!with_hybrid.nil?) ? "hybrid: #{with_hybrid}" : ""},
175
- #{(!bm25.nil?) ? "bm25: #{bm25}" : ""},
176
- #{(!ask.nil?) ? "ask: #{ask}" : ""},
177
- #{(!where.nil?) ? "where: #{where}" : ""},
178
- #{(!sort.nil?) ? "sort: #{sort}" : ""}
179
- ) {
197
+ #{class_name}#{arguments} {
180
198
  #{fields}
181
199
  }
182
200
  }
@@ -189,32 +207,88 @@ module Weaviate
189
207
  fields:,
190
208
  tenant: nil,
191
209
  where: nil,
210
+ object_limit: nil,
192
211
  near_text: nil,
193
212
  near_vector: nil,
194
213
  near_image: nil,
195
- near_object: nil
214
+ near_object: nil,
215
+ group_by: nil,
216
+ limit: nil,
217
+ with_hybrid: nil
196
218
  )
197
- client.graphql.parse <<~GRAPHQL
198
- query(
199
- $group_by: [String],
200
- $object_limit: Int,
201
- ) {
219
+ arguments = build_arguments(
220
+ objectLimit: integer_value(object_limit),
221
+ limit: integer_value(limit),
222
+ groupBy: group_by && Array(group_by).map(&:to_s).to_json,
223
+ tenant: string_value(tenant),
224
+ nearText: near_text,
225
+ nearVector: near_vector,
226
+ nearImage: near_image,
227
+ nearObject: near_object,
228
+ hybrid: with_hybrid,
229
+ where: where
230
+ )
231
+
232
+ <<~GRAPHQL
233
+ {
202
234
  Aggregate {
203
- #{class_name}(
204
- objectLimit: $object_limit,
205
- groupBy: $group_by,
206
- #{(!tenant.nil?) ? "tenant: \"#{tenant}\"" : ""},
207
- #{(!near_text.nil?) ? "nearText: #{near_text}" : ""},
208
- #{(!near_vector.nil?) ? "nearVector: #{near_vector}" : ""},
209
- #{(!near_image.nil?) ? "nearImage: #{near_image}" : ""},
210
- #{(!near_object.nil?) ? "nearObject: #{near_object}" : ""},
211
- #{(!where.nil?) ? "where: #{where}" : ""}
212
- ) {
235
+ #{class_name}#{arguments} {
213
236
  #{fields}
214
237
  }
215
238
  }
216
239
  }
217
240
  GRAPHQL
218
241
  end
242
+
243
+ # Only arguments that were provided are included in the query.
244
+ # Weaviate fails on explicit nulls, e.g. "interface conversion: interface {} is nil, not int".
245
+ # String arguments are passed through as raw GraphQL, anything else is serialized with {#graphql_value}.
246
+ def build_arguments(**arguments)
247
+ arguments = arguments.reject { |_, value| value.nil? }
248
+ return "" if arguments.empty?
249
+
250
+ arguments = arguments.map do |name, value|
251
+ "#{name}: #{value.is_a?(String) ? value : graphql_value(value)}"
252
+ end
253
+
254
+ "(#{arguments.join(", ")})"
255
+ end
256
+
257
+ # Serializes a Ruby value into a GraphQL input value:
258
+ # { path: ["category"], operator: :Equal, valueText: "SCIENCE" }
259
+ # => { path: ["category"], operator: Equal, valueText: "SCIENCE" }
260
+ # Strings are quoted and escaped, symbols become enums.
261
+ def graphql_value(value, key = nil)
262
+ case value
263
+ when Hash
264
+ "{ #{value.map { |k, v| "#{graphql_name(k)}: #{graphql_value(v, k.to_s)}" }.join(", ")} }"
265
+ when Array
266
+ "[#{value.map { |v| graphql_value(v, key) }.join(", ")}]"
267
+ when Symbol
268
+ graphql_name(value)
269
+ when String
270
+ (ENUM_KEYS.include?(key) && value.match?(GRAPHQL_NAME)) ? value : value.to_json
271
+ when Numeric, true, false
272
+ value.to_s
273
+ when nil
274
+ "null"
275
+ else
276
+ raise ArgumentError, "Cannot convert #{value.class} to a GraphQL value"
277
+ end
278
+ end
279
+
280
+ def graphql_name(name)
281
+ name = name.to_s
282
+ raise ArgumentError, "Invalid GraphQL name: #{name.inspect}" unless name.match?(GRAPHQL_NAME)
283
+ name
284
+ end
285
+
286
+ def integer_value(value)
287
+ Integer(value) unless value.nil?
288
+ end
289
+
290
+ def string_value(value)
291
+ value.to_s.to_json unless value.nil?
292
+ end
219
293
  end
220
294
  end