parse-stack-next 5.3.0 → 5.4.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 +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +461 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +12 -4
- data/README.md +160 -3
- data/Rakefile +52 -3
- data/docs/atlas_vector_search_guide.md +86 -2
- data/docs/client_sdk_guide.md +5 -0
- data/docs/mcp_guide.md +59 -4
- data/docs/mongodb_direct_guide.md +93 -1
- data/docs/usage_guide.md +11 -1
- data/docs/webhooks_guide.md +418 -0
- data/examples/README.md +46 -0
- data/examples/basic_client.rb +93 -0
- data/examples/basic_server.rb +109 -0
- data/examples/live_query_listener.rb +98 -0
- data/examples/rag_chatbot.rb +221 -0
- data/examples/webhook_server.rb +111 -0
- data/lib/parse/agent/mcp_rack_app.rb +285 -62
- data/lib/parse/agent/tools.rb +45 -5
- data/lib/parse/api/aggregate.rb +7 -1
- data/lib/parse/api/cloud_functions.rb +12 -4
- data/lib/parse/api/hooks.rb +46 -9
- data/lib/parse/api/objects.rb +16 -2
- data/lib/parse/api/path_segment.rb +33 -0
- data/lib/parse/api/server.rb +94 -0
- data/lib/parse/api/users.rb +58 -2
- data/lib/parse/atlas_search.rb +7 -7
- data/lib/parse/client/body_builder.rb +5 -0
- data/lib/parse/client/protocol.rb +4 -0
- data/lib/parse/client.rb +55 -2
- data/lib/parse/embeddings/spend_cap.rb +255 -0
- data/lib/parse/embeddings.rb +1 -0
- data/lib/parse/live_query/client.rb +3 -1
- data/lib/parse/live_query/subscription.rb +32 -5
- data/lib/parse/model/acl.rb +4 -2
- data/lib/parse/model/classes/audience.rb +52 -4
- data/lib/parse/model/classes/user.rb +180 -3
- data/lib/parse/model/core/embed_managed.rb +113 -0
- data/lib/parse/model/core/querying.rb +3 -1
- data/lib/parse/model/core/vector_searchable.rb +161 -0
- data/lib/parse/model/object.rb +28 -5
- data/lib/parse/mongodb.rb +7 -1
- data/lib/parse/pipeline_security.rb +5 -3
- data/lib/parse/query/constraints.rb +29 -0
- data/lib/parse/query.rb +265 -27
- data/lib/parse/retrieval/agent_tool.rb +49 -0
- data/lib/parse/retrieval/reranker/cohere.rb +218 -0
- data/lib/parse/retrieval/reranker.rb +157 -0
- data/lib/parse/retrieval/retriever.rb +110 -23
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +17 -0
- data/lib/parse/two_factor_auth/user_extension.rb +123 -31
- data/lib/parse/vector_search/hybrid.rb +578 -0
- data/lib/parse/webhooks/payload.rb +252 -7
- data/lib/parse/webhooks/trigger_audit.rb +502 -0
- data/lib/parse/webhooks.rb +215 -3
- data/scripts/docker/Dockerfile.parse +5 -1
- data/scripts/docker/docker-compose.test.yml +31 -0
- data/scripts/docker/docker-compose.verifyemail.yml +4 -0
- data/scripts/docker/preflight.sh +76 -0
- data/scripts/start-parse.sh +52 -4
- metadata +15 -1
|
@@ -128,6 +128,39 @@ module Parse
|
|
|
128
128
|
base.extend(ClassMethods)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
# Recompute this record's managed embedding(s) in-place, NOW,
|
|
132
|
+
# without a save. Runs the same digest-tracked recompute the
|
|
133
|
+
# `before_save` callback runs: a provider call happens only when the
|
|
134
|
+
# source text/URL changed since the last embed (digest miss). Useful
|
|
135
|
+
# to populate the vector before inspecting it, or to force a refresh
|
|
136
|
+
# in a console.
|
|
137
|
+
#
|
|
138
|
+
# @param field [Symbol, nil] limit to one embed target; nil
|
|
139
|
+
# recomputes every declared directive.
|
|
140
|
+
# @return [self]
|
|
141
|
+
# @raise [ArgumentError] when `field:` names no embed target, or the
|
|
142
|
+
# class declares no `embed` directives.
|
|
143
|
+
def compute_embedding!(field: nil)
|
|
144
|
+
directives = self.class.embed_directives
|
|
145
|
+
if directives.empty?
|
|
146
|
+
raise ArgumentError, "#{self.class}#compute_embedding!: no `embed` directives declared."
|
|
147
|
+
end
|
|
148
|
+
selected =
|
|
149
|
+
if field
|
|
150
|
+
d = directives[field.to_sym]
|
|
151
|
+
unless d
|
|
152
|
+
raise ArgumentError,
|
|
153
|
+
"#{self.class}#compute_embedding!: :#{field} is not an embed target " \
|
|
154
|
+
"(have #{directives.keys.inspect})."
|
|
155
|
+
end
|
|
156
|
+
[d]
|
|
157
|
+
else
|
|
158
|
+
directives.values
|
|
159
|
+
end
|
|
160
|
+
selected.each { |directive| Parse::Core::EmbedManaged.recompute_embedding!(self, directive) }
|
|
161
|
+
self
|
|
162
|
+
end
|
|
163
|
+
|
|
131
164
|
module ClassMethods
|
|
132
165
|
# Per-class registry of {EmbedDirective}s keyed by target vector
|
|
133
166
|
# property symbol. Read by tests and tooling; written only by
|
|
@@ -300,6 +333,86 @@ module Parse
|
|
|
300
333
|
into
|
|
301
334
|
end
|
|
302
335
|
|
|
336
|
+
# Backfill embeddings for records whose managed vector field is
|
|
337
|
+
# still null — the bulk counterpart to the per-save embed path.
|
|
338
|
+
# Walks the class with objectId-cursor pagination (robust to the
|
|
339
|
+
# result set shrinking as records are embedded; terminates even
|
|
340
|
+
# when a record has no source text and stays null), saving each
|
|
341
|
+
# pending record so its `before_save` embed callback runs.
|
|
342
|
+
#
|
|
343
|
+
# Intended as an admin / maintenance operation: it reads and
|
|
344
|
+
# writes through the default client, so run it with a master-key
|
|
345
|
+
# client (or pass `save_opts:` carrying a `session_token:` that can
|
|
346
|
+
# write every row).
|
|
347
|
+
#
|
|
348
|
+
# @param field [Symbol, nil] limit the backfill to one embed
|
|
349
|
+
# target; nil processes every declared directive.
|
|
350
|
+
# @param batch_size [Integer] rows fetched per round (default 100).
|
|
351
|
+
# @param limit [Integer, nil] stop after embedding at most this
|
|
352
|
+
# many records across all directives; nil = no cap.
|
|
353
|
+
# @param where [Hash, nil] extra query constraints AND-ed with the
|
|
354
|
+
# null-target filter (e.g. `{ published: true }`).
|
|
355
|
+
# @param save_opts [Hash] options forwarded to each `record.save`
|
|
356
|
+
# (e.g. `session_token:`).
|
|
357
|
+
# @return [Integer] number of records saved (embedded).
|
|
358
|
+
# @raise [ArgumentError] when `field:` names no embed target, or
|
|
359
|
+
# the class declares no `embed` directives.
|
|
360
|
+
def embed_pending!(field: nil, batch_size: 100, limit: nil, where: nil, save_opts: {})
|
|
361
|
+
bs = Integer(batch_size)
|
|
362
|
+
raise ArgumentError, "#{self}.embed_pending!: batch_size must be positive." if bs <= 0
|
|
363
|
+
directives = resolve_embed_directives_for_backfill(field)
|
|
364
|
+
|
|
365
|
+
processed = 0
|
|
366
|
+
directives.each do |directive|
|
|
367
|
+
remaining = limit ? (limit - processed) : nil
|
|
368
|
+
break if remaining && remaining <= 0
|
|
369
|
+
processed += backfill_embed_directive!(directive, bs, where, remaining, save_opts)
|
|
370
|
+
end
|
|
371
|
+
processed
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# @!visibility private
|
|
375
|
+
def resolve_embed_directives_for_backfill(field)
|
|
376
|
+
if field
|
|
377
|
+
d = embed_directives[field.to_sym]
|
|
378
|
+
unless d
|
|
379
|
+
raise ArgumentError,
|
|
380
|
+
"#{self}.embed_pending!: :#{field} is not an embed target " \
|
|
381
|
+
"(have #{embed_directives.keys.inspect})."
|
|
382
|
+
end
|
|
383
|
+
[d]
|
|
384
|
+
else
|
|
385
|
+
ds = embed_directives.values
|
|
386
|
+
raise ArgumentError, "#{self}.embed_pending!: no `embed` directives declared." if ds.empty?
|
|
387
|
+
ds
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# @!visibility private
|
|
392
|
+
# objectId-cursor walk over rows where `directive.into` is null.
|
|
393
|
+
def backfill_embed_directive!(directive, batch_size, where, remaining, save_opts)
|
|
394
|
+
count = 0
|
|
395
|
+
cursor = nil
|
|
396
|
+
loop do
|
|
397
|
+
q = query(directive.into.null => true)
|
|
398
|
+
q = q.where(where) if where.is_a?(Hash) && !where.empty?
|
|
399
|
+
q = q.where(:objectId.gt => cursor) if cursor
|
|
400
|
+
q.order(:objectId.asc)
|
|
401
|
+
q.limit(batch_size)
|
|
402
|
+
batch = q.results
|
|
403
|
+
break if batch.nil? || batch.empty?
|
|
404
|
+
|
|
405
|
+
batch.each do |record|
|
|
406
|
+
cursor = record.id
|
|
407
|
+
record.save(**save_opts)
|
|
408
|
+
count += 1
|
|
409
|
+
return count if remaining && count >= remaining
|
|
410
|
+
end
|
|
411
|
+
break if batch.length < batch_size
|
|
412
|
+
end
|
|
413
|
+
count
|
|
414
|
+
end
|
|
415
|
+
|
|
303
416
|
# @!visibility private
|
|
304
417
|
# Prepend a module that intercepts the public `<into>=` setter
|
|
305
418
|
# and raises {ProtectedFieldError} unless the current thread has
|
|
@@ -554,7 +554,7 @@ module Parse
|
|
|
554
554
|
# @return [Parse::LiveQuery::Subscription] the subscription object
|
|
555
555
|
# @see Parse::LiveQuery::Subscription
|
|
556
556
|
# @see Parse::Query#subscribe
|
|
557
|
-
def subscribe(where: {}, fields: nil, session_token: nil, client: nil,
|
|
557
|
+
def subscribe(where: {}, fields: nil, keys: nil, watch: nil, session_token: nil, client: nil,
|
|
558
558
|
use_master_key: false, &block)
|
|
559
559
|
# Fall through to the ambient set by `Parse.with_session` / `Parse.login`
|
|
560
560
|
# so a caller wrapping a region with `with_session(user) { Klass.subscribe ... }`
|
|
@@ -565,6 +565,8 @@ module Parse
|
|
|
565
565
|
end
|
|
566
566
|
query(where).subscribe(
|
|
567
567
|
fields: fields,
|
|
568
|
+
keys: keys,
|
|
569
|
+
watch: watch,
|
|
568
570
|
session_token: session_token,
|
|
569
571
|
client: client,
|
|
570
572
|
use_master_key: use_master_key,
|
|
@@ -63,6 +63,54 @@ module Parse
|
|
|
63
63
|
# binding it to a provider at the property level.
|
|
64
64
|
class EmbedderNotConfigured < ArgumentError; end
|
|
65
65
|
|
|
66
|
+
# Accepted {#vector_visibility} modes.
|
|
67
|
+
VECTOR_VISIBILITY_MODES = %i[owner_only public].freeze
|
|
68
|
+
|
|
69
|
+
# Class-level default for whether this class's `:vector` properties
|
|
70
|
+
# are included in `as_json` serialization.
|
|
71
|
+
#
|
|
72
|
+
# * `:owner_only` (default) — vectors are OMITTED from `as_json`
|
|
73
|
+
# unless the caller passes `include_vectors: true`. Embeddings are
|
|
74
|
+
# large and leak ML signal; the safe default keeps them off the
|
|
75
|
+
# wire and out of API responses. Row-level read access is still
|
|
76
|
+
# governed by ACL as usual — this controls serialization exposure,
|
|
77
|
+
# not row authorization.
|
|
78
|
+
# * `:public` — vectors are INCLUDED in `as_json` by default (a
|
|
79
|
+
# caller can still suppress per-call with `include_vectors: false`).
|
|
80
|
+
#
|
|
81
|
+
# class Article < Parse::Object
|
|
82
|
+
# vector_visibility :public # expose embeddings in as_json
|
|
83
|
+
# property :embedding, :vector, dimensions: 1536, provider: :openai
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# Read the effective mode by calling with no argument; it inherits
|
|
87
|
+
# from the superclass when unset on the subclass.
|
|
88
|
+
#
|
|
89
|
+
# @param mode [Symbol, nil] one of {VECTOR_VISIBILITY_MODES}, or nil
|
|
90
|
+
# to read the current effective mode.
|
|
91
|
+
# @return [Symbol] the effective mode (when reading) or the mode set.
|
|
92
|
+
# @raise [ArgumentError] on an unknown mode.
|
|
93
|
+
def vector_visibility(mode = nil)
|
|
94
|
+
if mode.nil?
|
|
95
|
+
return @vector_visibility if defined?(@vector_visibility) && @vector_visibility
|
|
96
|
+
return superclass.vector_visibility if superclass.respond_to?(:vector_visibility)
|
|
97
|
+
return :owner_only
|
|
98
|
+
end
|
|
99
|
+
m = mode.to_sym
|
|
100
|
+
unless VECTOR_VISIBILITY_MODES.include?(m)
|
|
101
|
+
raise ArgumentError,
|
|
102
|
+
"#{self}.vector_visibility: mode must be one of " \
|
|
103
|
+
"#{VECTOR_VISIBILITY_MODES.inspect} (got #{mode.inspect})."
|
|
104
|
+
end
|
|
105
|
+
@vector_visibility = m
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @return [Boolean] whether `:vector` fields are serialized into
|
|
109
|
+
# `as_json` by default for this class (true only for `:public`).
|
|
110
|
+
def vectors_public_by_default?
|
|
111
|
+
vector_visibility == :public
|
|
112
|
+
end
|
|
113
|
+
|
|
66
114
|
# Find documents whose declared `:vector` property is closest to
|
|
67
115
|
# `vector:` under the Atlas vectorSearch index's similarity
|
|
68
116
|
# function.
|
|
@@ -169,6 +217,97 @@ module Parse
|
|
|
169
217
|
build_vector_hits(raw_hits)
|
|
170
218
|
end
|
|
171
219
|
|
|
220
|
+
# Hybrid (lexical + vector) search with reciprocal-rank fusion.
|
|
221
|
+
#
|
|
222
|
+
# Runs a lexical Atlas Search branch and a `$vectorSearch` branch
|
|
223
|
+
# independently, then fuses their ranked results client-side via RRF
|
|
224
|
+
# (or, on Atlas 8.0+, server-side via native `$rankFusion` when
|
|
225
|
+
# detected). Both branches enforce ACL/CLP/protectedFields before
|
|
226
|
+
# fusion — see {Parse::VectorSearch::Hybrid}.
|
|
227
|
+
#
|
|
228
|
+
# @example
|
|
229
|
+
# Song.hybrid_search(
|
|
230
|
+
# text: "love songs about rain",
|
|
231
|
+
# lexical: { index: "song_search", query: "rain love" },
|
|
232
|
+
# vector: { num_candidates: 200 },
|
|
233
|
+
# k: 20,
|
|
234
|
+
# fusion: { k_constant: 60, weights: { lexical: 0.4, vector: 0.6 } },
|
|
235
|
+
# )
|
|
236
|
+
#
|
|
237
|
+
# @param text [String, nil] natural-language query. Embedded (via
|
|
238
|
+
# the resolved `:vector` property's `provider:`) for the vector
|
|
239
|
+
# branch, and used as the lexical query unless `lexical[:query]`
|
|
240
|
+
# overrides it.
|
|
241
|
+
# @param query_vector [Array<Float>, Parse::Vector, nil] pre-computed
|
|
242
|
+
# query embedding (alternative to `text:` for the vector branch).
|
|
243
|
+
# @param lexical [Hash] lexical branch config (`:query`, `:index`,
|
|
244
|
+
# `:fields`, `:filter`, `:fuzzy`). `:query` defaults to `text:`.
|
|
245
|
+
# @param vector [Hash] vector branch config (`:field`, `:index`,
|
|
246
|
+
# `:num_candidates`, `:filter`, `:vector_filter`). `:field`
|
|
247
|
+
# defaults to the class's sole `:vector` property; `:index` is
|
|
248
|
+
# auto-discovered when omitted.
|
|
249
|
+
# @param k [Integer] number of fused hits to return.
|
|
250
|
+
# @param fusion [Hash, nil] `:method` (`:rrf` / `:rrf_client`),
|
|
251
|
+
# `:k_constant`, `:weights` (`{ lexical:, vector: }`).
|
|
252
|
+
# @param raw [Boolean] return fused raw rows instead of built
|
|
253
|
+
# Parse::Object instances.
|
|
254
|
+
# @param scope_opts [Hash] ACL/CLP scope kwargs forwarded to both
|
|
255
|
+
# branches (`session_token:` / `master:` / `acl_user:` /
|
|
256
|
+
# `acl_role:`).
|
|
257
|
+
# @return [Array<Parse::Object>] fused, RRF-ordered; each carries
|
|
258
|
+
# `#hybrid_score` and `#hybrid_ranks` (and `#vector_score` /
|
|
259
|
+
# `#search_score` when the branch contributed). `raw: true`
|
|
260
|
+
# returns the fused Hashes.
|
|
261
|
+
def hybrid_search(text: nil, query_vector: nil, lexical: {}, vector: {},
|
|
262
|
+
k: 20, fusion: nil, raw: false, **scope_opts)
|
|
263
|
+
require_relative "../../vector_search/hybrid"
|
|
264
|
+
lex = (lexical || {}).transform_keys(&:to_sym)
|
|
265
|
+
vec = (vector || {}).transform_keys(&:to_sym)
|
|
266
|
+
|
|
267
|
+
field_sym = resolve_vector_field!(vec[:field])
|
|
268
|
+
declared_dims = vector_properties.dig(field_sym, :dimensions)
|
|
269
|
+
|
|
270
|
+
qv = query_vector || vec[:query_vector]
|
|
271
|
+
qv =
|
|
272
|
+
if qv.nil?
|
|
273
|
+
unless text.is_a?(String) && !text.strip.empty?
|
|
274
|
+
raise ArgumentError,
|
|
275
|
+
"#{self}.hybrid_search: pass `text:` (to embed) or a `query_vector:`."
|
|
276
|
+
end
|
|
277
|
+
embed_query_text!(text, field_sym)
|
|
278
|
+
else
|
|
279
|
+
coerce_query_vector(qv)
|
|
280
|
+
end
|
|
281
|
+
Parse::VectorSearch.validate_query_vector!(qv, dimensions: declared_dims)
|
|
282
|
+
|
|
283
|
+
lexical_query = lex[:query] || text
|
|
284
|
+
unless lexical_query.is_a?(String) && !lexical_query.strip.empty?
|
|
285
|
+
raise ArgumentError,
|
|
286
|
+
"#{self}.hybrid_search: needs a lexical query — pass `text:` or `lexical: { query: }`."
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
vector_index = vec[:index] || resolve_vector_index!(field_sym, nil)
|
|
290
|
+
|
|
291
|
+
fused = Parse::VectorSearch::Hybrid.search(
|
|
292
|
+
parse_class,
|
|
293
|
+
lexical: {
|
|
294
|
+
query: lexical_query, index: lex[:index], fields: lex[:fields],
|
|
295
|
+
filter: lex[:filter], fuzzy: lex[:fuzzy],
|
|
296
|
+
},
|
|
297
|
+
vector: {
|
|
298
|
+
query_vector: qv, field: field_sym, index: vector_index,
|
|
299
|
+
num_candidates: vec[:num_candidates], filter: vec[:filter],
|
|
300
|
+
vector_filter: vec[:vector_filter],
|
|
301
|
+
},
|
|
302
|
+
k: k,
|
|
303
|
+
fusion: fusion,
|
|
304
|
+
**scope_opts,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return fused if raw
|
|
308
|
+
build_hybrid_hits(fused)
|
|
309
|
+
end
|
|
310
|
+
|
|
172
311
|
private
|
|
173
312
|
|
|
174
313
|
def resolve_vector_field!(field)
|
|
@@ -280,6 +419,28 @@ module Parse
|
|
|
280
419
|
obj
|
|
281
420
|
end.compact
|
|
282
421
|
end
|
|
422
|
+
|
|
423
|
+
# Build Parse::Object instances from fused hybrid rows, attaching
|
|
424
|
+
# the fused score / per-branch ranks plus whatever per-branch scores
|
|
425
|
+
# survived the merge (`_vscore`, `_score`).
|
|
426
|
+
def build_hybrid_hits(rows)
|
|
427
|
+
return [] if rows.nil? || rows.empty?
|
|
428
|
+
converted = Parse::MongoDB.convert_documents_to_parse(rows, parse_class)
|
|
429
|
+
converted.each_with_index.map do |doc, idx|
|
|
430
|
+
obj = Parse::Object.build(doc, parse_class)
|
|
431
|
+
next nil unless obj
|
|
432
|
+
src = rows[idx]
|
|
433
|
+
hscore = src["_hybrid_score"] || src[:_hybrid_score]
|
|
434
|
+
hranks = src["_hybrid_ranks"] || src[:_hybrid_ranks]
|
|
435
|
+
vscore = src["_vscore"] || src[:_vscore]
|
|
436
|
+
sscore = src["_score"] || src[:_score]
|
|
437
|
+
obj.instance_variable_set(:@_hybrid_score, hscore) unless hscore.nil?
|
|
438
|
+
obj.instance_variable_set(:@_hybrid_ranks, hranks) unless hranks.nil?
|
|
439
|
+
obj.instance_variable_set(:@_vector_score, vscore) unless vscore.nil?
|
|
440
|
+
obj.instance_variable_set(:@_search_score, sscore) unless sscore.nil?
|
|
441
|
+
obj
|
|
442
|
+
end.compact
|
|
443
|
+
end
|
|
283
444
|
end
|
|
284
445
|
end
|
|
285
446
|
end
|
data/lib/parse/model/object.rb
CHANGED
|
@@ -182,6 +182,14 @@ module Parse
|
|
|
182
182
|
# @return [Hash, nil] Atlas Search highlights blob.
|
|
183
183
|
def search_highlights; @_search_highlights; end
|
|
184
184
|
|
|
185
|
+
# @return [Float, nil] fused reciprocal-rank-fusion score from
|
|
186
|
+
# `Class.hybrid_search`.
|
|
187
|
+
def hybrid_score; @_hybrid_score; end
|
|
188
|
+
|
|
189
|
+
# @return [Hash, nil] per-branch 1-based ranks from
|
|
190
|
+
# `Class.hybrid_search` (`{ lexical:, vector: }`).
|
|
191
|
+
def hybrid_ranks; @_hybrid_ranks; end
|
|
192
|
+
|
|
185
193
|
# @return [Model::TYPE_OBJECT]
|
|
186
194
|
def __type; Parse::Model::TYPE_OBJECT; end
|
|
187
195
|
|
|
@@ -384,7 +392,12 @@ module Parse
|
|
|
384
392
|
end
|
|
385
393
|
|
|
386
394
|
# The set of default ACLs to be applied on newly created instances of this class.
|
|
387
|
-
#
|
|
395
|
+
# The result follows the class's {acl_policy_setting}: the shipped default
|
|
396
|
+
# policy is `:owner_else_private`, whose fallback half is {Parse::ACL.private}
|
|
397
|
+
# (an empty ACL — readable only by the master key until an owner is resolved
|
|
398
|
+
# at save time). Classes that opt into a `:public*` policy, or set
|
|
399
|
+
# {default_acl_private} / {set_default_acl}, get the corresponding permissions
|
|
400
|
+
# instead.
|
|
388
401
|
# @see Parse::ACL.everyone
|
|
389
402
|
# @see Parse::ACL.private
|
|
390
403
|
# @return [Parse::ACL] the current default ACLs for this class.
|
|
@@ -398,8 +411,10 @@ module Parse
|
|
|
398
411
|
end
|
|
399
412
|
|
|
400
413
|
# A method to set default ACLs to be applied for newly created
|
|
401
|
-
# instances of this class.
|
|
402
|
-
#
|
|
414
|
+
# instances of this class. Unless overridden, subclasses inherit the
|
|
415
|
+
# shipped `:owner_else_private` policy (records are private/master-only
|
|
416
|
+
# until an owner is resolved at save time); use this method (or
|
|
417
|
+
# {acl_policy}) to grant broader access.
|
|
403
418
|
# @example
|
|
404
419
|
# class AdminData < Parse::Object
|
|
405
420
|
#
|
|
@@ -1223,8 +1238,16 @@ module Parse
|
|
|
1223
1238
|
# signal to clients, and they round-trip through the dedicated
|
|
1224
1239
|
# embed/find_similar pipelines rather than the standard REST
|
|
1225
1240
|
# save/find. Pass `include_vectors: true` to opt back in (e.g.,
|
|
1226
|
-
# for tests or internal mongo-direct bulk writes).
|
|
1227
|
-
|
|
1241
|
+
# for tests or internal mongo-direct bulk writes). A class may flip
|
|
1242
|
+
# the per-class default with `vector_visibility :public`; an explicit
|
|
1243
|
+
# `include_vectors:` in the call always wins over the class default.
|
|
1244
|
+
include_vectors =
|
|
1245
|
+
if opts.key?(:include_vectors)
|
|
1246
|
+
opts[:include_vectors] == true
|
|
1247
|
+
else
|
|
1248
|
+
self.class.respond_to?(:vectors_public_by_default?) && self.class.vectors_public_by_default?
|
|
1249
|
+
end
|
|
1250
|
+
unless include_vectors
|
|
1228
1251
|
vector_fields = self.class.respond_to?(:fields) ? self.class.fields(:vector).keys.map(&:to_s) : []
|
|
1229
1252
|
if vector_fields.any?
|
|
1230
1253
|
except = Array(opts[:except]).map(&:to_s) | vector_fields
|
data/lib/parse/mongodb.rb
CHANGED
|
@@ -1499,7 +1499,7 @@ module Parse
|
|
|
1499
1499
|
# @raise [Parse::ACLScope::ACLRequired] when neither
|
|
1500
1500
|
# `session_token:` nor `master: true` is supplied and
|
|
1501
1501
|
# {Parse::ACLScope.require_session_token} is enabled.
|
|
1502
|
-
def aggregate(collection_name, pipeline, max_time_ms: nil, rewrite_lookups: nil, allow_internal_fields: false, session_token: nil, master: nil, acl_user: nil, acl_role: nil, read_preference: nil)
|
|
1502
|
+
def aggregate(collection_name, pipeline, max_time_ms: nil, rewrite_lookups: nil, allow_internal_fields: false, session_token: nil, master: nil, acl_user: nil, acl_role: nil, read_preference: nil, hint: nil)
|
|
1503
1503
|
# AS::N envelope. Payload is intentionally metadata-only —
|
|
1504
1504
|
# `stage_count`, `stage_types`, `collection`, `scope`,
|
|
1505
1505
|
# `result_count`, `max_time_ms`, `read_preference`. Pipeline
|
|
@@ -1620,6 +1620,11 @@ module Parse
|
|
|
1620
1620
|
|
|
1621
1621
|
agg_opts = {}
|
|
1622
1622
|
agg_opts[:max_time_ms] = max_time_ms if max_time_ms
|
|
1623
|
+
# Forced index hint (Query#hint). Mirrors Parse Server's REST `hint`
|
|
1624
|
+
# on the mongo-direct path so a bad plan diagnosed with `explain` can
|
|
1625
|
+
# be corrected here too. Accepts an index name (String) or a key
|
|
1626
|
+
# pattern (Hash).
|
|
1627
|
+
agg_opts[:hint] = hint unless hint.nil?
|
|
1623
1628
|
coll = collection(collection_name)
|
|
1624
1629
|
if (mode = normalize_read_preference(read_preference))
|
|
1625
1630
|
coll = coll.with(read: { mode: mode })
|
|
@@ -1838,6 +1843,7 @@ module Parse
|
|
|
1838
1843
|
cursor = cursor.skip(options[:skip]) if options[:skip]
|
|
1839
1844
|
cursor = cursor.sort(options[:sort]) if options[:sort]
|
|
1840
1845
|
cursor = cursor.projection(options[:projection]) if options[:projection]
|
|
1846
|
+
cursor = cursor.hint(options[:hint]) unless options[:hint].nil?
|
|
1841
1847
|
cursor = cursor.max_time_ms(max_time_ms) if max_time_ms
|
|
1842
1848
|
results = cursor.to_a
|
|
1843
1849
|
|
|
@@ -182,13 +182,15 @@ module Parse
|
|
|
182
182
|
# that `Parse::AtlasSearch` calls do not break. `$vectorSearch` is
|
|
183
183
|
# included for `Parse::VectorSearch` — like `$search`, it is a
|
|
184
184
|
# read-only Atlas index stage and must be the FIRST stage of the
|
|
185
|
-
# pipeline (Atlas refuses it otherwise).
|
|
185
|
+
# pipeline (Atlas refuses it otherwise). `$rankFusion` (Atlas 8.0+)
|
|
186
|
+
# is the native server-side reciprocal-rank-fusion stage used by
|
|
187
|
+
# `Parse::VectorSearch::Hybrid` — also a read-only stage-0 operator.
|
|
186
188
|
ALLOWED_STAGES = %w[
|
|
187
189
|
$match $group $sort $project $limit $skip $unwind $lookup
|
|
188
190
|
$count $addFields $set $unset $bucket $bucketAuto $facet
|
|
189
191
|
$sample $sortByCount $replaceRoot $replaceWith $redact
|
|
190
192
|
$graphLookup $unionWith
|
|
191
|
-
$search $searchMeta $listSearchIndexes $vectorSearch
|
|
193
|
+
$search $searchMeta $listSearchIndexes $vectorSearch $rankFusion
|
|
192
194
|
].freeze
|
|
193
195
|
|
|
194
196
|
# Atlas operators that are valid only as the FIRST stage of a
|
|
@@ -202,7 +204,7 @@ module Parse
|
|
|
202
204
|
# for full-text and vector search is the dedicated
|
|
203
205
|
# `atlas_search` / `semantic_search` tools, not raw aggregate.
|
|
204
206
|
STAGE0_ONLY_ATLAS_STAGES = %w[
|
|
205
|
-
$search $searchMeta $vectorSearch $listSearchIndexes
|
|
207
|
+
$search $searchMeta $vectorSearch $listSearchIndexes $rankFusion
|
|
206
208
|
].freeze
|
|
207
209
|
|
|
208
210
|
# Cap on the length of a caller-supplied `$regex` (or the `regex:`
|
|
@@ -499,6 +499,35 @@ module Parse
|
|
|
499
499
|
end
|
|
500
500
|
end
|
|
501
501
|
|
|
502
|
+
# Equivalent to the $containedBy Parse query operation. Matches documents
|
|
503
|
+
# where the array field's values are all within the supplied set (the
|
|
504
|
+
# inverse of {ContainsAllConstraint}: the field must be a *subset* of the
|
|
505
|
+
# provided array). The field column should be of type {Array} in your
|
|
506
|
+
# Parse class.
|
|
507
|
+
#
|
|
508
|
+
# q.where :field.contained_by => [1, 2, 3]
|
|
509
|
+
# q.where :tags.contained_by => ["ruby", "rails", "parse"]
|
|
510
|
+
#
|
|
511
|
+
# @see ContainsAllConstraint
|
|
512
|
+
# @see ContainedInConstraint
|
|
513
|
+
class ContainedByConstraint < Constraint
|
|
514
|
+
# @!method contained_by
|
|
515
|
+
# A registered method on a symbol to create the constraint.
|
|
516
|
+
# Maps to Parse operator "$containedBy".
|
|
517
|
+
# @example
|
|
518
|
+
# q.where :tags.contained_by => ["ruby", "rails"]
|
|
519
|
+
# @return [ContainedByConstraint]
|
|
520
|
+
constraint_keyword :$containedBy
|
|
521
|
+
register :contained_by
|
|
522
|
+
|
|
523
|
+
# @return [Hash] the compiled constraint.
|
|
524
|
+
def build
|
|
525
|
+
val = formatted_value
|
|
526
|
+
val = [val].compact unless val.is_a?(Array)
|
|
527
|
+
{ @operation.operand => { key => val } }
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
502
531
|
# Array size constraint using MongoDB aggregation.
|
|
503
532
|
# Parse Server does not natively support $size query constraint, so we use
|
|
504
533
|
# MongoDB aggregation pipeline with $expr and $size to check array length.
|