parse-stack-next 5.2.1 → 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/.bundle/config +1 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +616 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +12 -4
- data/README.md +296 -3
- data/Rakefile +243 -41
- data/docs/atlas_vector_search_guide.md +86 -2
- data/docs/client_sdk_guide.md +38 -0
- data/docs/mcp_guide.md +119 -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 +174 -9
- 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/associations/belongs_to.rb +47 -0
- data/lib/parse/model/classes/audience.rb +52 -4
- data/lib/parse/model/classes/user.rb +200 -3
- data/lib/parse/model/core/embed_managed.rb +113 -0
- data/lib/parse/model/core/pluralized_aliases.rb +30 -0
- data/lib/parse/model/core/properties.rb +27 -0
- data/lib/parse/model/core/querying.rb +73 -1
- data/lib/parse/model/core/vector_searchable.rb +161 -0
- data/lib/parse/model/file.rb +35 -2
- 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 +173 -1
- 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 +399 -11
- 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 +16 -1
|
@@ -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/file.rb
CHANGED
|
@@ -734,10 +734,43 @@ module Parse
|
|
|
734
734
|
ATTRIBUTES
|
|
735
735
|
end
|
|
736
736
|
|
|
737
|
-
#
|
|
737
|
+
# The value used to decide whether two {Parse::File}s refer to the same
|
|
738
|
+
# underlying file -- for equality ({#==}) and, through it, for dirty
|
|
739
|
+
# tracking (the property setter compares files with `==` to decide whether
|
|
740
|
+
# a `:file` field changed).
|
|
741
|
+
#
|
|
742
|
+
# Today this is the bare canonical {#url}: signed-URL query parameters are
|
|
743
|
+
# stripped into `@presigned_url` and `force_ssl` coercion is applied, so two
|
|
744
|
+
# files at the same storage location compare equal regardless of how the URL
|
|
745
|
+
# was signed or whether it was `http`/`https`. The URL is the best identity
|
|
746
|
+
# signal currently available -- Parse Server's S3 files adapter does not
|
|
747
|
+
# surface a content digest (ETag / MD5 / sha256) through `Parse::File`.
|
|
748
|
+
#
|
|
749
|
+
# FUTURE DIRECTION: when a files adapter can expose a content hash, this is
|
|
750
|
+
# the single seam to override so equality keys off file *content* instead of
|
|
751
|
+
# URL -- e.g. a custom `Parse::File` subclass or adapter shim returning the
|
|
752
|
+
# S3 ETag / sha256 here. Overriding this one method updates {#==} (and a
|
|
753
|
+
# future `#eql?`/`#hash` pair, if added) without touching dirty tracking.
|
|
754
|
+
# No content-hash source exists yet, so the URL is authoritative for now.
|
|
755
|
+
# @return [String, nil]
|
|
756
|
+
def content_signature
|
|
757
|
+
url
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
# @return [Boolean] Two files are equal when their {#content_signature}
|
|
761
|
+
# matches. Both sides go through the same reader, so the comparison is
|
|
762
|
+
# symmetric and force_ssl-consistent: the previous `@url == u.url` form
|
|
763
|
+
# compared one side's raw stored URL against the other's normalized
|
|
764
|
+
# reader, so two files at the same location read as unequal whenever
|
|
765
|
+
# {Parse::File.force_ssl} coerced one side from `http://` to `https://`
|
|
766
|
+
# (and `a == b` disagreed with `b == a`). Because the default signature is
|
|
767
|
+
# the bare canonical URL (signed-URL query parameters stripped into
|
|
768
|
+
# `@presigned_url`), a freshly re-signed URL for the same object is equal
|
|
769
|
+
# while a different underlying location is not. See {#content_signature}
|
|
770
|
+
# for the content-hash override seam.
|
|
738
771
|
def ==(u)
|
|
739
772
|
return false unless u.is_a?(self.class)
|
|
740
|
-
|
|
773
|
+
content_signature == u.content_signature
|
|
741
774
|
end
|
|
742
775
|
|
|
743
776
|
# Allows mass assignment from a Parse JSON hash.
|
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.
|