loams 0.1.0 → 0.1.1
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 +4 -4
- data/README.md +80 -2
- data/gen/loams-descriptor.binpb +0 -0
- data/lib/loams/app.rb +726 -0
- data/lib/loams/batch.rb +93 -0
- data/lib/loams/builders.rb +622 -0
- data/lib/loams/client.rb +76 -2
- data/lib/loams/consistency.rb +201 -0
- data/lib/loams/descriptors.rb +6 -0
- data/lib/loams/errors.rb +60 -0
- data/lib/loams/invoker.rb +41 -2
- data/lib/loams/subscribe.rb +75 -0
- data/lib/loams/transport.rb +5 -3
- data/lib/loams/values.rb +87 -0
- data/lib/loams/version.rb +1 -1
- data/lib/loams.rb +11 -0
- metadata +7 -1
data/lib/loams/app.rb
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "loams/descriptors"
|
|
5
|
+
require "loams/builders"
|
|
6
|
+
require "loams/batch"
|
|
7
|
+
require "loams/consistency"
|
|
8
|
+
require "loams/pagination"
|
|
9
|
+
require "loams/subscribe"
|
|
10
|
+
require "loams/values"
|
|
11
|
+
|
|
12
|
+
module Loams
|
|
13
|
+
# The app-building modules (SDKP §2.2–§2.5): `namespaces`, `collections`,
|
|
14
|
+
# `documents`, `search`, `vector` and `graph`.
|
|
15
|
+
#
|
|
16
|
+
# Each call names its RPC and builds the request's proto3 JSON; the binding
|
|
17
|
+
# (retry class, idempotency-key field, request and response types) is read
|
|
18
|
+
# from the committed descriptor set, never typed here. Requests go through
|
|
19
|
+
# the client's invoker, so retries, keys, token refresh, timeouts and errors
|
|
20
|
+
# are R1–R12 as for every other module. Answers are the response messages;
|
|
21
|
+
# `Loams.to_h` turns one into plain Ruby.
|
|
22
|
+
module App
|
|
23
|
+
# The RPCs behind every app call, `[module, call] => rpc`. Checked against
|
|
24
|
+
# `sdks/fixtures/facade-index.json` by `ruby_facade_matches_index`.
|
|
25
|
+
CALLS = {
|
|
26
|
+
%w[namespaces create] => "loams.collection.v1.NamespaceService/CreateNamespace",
|
|
27
|
+
%w[namespaces list] => "loams.collection.v1.NamespaceService/ListNamespaces",
|
|
28
|
+
%w[collections create] => "loams.collection.v1.CollectionService/CreateCollection",
|
|
29
|
+
%w[collections list] => "loams.collection.v1.CollectionService/ListCollections",
|
|
30
|
+
%w[collections get] => "loams.collection.v1.CollectionService/GetCollection",
|
|
31
|
+
%w[collections drop] => "loams.collection.v1.CollectionService/DropCollection",
|
|
32
|
+
%w[collections add_fields] => "loams.collection.v1.CollectionService/AddFields",
|
|
33
|
+
%w[collections list_versions] => "loams.collection.v1.CollectionService/ListVersions",
|
|
34
|
+
%w[collections update_aliases] => "loams.collection.v1.CollectionService/UpdateAliases",
|
|
35
|
+
%w[collections scan] => "loams.collection.v1.CollectionService/Scan",
|
|
36
|
+
%w[collections set_hot] => "loams.collection.v1.CollectionService/SetHot",
|
|
37
|
+
%w[collections warm] => "loams.collection.v1.CollectionService/WarmCollection",
|
|
38
|
+
%w[documents write] => "loams.collection.v1.DocumentService/WriteDocuments",
|
|
39
|
+
%w[documents get] => "loams.collection.v1.DocumentService/GetDocuments",
|
|
40
|
+
%w[documents scroll] => "loams.collection.v1.DocumentService/ScrollDocuments",
|
|
41
|
+
%w[documents count] => "loams.collection.v1.DocumentService/CountDocuments",
|
|
42
|
+
%w[documents delete_by_filter] => "loams.collection.v1.DocumentService/DeleteByFilter",
|
|
43
|
+
%w[documents patch_by_filter] => "loams.collection.v1.DocumentService/PatchByFilter",
|
|
44
|
+
%w[search query] => "loams.collection.v1.QueryService/Search",
|
|
45
|
+
%w[vector search] => "loams.collection.v1.QueryService/Search",
|
|
46
|
+
%w[graph create] => "loams.graph.v1.GraphAdminService/CreateGraph",
|
|
47
|
+
%w[graph get] => "loams.graph.v1.GraphAdminService/GetGraph",
|
|
48
|
+
%w[graph list] => "loams.graph.v1.GraphAdminService/ListGraphs",
|
|
49
|
+
%w[graph update] => "loams.graph.v1.GraphAdminService/UpdateGraph",
|
|
50
|
+
%w[graph delete] => "loams.graph.v1.GraphAdminService/DeleteGraph",
|
|
51
|
+
%w[graph schema] => "loams.graph.v1.GraphAdminService/GetSchema",
|
|
52
|
+
%w[graph query] => "loams.graph.v1.GraphService/Execute",
|
|
53
|
+
%w[graph query_batch] => "loams.graph.v1.GraphService/ExecuteBatch",
|
|
54
|
+
%w[graph query_stream] => "loams.graph.v1.GraphService/ExecuteStream",
|
|
55
|
+
%w[graph explain] => "loams.graph.v1.GraphService/Explain",
|
|
56
|
+
%w[sql query] => "loams.sql.v1.SqlService/Query",
|
|
57
|
+
%w[sql query_arrow] => "loams.sql.v1.SqlService/QueryArrow",
|
|
58
|
+
%w[streams create] => "loams.stream.v1.StreamService/CreateStream",
|
|
59
|
+
%w[streams get] => "loams.stream.v1.StreamService/DescribeStream",
|
|
60
|
+
%w[streams list] => "loams.stream.v1.StreamService/ListStreams",
|
|
61
|
+
%w[streams produce] => "loams.stream.v1.StreamService/Produce",
|
|
62
|
+
%w[streams fetch] => "loams.stream.v1.StreamService/Fetch",
|
|
63
|
+
%w[streams produce_events] => "loams.stream.v1.StreamService/ProduceCloudEvents",
|
|
64
|
+
%w[streams fetch_events] => "loams.stream.v1.StreamService/FetchCloudEvents",
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
# The packages whose reads take the session's token (SDKP-17).
|
|
68
|
+
SESSION_SCOPES = %w[loams.collection.v1].freeze
|
|
69
|
+
|
|
70
|
+
# What every module shares: the call path from proto3 JSON to an answer.
|
|
71
|
+
class Base
|
|
72
|
+
def initialize(client)
|
|
73
|
+
@client = client
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Sends +request+ (proto3 JSON, or a message) to +rpc+.
|
|
79
|
+
#
|
|
80
|
+
# R11: a request with a `namespace` field gets the client's default
|
|
81
|
+
# namespace when it leaves it empty, and with neither it is refused
|
|
82
|
+
# here, before any network call. R4: with `session_consistency`, a read
|
|
83
|
+
# that leaves `consistency` unset reads at least the session's token, and
|
|
84
|
+
# every token an answer carries is merged into the session.
|
|
85
|
+
def rpc(path, request, options = {})
|
|
86
|
+
binding = Descriptors.binding_for_rpc(path)
|
|
87
|
+
raise UnimplementedError.new("this SDK's descriptor set has no #{path}", code: :unimplemented) if binding.nil?
|
|
88
|
+
|
|
89
|
+
message = to_message(binding, request)
|
|
90
|
+
fill_namespace(binding, message, path)
|
|
91
|
+
# SQL reads collections, so it reads (and adds to) the collection token.
|
|
92
|
+
scope = binding.package_name == "loams.sql.v1" ? "loams.collection.v1" : binding.package_name
|
|
93
|
+
# SDKP-17: only collection (and SQL) reads take the session's token; a
|
|
94
|
+
# graph's tokens name one graph's stream.
|
|
95
|
+
@client.session&.apply(scope, message) if SESSION_SCOPES.include?(scope)
|
|
96
|
+
call_options = options.dup
|
|
97
|
+
if binding.server_streaming?
|
|
98
|
+
return @client.invoker.server_stream(binding, message, call_options)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
answer = @client.invoker.unary(binding, message, call_options)
|
|
102
|
+
@client.session&.observe_message(scope, answer)
|
|
103
|
+
answer
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_message(binding, request)
|
|
107
|
+
return request if request.is_a?(binding.request.msgclass)
|
|
108
|
+
|
|
109
|
+
binding.request.msgclass.decode_json(JSON.generate(request || {}))
|
|
110
|
+
rescue Google::Protobuf::ParseError => e
|
|
111
|
+
raise InvalidArgumentError.new("#{binding.rpc}: #{e.message}", code: :invalid_argument, rpc: binding.rpc)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def fill_namespace(binding, message, path)
|
|
115
|
+
field = binding.request.lookup("namespace")
|
|
116
|
+
return if field.nil? || field.type != :string
|
|
117
|
+
|
|
118
|
+
current = message["namespace"]
|
|
119
|
+
return unless current.nil? || current.empty?
|
|
120
|
+
|
|
121
|
+
default = @client.namespace
|
|
122
|
+
if default.nil? || default.empty?
|
|
123
|
+
raise InvalidArgumentError.new(
|
|
124
|
+
"#{path} needs a namespace: pass namespace: or give the client one",
|
|
125
|
+
code: :invalid_argument,
|
|
126
|
+
reason: :namespace_required,
|
|
127
|
+
rpc: path,
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
message["namespace"] = default
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# `{namespace: ns}` when +ns+ is given, so the client default applies
|
|
134
|
+
# otherwise.
|
|
135
|
+
def ns(namespace)
|
|
136
|
+
namespace.nil? ? {} : { "namespace" => namespace.to_s }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def call_opts(kw)
|
|
140
|
+
kw.slice(:timeout, :headers, :idempotency_key, :max_retries)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Every item of a paged list, following `next_page_token`.
|
|
144
|
+
def paged(path, request, items, options)
|
|
145
|
+
binding = Descriptors.binding_for_rpc(path)
|
|
146
|
+
PageIterator.new(
|
|
147
|
+
fetch: proc { |token| rpc(path, request.merge("pageToken" => token.to_s), options) },
|
|
148
|
+
items_field: binding.response.lookup(items),
|
|
149
|
+
next_page_token_field: binding.response.lookup("next_page_token"),
|
|
150
|
+
).each
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Any app call by its facade name, over its request's proto3 JSON: what
|
|
155
|
+
# `Client#call` goes through.
|
|
156
|
+
class Raw < Base
|
|
157
|
+
def call(name, request, **opts)
|
|
158
|
+
mod, call = name.split(".", 2)
|
|
159
|
+
path = CALLS[[mod, call]] or raise ArgumentError, "no app call #{name}"
|
|
160
|
+
rpc(path, request, call_opts(opts))
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# `namespaces`: the container every collection and stream lives in.
|
|
165
|
+
class Namespaces < Base
|
|
166
|
+
def create(namespace, **opts)
|
|
167
|
+
rpc(CALLS[%w[namespaces create]], { "namespace" => namespace.to_s }, call_opts(opts))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def list(page_size: nil, page_token: nil, **opts)
|
|
171
|
+
rpc(CALLS[%w[namespaces list]], Builders.compact({ "pageSize" => page_size, "pageToken" => page_token }), call_opts(opts))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Every namespace, across pages, as an Enumerator.
|
|
175
|
+
def list_all(page_size: nil, **opts)
|
|
176
|
+
paged(CALLS[%w[namespaces list]], Builders.compact({ "pageSize" => page_size }), "namespaces", call_opts(opts))
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# `collections`: schema, aliases, versions and the hot tier.
|
|
181
|
+
class Collections < Base
|
|
182
|
+
# +schema+ is a `Schema` or its Hash.
|
|
183
|
+
def create(name, schema:, partitions: nil, namespace: nil, **opts)
|
|
184
|
+
rpc(CALLS[%w[collections create]], ns(namespace).merge(Builders.compact({
|
|
185
|
+
"name" => name.to_s, "schema" => Builders.json(schema), "partitions" => partitions,
|
|
186
|
+
})), call_opts(opts))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def get(name, namespace: nil, **opts)
|
|
190
|
+
rpc(CALLS[%w[collections get]], ns(namespace).merge("collection" => name.to_s), call_opts(opts))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def list(page_size: nil, page_token: nil, namespace: nil, **opts)
|
|
194
|
+
rpc(CALLS[%w[collections list]], ns(namespace).merge(Builders.compact({ "pageSize" => page_size, "pageToken" => page_token })), call_opts(opts))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def list_all(page_size: nil, namespace: nil, **opts)
|
|
198
|
+
paged(CALLS[%w[collections list]], ns(namespace).merge(Builders.compact({ "pageSize" => page_size })), "collections", call_opts(opts))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def drop(name, namespace: nil, **opts)
|
|
202
|
+
rpc(CALLS[%w[collections drop]], ns(namespace).merge("collection" => name.to_s), call_opts(opts))
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Additive schema change: +fields+ and +vectors+ are the schema
|
|
206
|
+
# builder's field and vector Hashes (`Schema#to_h["fields"]`).
|
|
207
|
+
def add_fields(name, fields: [], vectors: [], annotations: nil, namespace: nil, **opts)
|
|
208
|
+
fields = Builders.json(fields)
|
|
209
|
+
fields = fields["fields"] if fields.is_a?(Hash)
|
|
210
|
+
rpc(CALLS[%w[collections add_fields]], ns(namespace).merge(Builders.compact({
|
|
211
|
+
"collection" => name.to_s, "fields" => fields, "vectors" => vectors, "annotations" => annotations,
|
|
212
|
+
})), call_opts(opts))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def list_versions(name, page_size: nil, page_token: nil, namespace: nil, **opts)
|
|
216
|
+
rpc(CALLS[%w[collections list_versions]], ns(namespace).merge(Builders.compact({
|
|
217
|
+
"collection" => name.to_s, "pageSize" => page_size, "pageToken" => page_token,
|
|
218
|
+
})), call_opts(opts))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# +actions+: `[{create: {alias:, collection:}}, {delete: {alias:}}]`.
|
|
222
|
+
def update_aliases(actions, namespace: nil, **opts)
|
|
223
|
+
rpc(CALLS[%w[collections update_aliases]], ns(namespace).merge("actions" => actions), call_opts(opts))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def scan(name, at: nil, namespace: nil, **opts)
|
|
227
|
+
rpc(CALLS[%w[collections scan]], ns(namespace).merge(Builders.compact({ "collection" => name.to_s, "at" => at })), call_opts(opts))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def set_hot(name, vectors: false, text: false, fragments: false, namespace: nil, **opts)
|
|
231
|
+
rpc(CALLS[%w[collections set_hot]], ns(namespace).merge(
|
|
232
|
+
"collection" => name.to_s, "hot" => { "vectors" => vectors, "text" => text, "fragments" => fragments },
|
|
233
|
+
), call_opts(opts))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def warm(name, namespace: nil, **opts)
|
|
237
|
+
rpc(CALLS[%w[collections warm]], ns(namespace).merge("collection" => name.to_s), call_opts(opts))
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# `documents`: write, get, scroll, count and the filter writes.
|
|
242
|
+
class Documents < Base
|
|
243
|
+
# Raw write ops (`WriteOp` JSON). Returns the response; its `token` is
|
|
244
|
+
# the write's consistency token.
|
|
245
|
+
def write(collection, ops, report_existence: false, namespace: nil, **opts)
|
|
246
|
+
rpc(CALLS[%w[documents write]], ns(namespace).merge(
|
|
247
|
+
"collection" => collection.to_s, "ops" => ops, "reportExistence" => report_existence,
|
|
248
|
+
), call_opts(opts))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# +docs+: `[{id:, source:, vectors:, sparse_vectors:}]`.
|
|
252
|
+
def upsert(collection, docs, **kw)
|
|
253
|
+
write(collection, docs.map { |d| { "upsert" => Documents.document(d) } }, **kw)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def delete(collection, ids, **kw)
|
|
257
|
+
write(collection, Array(ids).map { |id| { "delete" => { "id" => Values.document_id(id) } } }, **kw)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Sets the keys of +set+ and removes the keys named in +unset+.
|
|
261
|
+
def patch(collection, id, set: nil, unset: nil, vectors: nil, mode: nil, **kw)
|
|
262
|
+
patch = Builders.compact({
|
|
263
|
+
"id" => Values.document_id(id), "mode" => mode&.to_s, "source" => set,
|
|
264
|
+
"deleteKeys" => unset&.map(&:to_s), "vectors" => vectors,
|
|
265
|
+
})
|
|
266
|
+
write(collection, [{ "patch" => patch }], **kw)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# R13: +docs+ (any Enumerable of `{id:, source:, …}`) in chunks, one
|
|
270
|
+
# in flight at a time. Returns a `BatchResult` (the merged token, the
|
|
271
|
+
# chunks and the rows); a failed chunk raises `BatchError`.
|
|
272
|
+
def write_batch(collection, docs, chunk_rows: Batch::DEFAULT_CHUNK_ROWS, chunk_bytes: Batch::DEFAULT_CHUNK_BYTES, namespace: nil, **opts)
|
|
273
|
+
ops = docs.lazy.map { |d| { "upsert" => Documents.document(d) } }
|
|
274
|
+
Batch.run(ops, chunk_rows: chunk_rows, chunk_bytes: chunk_bytes) do |chunk, key|
|
|
275
|
+
write(collection, chunk, namespace: namespace, **opts, idempotency_key: key)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# +select+ is a projection Hash (`{source: {include: […]}}`);
|
|
280
|
+
# +consistency+ is `:strong`, `:eventual` or a token.
|
|
281
|
+
def get(collection, ids, select: nil, consistency: nil, namespace: nil, **opts)
|
|
282
|
+
rpc(CALLS[%w[documents get]], ns(namespace).merge(Builders.compact({
|
|
283
|
+
"collection" => collection.to_s, "ids" => Array(ids).map { |id| Values.document_id(id) },
|
|
284
|
+
"select" => select, "consistency" => Search.consistency_json(consistency),
|
|
285
|
+
})), call_opts(opts))
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# One page. +where+ is a typed `Filter`; +filter+ is the server's native
|
|
289
|
+
# filter JSON.
|
|
290
|
+
def scroll(collection, where: nil, filter: nil, after: nil, limit: nil, select: nil, consistency: nil, namespace: nil, **opts)
|
|
291
|
+
rpc(CALLS[%w[documents scroll]], filtered(CALLS[%w[documents scroll]], ns(namespace).merge(Builders.compact({
|
|
292
|
+
"collection" => collection.to_s, "after" => after && Values.document_id(after),
|
|
293
|
+
"limit" => limit, "select" => select, "consistency" => Search.consistency_json(consistency),
|
|
294
|
+
})), where, filter), call_opts(opts))
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Every document, page after page, until the server stops answering a
|
|
298
|
+
# `next` cursor. An Enumerator.
|
|
299
|
+
def scroll_all(collection, **kw)
|
|
300
|
+
return to_enum(:scroll_all, collection, **kw) unless block_given?
|
|
301
|
+
|
|
302
|
+
after = kw.delete(:after)
|
|
303
|
+
loop do
|
|
304
|
+
page = scroll(collection, **kw, after: after)
|
|
305
|
+
page.documents.each { |doc| yield doc }
|
|
306
|
+
break if page.next.nil?
|
|
307
|
+
|
|
308
|
+
after = Values.to_ruby(page.next)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def count(collection, where: nil, filter: nil, consistency: nil, namespace: nil, **opts)
|
|
313
|
+
rpc(CALLS[%w[documents count]], filtered(CALLS[%w[documents count]], ns(namespace).merge(Builders.compact({
|
|
314
|
+
"collection" => collection.to_s, "consistency" => Search.consistency_json(consistency),
|
|
315
|
+
})), where, filter), call_opts(opts))
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def delete_by_filter(collection, where: nil, filter: nil, max_rows: nil, allow_partial: false, cursor: nil, namespace: nil, **opts)
|
|
319
|
+
rpc(CALLS[%w[documents delete_by_filter]], filtered(CALLS[%w[documents delete_by_filter]], ns(namespace).merge(Builders.compact({
|
|
320
|
+
"collection" => collection.to_s, "maxRows" => max_rows, "allowPartial" => allow_partial, "cursor" => cursor,
|
|
321
|
+
})), where, filter), call_opts(opts))
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def patch_by_filter(collection, patch, where: nil, filter: nil, max_rows: nil, allow_partial: false, cursor: nil, namespace: nil, **opts)
|
|
325
|
+
rpc(CALLS[%w[documents patch_by_filter]], filtered(CALLS[%w[documents patch_by_filter]], ns(namespace).merge(Builders.compact({
|
|
326
|
+
"collection" => collection.to_s, "patch" => patch, "maxRows" => max_rows, "allowPartial" => allow_partial, "cursor" => cursor,
|
|
327
|
+
})), where, filter), call_opts(opts))
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# One document as a `WriteDocument`'s JSON.
|
|
331
|
+
def self.document(doc)
|
|
332
|
+
doc = doc.transform_keys(&:to_s)
|
|
333
|
+
Builders.compact({
|
|
334
|
+
"id" => Values.document_id(doc.fetch("id")),
|
|
335
|
+
"source" => doc["source"] || {},
|
|
336
|
+
"vectors" => doc["vectors"],
|
|
337
|
+
"sparseVectors" => doc["sparse_vectors"] || doc["sparseVectors"],
|
|
338
|
+
})
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
private
|
|
342
|
+
|
|
343
|
+
# The typed `where` goes in the request's `where` field (SDKP-3), which
|
|
344
|
+
# needs protos that declare it; the native `filter` JSON goes as is.
|
|
345
|
+
def filtered(path, request, where, filter)
|
|
346
|
+
unless where.nil?
|
|
347
|
+
binding = Descriptors.binding_for_rpc(path)
|
|
348
|
+
if binding.request.lookup("where").nil?
|
|
349
|
+
raise InvalidArgumentError.new(
|
|
350
|
+
"#{path} takes no typed where in this SDK's protos; pass filter: (the native filter JSON)",
|
|
351
|
+
code: :invalid_argument,
|
|
352
|
+
rpc: path,
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
request = request.merge("where" => Builders.json(where))
|
|
356
|
+
end
|
|
357
|
+
request = request.merge("filter" => filter) unless filter.nil?
|
|
358
|
+
request
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# `search`: one search over one collection: text, vector, sparse or
|
|
363
|
+
# hybrid, with filters, fusion, highlights, `select` and pagination.
|
|
364
|
+
class SearchModule < Base
|
|
365
|
+
# +request+ is a `Search` builder or the `SearchRequest`'s JSON, checked
|
|
366
|
+
# (`Search#build`) and sent as built.
|
|
367
|
+
def query(request, **opts)
|
|
368
|
+
rpc(CALLS[%w[search query]], Search.wire(request), call_opts(opts))
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# A text search: a match on one field, a multi-match on several, or a
|
|
372
|
+
# query string.
|
|
373
|
+
def text(collection, text, fields: nil, field: nil, filter: nil, limit: nil, namespace: nil, **opts)
|
|
374
|
+
s = Search.on(collection, namespace: namespace).text(text, fields: fields, field: field)
|
|
375
|
+
s.filter(filter) if filter
|
|
376
|
+
s.limit(limit) if limit
|
|
377
|
+
query(s, **opts)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Text plus a vector on +field+, fused (`Fusion.rrf` by default).
|
|
381
|
+
def hybrid(collection, text, vector, field:, fields: nil, fusion: nil, k: nil, filter: nil, limit: nil, namespace: nil, **opts)
|
|
382
|
+
s = Search.on(collection, namespace: namespace)
|
|
383
|
+
.text(text, fields: fields, k: k)
|
|
384
|
+
.vector(field, vector, k: k)
|
|
385
|
+
.fuse(fusion || Fusion.rrf)
|
|
386
|
+
s.filter(filter) if filter
|
|
387
|
+
s.limit(limit) if limit
|
|
388
|
+
query(s, **opts)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Every hit, page after page through `search_after`, until a page has
|
|
392
|
+
# fewer than `limit` hits. The request's sort must end with the `pk`
|
|
393
|
+
# tiebreaker, or it is refused here, before any network call.
|
|
394
|
+
def query_all(request, **opts)
|
|
395
|
+
return to_enum(:query_all, request, **opts) unless block_given?
|
|
396
|
+
|
|
397
|
+
page_request = Builders.json(request)
|
|
398
|
+
unless Search.tiebreak?(page_request)
|
|
399
|
+
raise InvalidArgumentError.new(
|
|
400
|
+
"query_all needs a sort that ends with the primary key, such as .sort(:score, :pk)",
|
|
401
|
+
code: :invalid_argument,
|
|
402
|
+
reason: :search_after_needs_tiebreak,
|
|
403
|
+
metadata: { "field" => "sort" },
|
|
404
|
+
)
|
|
405
|
+
end
|
|
406
|
+
limit = page_request["limit"] || Search::DEFAULT_K
|
|
407
|
+
loop do
|
|
408
|
+
page = query(page_request, **opts)
|
|
409
|
+
page.hits.each { |hit| yield hit }
|
|
410
|
+
break if page.hits.size < limit || page.hits.empty?
|
|
411
|
+
|
|
412
|
+
page_request = page_request.merge("searchAfter" => Values.to_ruby(page.hits.last.sort_values))
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# `vector`: the same RPC as `search.query`, with the vector shape as its
|
|
419
|
+
# required arguments (Q613).
|
|
420
|
+
class VectorModule < Base
|
|
421
|
+
def search(collection, vector, field:, limit: nil, k: nil, filter: nil, namespace: nil, **opts)
|
|
422
|
+
s = Search.on(collection, namespace: namespace).vector(field, vector, k: k)
|
|
423
|
+
s.filter(filter) if filter
|
|
424
|
+
s.limit(limit) if limit
|
|
425
|
+
rpc(CALLS[%w[vector search]], Search.wire(s), call_opts(opts))
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# `graph`: stored property graphs, queried with GQL. Experimental: the
|
|
430
|
+
# package is `unstable`.
|
|
431
|
+
class Graph < Base
|
|
432
|
+
LANGUAGES = {
|
|
433
|
+
gql: "QUERY_LANGUAGE_GQL", cypher: "QUERY_LANGUAGE_CYPHER", sql_pgq: "QUERY_LANGUAGE_SQL_PGQ",
|
|
434
|
+
}.freeze
|
|
435
|
+
|
|
436
|
+
def unstable?
|
|
437
|
+
true
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def create(name, mapping: nil, mode: nil, limits: nil, namespace: nil, **opts)
|
|
441
|
+
rpc(CALLS[%w[graph create]], ns(namespace).merge(Builders.compact({
|
|
442
|
+
"name" => name.to_s, "mapping" => mapping, "limits" => limits,
|
|
443
|
+
"mode" => mode && "GRAPH_MODE_#{mode.to_s.upcase}",
|
|
444
|
+
})), call_opts(opts))
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def get(name, namespace: nil, **opts)
|
|
448
|
+
rpc(CALLS[%w[graph get]], ns(namespace).merge("name" => name.to_s), call_opts(opts))
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def list(page_size: nil, page_token: nil, namespace: nil, **opts)
|
|
452
|
+
rpc(CALLS[%w[graph list]], ns(namespace).merge(Builders.compact({ "pageSize" => page_size, "pageToken" => page_token })), call_opts(opts))
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def list_all(page_size: nil, namespace: nil, **opts)
|
|
456
|
+
paged(CALLS[%w[graph list]], ns(namespace).merge(Builders.compact({ "pageSize" => page_size })), "graphs", call_opts(opts))
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# +graph+ is the `Graph` JSON (with `namespace` and `name`), and
|
|
460
|
+
# +update_mask+ the paths it changes.
|
|
461
|
+
def update(graph, update_mask:, expected_version: nil, **opts)
|
|
462
|
+
graph = graph.transform_keys(&:to_s)
|
|
463
|
+
graph["namespace"] ||= @client.namespace
|
|
464
|
+
rpc(CALLS[%w[graph update]], Builders.compact({
|
|
465
|
+
"graph" => graph, "updateMask" => Array(update_mask).join(","),
|
|
466
|
+
"expectedVersion" => expected_version&.to_s,
|
|
467
|
+
}), call_opts(opts))
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Answers an `Operation`.
|
|
471
|
+
def delete(name, namespace: nil, **opts)
|
|
472
|
+
rpc(CALLS[%w[graph delete]], ns(namespace).merge("name" => name.to_s), call_opts(opts))
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def schema(name, namespace: nil, **opts)
|
|
476
|
+
rpc(CALLS[%w[graph schema]], ns(namespace).merge("name" => name.to_s), call_opts(opts))
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# One statement (GQL by default). +parameters+ are Ruby values.
|
|
480
|
+
def query(graph, statement, parameters: {}, read_only: false, language: :gql, consistency: nil,
|
|
481
|
+
max_rows: nil, namespace: nil, **opts)
|
|
482
|
+
rpc(CALLS[%w[graph query]], execute_request(graph, statement, parameters, read_only, language, consistency, max_rows, namespace), call_opts(opts))
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# +statements+: `[{statement:, parameters:}]`, run in one batch.
|
|
486
|
+
def query_batch(graph, statements, atomic: true, language: :gql, namespace: nil, **opts)
|
|
487
|
+
list = statements.map do |s|
|
|
488
|
+
s = s.transform_keys(&:to_s)
|
|
489
|
+
{ "statement" => s.fetch("statement"), "parameters" => params(s["parameters"] || {}) }
|
|
490
|
+
end
|
|
491
|
+
rpc(CALLS[%w[graph query_batch]], ns(namespace).merge(
|
|
492
|
+
"graph" => graph.to_s, "statements" => list, "atomic" => atomic, "language" => language_name(language),
|
|
493
|
+
), call_opts(opts))
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# A server stream of `ResultChunk`s (gRPC-only on Connect-unary transports).
|
|
497
|
+
def query_stream(graph, statement, parameters: {}, read_only: true, language: :gql, chunk_rows: nil, namespace: nil, **opts)
|
|
498
|
+
inner = execute_request(graph, statement, parameters, read_only, language, nil, nil, namespace || @client.namespace)
|
|
499
|
+
rpc(CALLS[%w[graph query_stream]], Builders.compact({ "request" => inner, "chunkRows" => chunk_rows }), call_opts(opts))
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def explain(graph, statement, parameters: {}, profile: false, language: :gql, namespace: nil, **opts)
|
|
503
|
+
rpc(CALLS[%w[graph explain]], ns(namespace).merge(
|
|
504
|
+
"graph" => graph.to_s, "statement" => statement.to_s, "parameters" => params(parameters),
|
|
505
|
+
"profile" => profile, "language" => language_name(language),
|
|
506
|
+
), call_opts(opts))
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# An `ExecuteResponse`'s rows as Hashes, column name to Ruby value.
|
|
510
|
+
def self.records(response)
|
|
511
|
+
rows = response.rows
|
|
512
|
+
return [] if rows.nil?
|
|
513
|
+
|
|
514
|
+
columns = rows.columns.to_a
|
|
515
|
+
rows.rows.map { |row| columns.zip(row.values.map { |v| Values.from_graph_value(v) }).to_h }
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
private
|
|
519
|
+
|
|
520
|
+
def execute_request(graph, statement, parameters, read_only, language, consistency, max_rows, namespace)
|
|
521
|
+
ns(namespace).merge(Builders.compact({
|
|
522
|
+
"graph" => graph.to_s, "statement" => statement.to_s, "language" => language_name(language),
|
|
523
|
+
"parameters" => params(parameters), "readOnly" => read_only,
|
|
524
|
+
"consistency" => graph_consistency(consistency), "maxRows" => max_rows,
|
|
525
|
+
}))
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def params(parameters)
|
|
529
|
+
parameters.to_h { |k, v| [k.to_s, Values.graph_value(v)] }
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def language_name(language)
|
|
533
|
+
LANGUAGES.fetch(language.to_sym) { language.to_s }
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def graph_consistency(level)
|
|
537
|
+
case level
|
|
538
|
+
when nil then nil
|
|
539
|
+
when :strong then { "strong" => {} }
|
|
540
|
+
when :eventual then { "eventual" => {} }
|
|
541
|
+
else { "atLeast" => level.to_s }
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
module App
|
|
548
|
+
# `sql`: one read-only statement over a namespace's collections, with
|
|
549
|
+
# `$1…$n` parameters. SQL reads see the session's collection writes.
|
|
550
|
+
class Sql < Base
|
|
551
|
+
# The answer's `rows` are lists in `columns` order; see `.records`.
|
|
552
|
+
def query(sql, params: [], max_rows: nil, consistency: nil, namespace: nil, **opts)
|
|
553
|
+
rpc(CALLS[%w[sql query]], request(sql, params, max_rows, consistency, namespace), call_opts(opts))
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# The same statement, answered as Arrow IPC bytes.
|
|
557
|
+
def query_arrow(sql, params: [], max_rows: nil, consistency: nil, namespace: nil, **opts)
|
|
558
|
+
rpc(CALLS[%w[sql query_arrow]], request(sql, params, max_rows, consistency, namespace), call_opts(opts)).arrow_ipc
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# A `QueryResponse`'s rows as Hashes, column name to value.
|
|
562
|
+
def self.records(response)
|
|
563
|
+
names = response.columns.map(&:name)
|
|
564
|
+
response.rows.map { |row| names.zip(Values.to_ruby(row)).to_h }
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
private
|
|
568
|
+
|
|
569
|
+
def request(sql, params, max_rows, consistency, namespace)
|
|
570
|
+
ns(namespace).merge(Builders.compact({
|
|
571
|
+
"sql" => sql.to_s, "params" => params.empty? ? nil : params,
|
|
572
|
+
"maxRows" => max_rows, "consistency" => Search.consistency_json(consistency),
|
|
573
|
+
}))
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# One fetched record, decoded: `offset`, `key` and `value` (binary
|
|
578
|
+
# Strings, or nil), `headers` and `timestamp_ms`.
|
|
579
|
+
StreamRecord = Struct.new(:offset, :key, :value, :headers, :timestamp_ms, keyword_init: true) do
|
|
580
|
+
def json
|
|
581
|
+
value && JSON.parse(value)
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# `streams`: a namespace's logs. Records are bytes on the wire; the module
|
|
586
|
+
# takes Strings (bytes as they are) or other values (sent as JSON) and
|
|
587
|
+
# answers bytes.
|
|
588
|
+
class Streams < Base
|
|
589
|
+
def create(name, partitions: 1, max_age_ms: nil, max_bytes: nil, namespace: nil, **opts)
|
|
590
|
+
retention = Builders.compact({ "maxAgeMs" => max_age_ms&.to_s, "maxBytes" => max_bytes&.to_s })
|
|
591
|
+
rpc(CALLS[%w[streams create]], ns(namespace).merge(Builders.compact({
|
|
592
|
+
"name" => name.to_s, "partitions" => partitions.zero? ? nil : partitions,
|
|
593
|
+
"retention" => retention.empty? ? nil : retention,
|
|
594
|
+
})), call_opts(opts))
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def get(stream, namespace: nil, **opts)
|
|
598
|
+
rpc(CALLS[%w[streams get]], ns(namespace).merge("stream" => stream.to_s), call_opts(opts))
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def list(page_size: nil, page_token: nil, namespace: nil, **opts)
|
|
602
|
+
rpc(CALLS[%w[streams list]], ns(namespace).merge(Builders.compact({ "pageSize" => page_size, "pageToken" => page_token })), call_opts(opts))
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def list_all(page_size: nil, namespace: nil, **opts)
|
|
606
|
+
paged(CALLS[%w[streams list]], ns(namespace).merge(Builders.compact({ "pageSize" => page_size })), "streams", call_opts(opts))
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Appends `records` to one partition. A record is a value, or
|
|
610
|
+
# `{key:, value:, headers: {name => value}}` with an optional key
|
|
611
|
+
# (SDKP-25). With `producer_id` and `producer_seq` a repeat is
|
|
612
|
+
# deduplicated (`duplicate`).
|
|
613
|
+
def produce(stream, records, partition: 0, producer_id: nil, producer_seq: nil, namespace: nil, **opts)
|
|
614
|
+
rpc(CALLS[%w[streams produce]], ns(namespace).merge(Builders.compact({
|
|
615
|
+
"stream" => stream.to_s, "partition" => partition.zero? ? nil : partition,
|
|
616
|
+
"records" => records.map { |r| Streams.record(r) },
|
|
617
|
+
"producerId" => producer_id, "producerSeq" => producer_seq&.to_s,
|
|
618
|
+
})), call_opts(opts))
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# Records at and after `offset`; `.records` decodes the answer.
|
|
622
|
+
def fetch(stream, partition: 0, offset: 0, max_records: nil, max_bytes: nil, max_wait_ms: nil, namespace: nil, **opts)
|
|
623
|
+
rpc(CALLS[%w[streams fetch]], ns(namespace).merge(Builders.compact({
|
|
624
|
+
"stream" => stream.to_s, "partition" => partition.zero? ? nil : partition,
|
|
625
|
+
"offset" => offset.zero? ? nil : offset.to_s, "maxBytes" => max_bytes,
|
|
626
|
+
"maxRecords" => max_records, "maxWaitMs" => max_wait_ms,
|
|
627
|
+
})), call_opts(opts))
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# CloudEvents (Hashes) appended as one JSON batch.
|
|
631
|
+
def produce_events(stream, events, partition: nil, namespace: nil, **opts)
|
|
632
|
+
rpc(CALLS[%w[streams produce_events]], ns(namespace).merge(Builders.compact({
|
|
633
|
+
"stream" => stream.to_s, "partition" => partition,
|
|
634
|
+
"jsonBatch" => [JSON.generate(events)].pack("m0"),
|
|
635
|
+
})), call_opts(opts))
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# CloudEvents at and after `offset`, decoded from the answer's JSON batch.
|
|
639
|
+
def fetch_events(stream, partition: 0, offset: 0, max_bytes: nil, max_wait_ms: nil, namespace: nil, **opts)
|
|
640
|
+
answer = rpc(CALLS[%w[streams fetch_events]], ns(namespace).merge(Builders.compact({
|
|
641
|
+
"stream" => stream.to_s, "partition" => partition.zero? ? nil : partition,
|
|
642
|
+
"offset" => offset.zero? ? nil : offset.to_s, "maxBytes" => max_bytes, "maxWaitMs" => max_wait_ms,
|
|
643
|
+
})), call_opts(opts))
|
|
644
|
+
answer.json_batch.empty? ? [] : JSON.parse(answer.json_batch)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# R14: every record from `from_offset` on, as a loop over `fetch`:
|
|
648
|
+
# enumerate it, `cancel` it, resume from `offset`.
|
|
649
|
+
def subscribe(stream, partition: 0, from_offset: 0, max_records: nil, max_wait_ms: nil, namespace: nil, **opts)
|
|
650
|
+
Subscription.new(from_offset: from_offset) do |offset|
|
|
651
|
+
Streams.records(fetch(stream, partition: partition, offset: offset, max_records: max_records,
|
|
652
|
+
max_wait_ms: max_wait_ms, namespace: namespace, **opts))
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# A `FetchResponse`'s records, decoded.
|
|
657
|
+
def self.records(response)
|
|
658
|
+
response.records.map do |r|
|
|
659
|
+
StreamRecord.new(
|
|
660
|
+
offset: r.offset, key: r.key, value: r.value,
|
|
661
|
+
headers: r.headers.to_h { |h| [h.key, h.value] }, timestamp_ms: r.timestamp_ms,
|
|
662
|
+
)
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# One record as a `Record`'s JSON (bytes as base64).
|
|
667
|
+
def self.record(record)
|
|
668
|
+
if record.is_a?(Hash) && (record.key?(:value) || record.key?("value") || record.key?(:key) || record.key?("key"))
|
|
669
|
+
r = record.transform_keys(&:to_s)
|
|
670
|
+
headers = (r["headers"] || {}).map { |k, v| Builders.compact({ "key" => k.to_s, "value" => v.nil? ? nil : b64(v) }) }
|
|
671
|
+
Builders.compact({
|
|
672
|
+
"key" => r["key"].nil? ? nil : b64(r["key"]),
|
|
673
|
+
"value" => r["value"].nil? ? nil : b64(r["value"]),
|
|
674
|
+
"headers" => headers.empty? ? nil : headers,
|
|
675
|
+
})
|
|
676
|
+
else
|
|
677
|
+
{ "value" => b64(record) }
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def self.b64(value)
|
|
682
|
+
[value.is_a?(String) ? value : JSON.generate(value)].pack("m0")
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# `client.collection("notes")`: every document and search call with the
|
|
688
|
+
# collection (and namespace) filled in. It holds no state of its own.
|
|
689
|
+
class CollectionHandle
|
|
690
|
+
attr_reader :name, :namespace
|
|
691
|
+
|
|
692
|
+
def initialize(client, name, namespace: nil)
|
|
693
|
+
@client = client
|
|
694
|
+
@name = name.to_s
|
|
695
|
+
@namespace = namespace
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def upsert(docs, **kw) = @client.documents.upsert(@name, docs, **with_ns(kw))
|
|
699
|
+
def get(ids, **kw) = @client.documents.get(@name, ids, **with_ns(kw))
|
|
700
|
+
def delete(ids, **kw) = @client.documents.delete(@name, ids, **with_ns(kw))
|
|
701
|
+
def patch(id, **kw) = @client.documents.patch(@name, id, **with_ns(kw))
|
|
702
|
+
def write_batch(docs, **kw) = @client.documents.write_batch(@name, docs, **with_ns(kw))
|
|
703
|
+
def count(**kw) = @client.documents.count(@name, **with_ns(kw))
|
|
704
|
+
def scroll_all(**kw, &block) = @client.documents.scroll_all(@name, **with_ns(kw), &block)
|
|
705
|
+
def vector_search(vector, **kw) = @client.vector.search(@name, vector, **with_ns(kw))
|
|
706
|
+
def drop(**kw) = @client.collections.drop(@name, **with_ns(kw))
|
|
707
|
+
def info(**kw) = @client.collections.get(@name, **with_ns(kw))
|
|
708
|
+
|
|
709
|
+
# +builder+ is a `Search` (its collection is set to this one), or a block
|
|
710
|
+
# that receives a fresh `Search.on(name)`.
|
|
711
|
+
def search(builder = nil, **opts)
|
|
712
|
+
builder ||= Search.on(@name)
|
|
713
|
+
builder = builder.dup
|
|
714
|
+
builder.collection(@name)
|
|
715
|
+
builder.namespace(@namespace) if @namespace && builder.namespace_name.nil?
|
|
716
|
+
yield builder if block_given?
|
|
717
|
+
@client.search.query(builder, **opts)
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
private
|
|
721
|
+
|
|
722
|
+
def with_ns(kw)
|
|
723
|
+
@namespace.nil? || kw.key?(:namespace) ? kw : kw.merge(namespace: @namespace)
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
end
|