parse-stack-next 5.4.1 → 5.5.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/CHANGELOG.md +344 -0
- data/Gemfile.lock +1 -1
- data/README.md +45 -6
- data/docs/atlas_vector_search_guide.md +314 -19
- data/lib/parse/api/users.rb +10 -0
- data/lib/parse/client.rb +19 -1
- data/lib/parse/embeddings/batch_embedder.rb +188 -0
- data/lib/parse/embeddings/cache.rb +322 -0
- data/lib/parse/embeddings/cohere.rb +31 -18
- data/lib/parse/embeddings/image_fetch.rb +347 -0
- data/lib/parse/embeddings/provider.rb +17 -11
- data/lib/parse/embeddings/spend_cap.rb +117 -3
- data/lib/parse/embeddings/voyage.rb +34 -25
- data/lib/parse/embeddings.rb +40 -3
- data/lib/parse/model/acl.rb +15 -11
- data/lib/parse/model/core/embed_managed.rb +243 -14
- data/lib/parse/model/core/vector_searchable.rb +157 -8
- data/lib/parse/query/constraint.rb +22 -0
- data/lib/parse/query/constraints.rb +271 -250
- data/lib/parse/query.rb +233 -42
- data/lib/parse/retrieval/agent_tool.rb +21 -14
- data/lib/parse/retrieval/retriever.rb +84 -0
- data/lib/parse/schema/search_index_migrator.rb +48 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/vector_search/hybrid.rb +39 -1
- data/lib/parse/vector_search.rb +34 -0
- data/lib/parse/webhooks/payload.rb +7 -1
- data/lib/parse/webhooks.rb +107 -21
- metadata +4 -1
|
@@ -372,6 +372,17 @@ module Parse
|
|
|
372
372
|
if pointer_fields
|
|
373
373
|
rows = Parse::CLPScope.filter_by_pointer_fields(rows, pointer_fields, resolution.user_id)
|
|
374
374
|
end
|
|
375
|
+
# NEW-VEC-1: the `$rankFusion` meta score is materialized
|
|
376
|
+
# BEFORE the ACL `$match`, so a surviving row's raw
|
|
377
|
+
# `_hybrid_score` encodes its rank among rows the caller
|
|
378
|
+
# cannot read — a cross-ACL inference channel for scoped
|
|
379
|
+
# callers probing with crafted queries. Recompute the
|
|
380
|
+
# surfaced score from the POST-filter ordering (the rows are
|
|
381
|
+
# already sorted by the true fused score, so relative order
|
|
382
|
+
# is preserved); the new value is a function of visible rows
|
|
383
|
+
# only. The client-side RRF path is unaffected — it ranks
|
|
384
|
+
# from already-filtered branch results.
|
|
385
|
+
recompute_scores_from_visible_order!(rows, k_constant: k_constant, weights: weights)
|
|
375
386
|
end
|
|
376
387
|
rows.map! { |doc| Parse::PipelineSecurity.strip_internal_fields(doc) }
|
|
377
388
|
rows
|
|
@@ -443,10 +454,20 @@ module Parse
|
|
|
443
454
|
# recognized-but-misused `$rankFusion` (or an unrelated auth/parse
|
|
444
455
|
# error) is treated as supported and surfaces its real error on the
|
|
445
456
|
# actual query rather than silently disabling native fusion.
|
|
457
|
+
#
|
|
458
|
+
# Deliberately narrow (NEW-VEC-2): a broad phrase like
|
|
459
|
+
# "is not allowed" also appears in MongoDB authorization errors
|
|
460
|
+
# ("not allowed to execute command aggregate"), which combined
|
|
461
|
+
# with the stage name in the message would misclassify an
|
|
462
|
+
# auth-failing cluster and cache the wrong probe verdict for
|
|
463
|
+
# PROBE_CACHE_TTL. Only phrases that unambiguously mean
|
|
464
|
+
# "this stage name is unknown to the parser" belong here; any
|
|
465
|
+
# other failure falls through to "supported" and the real query
|
|
466
|
+
# surfaces the real error (with the client path as fallback).
|
|
446
467
|
UNSUPPORTED_STAGE_FRAGMENTS = [
|
|
447
468
|
"unrecognized pipeline stage name",
|
|
448
469
|
"unknown aggregation stage",
|
|
449
|
-
"
|
|
470
|
+
"unknown stage",
|
|
450
471
|
].freeze
|
|
451
472
|
private_constant :UNSUPPORTED_STAGE_FRAGMENTS
|
|
452
473
|
|
|
@@ -455,6 +476,23 @@ module Parse
|
|
|
455
476
|
msg.include?("rankfusion") && UNSUPPORTED_STAGE_FRAGMENTS.any? { |f| msg.include?(f) }
|
|
456
477
|
end
|
|
457
478
|
|
|
479
|
+
# @!visibility private
|
|
480
|
+
# Replace each visible row's `_hybrid_score` with an RRF-shaped
|
|
481
|
+
# score derived from its position AMONG VISIBLE ROWS:
|
|
482
|
+
# `Σ_b weight_b / (k_constant + visible_rank)`. Monotone with the
|
|
483
|
+
# original fused order (input is already score-sorted), but
|
|
484
|
+
# carries no information about how many hidden rows ranked above
|
|
485
|
+
# or between the visible ones. See NEW-VEC-1.
|
|
486
|
+
def recompute_scores_from_visible_order!(rows, k_constant:, weights:)
|
|
487
|
+
w = weights ? symbolize(weights) : nil
|
|
488
|
+
total_weight = weight_for(w, :lexical).to_f + weight_for(w, :vector).to_f
|
|
489
|
+
rows.each_with_index do |doc, i|
|
|
490
|
+
next unless doc.is_a?(Hash)
|
|
491
|
+
doc["_hybrid_score"] = total_weight / (k_constant + i + 1)
|
|
492
|
+
end
|
|
493
|
+
rows
|
|
494
|
+
end
|
|
495
|
+
|
|
458
496
|
# -- probe cache -------------------------------------------------
|
|
459
497
|
|
|
460
498
|
PROBE_MUTEX_INIT = Mutex.new
|
data/lib/parse/vector_search.rb
CHANGED
|
@@ -95,7 +95,41 @@ module Parse
|
|
|
95
95
|
# one. Atlas's guidance: numCandidates ≥ 10 × limit, ≤ 10_000.
|
|
96
96
|
DEFAULT_NUM_CANDIDATES_MULTIPLIER = 20
|
|
97
97
|
|
|
98
|
+
# Accepted {.index_drift_policy} values.
|
|
99
|
+
INDEX_DRIFT_POLICIES = %i[warn raise ignore].freeze
|
|
100
|
+
|
|
98
101
|
class << self
|
|
102
|
+
# Policy applied when first-query index verification (see
|
|
103
|
+
# {Parse::Core::VectorSearchable}) finds the deployed Atlas
|
|
104
|
+
# vectorSearch index disagreeing with the model declaration —
|
|
105
|
+
# wrong `numDimensions`, wrong `similarity`, or a tenant-scope
|
|
106
|
+
# field missing from the index's `filter` paths.
|
|
107
|
+
#
|
|
108
|
+
# * `:warn` (default) — emit a `[Parse::VectorSearch:DRIFT]`
|
|
109
|
+
# warning once per (class, field, index) and continue. Drift
|
|
110
|
+
# usually means the index predates a model change; queries
|
|
111
|
+
# still run but return degraded or wrongly-scoped results.
|
|
112
|
+
# * `:raise` — fail the query with
|
|
113
|
+
# {Parse::Core::VectorSearchable::IndexDriftError}. Strict mode
|
|
114
|
+
# for deployments that treat drift as a release blocker.
|
|
115
|
+
# * `:ignore` — skip verification entirely.
|
|
116
|
+
#
|
|
117
|
+
# @param value [Symbol]
|
|
118
|
+
# @return [Symbol]
|
|
119
|
+
def index_drift_policy=(value)
|
|
120
|
+
v = value.respond_to?(:to_sym) ? value.to_sym : nil
|
|
121
|
+
unless v && INDEX_DRIFT_POLICIES.include?(v)
|
|
122
|
+
raise ArgumentError,
|
|
123
|
+
"Parse::VectorSearch.index_drift_policy must be one of " \
|
|
124
|
+
"#{INDEX_DRIFT_POLICIES.inspect} (got #{value.inspect})."
|
|
125
|
+
end
|
|
126
|
+
@index_drift_policy = v
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [Symbol] current drift policy (default `:warn`).
|
|
130
|
+
def index_drift_policy
|
|
131
|
+
@index_drift_policy ||= :warn
|
|
132
|
+
end
|
|
99
133
|
# Low-level `$vectorSearch` entry point.
|
|
100
134
|
#
|
|
101
135
|
# @param collection_name [String] Parse class name / Mongo
|
|
@@ -740,7 +740,13 @@ module Parse
|
|
|
740
740
|
# callback handling based on the request origin.
|
|
741
741
|
# @return [Boolean] true if the request originated from Ruby Parse Stack
|
|
742
742
|
def ruby_initiated?
|
|
743
|
-
|
|
743
|
+
# Stable memoization: a plain `||=` re-derives whenever the stored value
|
|
744
|
+
# is `false`, so a previously-computed (or externally-stamped, e.g. by
|
|
745
|
+
# Parse::Webhooks.call_route) `false` would be recomputed on every call
|
|
746
|
+
# and could disagree with the stamping caller. Cache on `defined?` so a
|
|
747
|
+
# `false` result is memoized exactly once and never silently re-derived.
|
|
748
|
+
return @ruby_initiated if defined?(@ruby_initiated) && !@ruby_initiated.nil?
|
|
749
|
+
@ruby_initiated = begin
|
|
744
750
|
request_id = nil
|
|
745
751
|
|
|
746
752
|
if @raw.respond_to?(:[])
|
data/lib/parse/webhooks.rb
CHANGED
|
@@ -455,34 +455,114 @@ module Parse
|
|
|
455
455
|
end
|
|
456
456
|
|
|
457
457
|
if type == :after_save && payload&.parse_object.present? && payload.parse_object.is_a?(Parse::Object)
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
#
|
|
461
|
-
#
|
|
462
|
-
#
|
|
463
|
-
#
|
|
464
|
-
#
|
|
465
|
-
#
|
|
466
|
-
# a chance to run callbacks, so we fire them here.
|
|
458
|
+
# The chained ActiveModel after_save/after_create callbacks are NOT
|
|
459
|
+
# fired here. `call!` dispatches every trigger twice -- once for the
|
|
460
|
+
# specific class route and once for the generic `"*"` route -- so
|
|
461
|
+
# firing the model callbacks inside this per-route block double-fired
|
|
462
|
+
# them for any app that registered BOTH a class route and a `"*"`
|
|
463
|
+
# route (e.g. an `after_save :send_email` would send two emails per
|
|
464
|
+
# save). The dispatch now lives in `run_after_save_chain`, which
|
|
465
|
+
# `call!` invokes exactly once per delivery after both route calls.
|
|
467
466
|
#
|
|
468
|
-
#
|
|
469
|
-
#
|
|
470
|
-
#
|
|
471
|
-
# so a handler that returns the parse_object -- the recommended
|
|
472
|
-
# before_save pattern, easy to copy by mistake -- must NOT silently
|
|
473
|
-
# suppress these callbacks. We normalize the result to `true` below
|
|
474
|
-
# so a returned object never leaks into the response or the log.
|
|
475
|
-
is_new = payload.original.nil?
|
|
476
|
-
unless trusted_ruby_initiated
|
|
477
|
-
payload.parse_object.run_after_create_callbacks if is_new
|
|
478
|
-
payload.parse_object.run_after_save_callbacks
|
|
479
|
-
end
|
|
467
|
+
# We still normalize the result to `true` so a handler that returned
|
|
468
|
+
# the parse_object (the recommended before_save pattern, easy to copy
|
|
469
|
+
# by mistake) never leaks an object into the response or the log.
|
|
480
470
|
result = true
|
|
481
471
|
end
|
|
482
472
|
|
|
483
473
|
result
|
|
484
474
|
end
|
|
485
475
|
|
|
476
|
+
# Fires the chained ActiveModel after_save (and after_create, for a new
|
|
477
|
+
# object) callbacks for an afterSave delivery -- exactly once per request.
|
|
478
|
+
#
|
|
479
|
+
# This lives in `call!` rather than `call_route` because `call!` dispatches
|
|
480
|
+
# every trigger twice (the specific class route AND the generic `"*"`
|
|
481
|
+
# route). Firing the model callbacks per-route would double-fire any side
|
|
482
|
+
# effect for an app that registered both routes. Calling this once, after
|
|
483
|
+
# both route calls, fires the chain exactly once regardless of how many
|
|
484
|
+
# routes matched.
|
|
485
|
+
#
|
|
486
|
+
# The decision to fire depends ONLY on request origin, never on what a
|
|
487
|
+
# handler returned: Parse Server discards the afterSave response body
|
|
488
|
+
# entirely, so a handler returning the parse_object must not suppress the
|
|
489
|
+
# callbacks. For trusted-Ruby-initiated saves (both the `_RB_` request-id
|
|
490
|
+
# header AND the master key) Parse Stack's local `run_callbacks :save`
|
|
491
|
+
# already fires these after the REST response returns, so we skip them
|
|
492
|
+
# here to avoid the double-fire. The route-present guard preserves the
|
|
493
|
+
# "an unregistered afterSave trigger never fires model callbacks" contract
|
|
494
|
+
# that `call_route`'s early return used to provide.
|
|
495
|
+
#
|
|
496
|
+
# @param payload [Parse::Webhooks::Payload] the afterSave payload.
|
|
497
|
+
# @return [void]
|
|
498
|
+
def run_after_save_chain(payload)
|
|
499
|
+
return unless payload&.after_save?
|
|
500
|
+
return unless payload.parse_object.is_a?(Parse::Object)
|
|
501
|
+
|
|
502
|
+
# Preserve the "no registered route => no model callbacks" behavior that
|
|
503
|
+
# call_route's `return unless routes[type][className].present?` enforced.
|
|
504
|
+
# Mirror that guard exactly: key on parse_class.to_s (as call_route does)
|
|
505
|
+
# and use `.present?` on the value -- registration stores an Array, and an
|
|
506
|
+
# empty/absent registration must NOT fire (matching the original).
|
|
507
|
+
after_save_routes = routes[:after_save]
|
|
508
|
+
return unless after_save_routes &&
|
|
509
|
+
(after_save_routes[payload.parse_class.to_s].present? ||
|
|
510
|
+
after_save_routes["*"].present?)
|
|
511
|
+
|
|
512
|
+
# Trusted-Ruby-initiated saves run their callbacks locally; firing again
|
|
513
|
+
# here would double them. This must match call_route's trusted_ruby_initiated
|
|
514
|
+
# EXACTLY. call_route runs (and stamps @ruby_initiated) before this for any
|
|
515
|
+
# matched route, so read that stamped value rather than recomputing via
|
|
516
|
+
# `ruby_initiated?` -- whose `||=` memoization re-derives on a stamped
|
|
517
|
+
# `false` and could disagree with call_route's header lookup.
|
|
518
|
+
return if payload.ruby_initiated? && payload.master? == true
|
|
519
|
+
|
|
520
|
+
# By the time afterSave fires the object is ALREADY persisted in Parse
|
|
521
|
+
# Server, and Parse Server discards the afterSave response body entirely
|
|
522
|
+
# (it resolves success even if the handler throws). So a chained callback
|
|
523
|
+
# that raises must not (a) 500 the webhook endpoint -- `call!`'s rescue
|
|
524
|
+
# only catches ResponseError / ValidationError, so a bare StandardError
|
|
525
|
+
# would escape -- nor (b) take out the OTHER phase's unrelated side
|
|
526
|
+
# effects. Run the after_create and after_save phases independently, each
|
|
527
|
+
# guarded, logging and swallowing any StandardError. This mirrors Parse's
|
|
528
|
+
# own afterSave semantics (log-and-continue on a post-persist failure):
|
|
529
|
+
# a raising `after_create :send_welcome_email` no longer silently skips
|
|
530
|
+
# an unrelated `after_save :reindex`, and neither can crash the endpoint.
|
|
531
|
+
obj = payload.parse_object
|
|
532
|
+
run_after_save_phase(obj, :after_create) if payload.original.nil?
|
|
533
|
+
run_after_save_phase(obj, :after_save)
|
|
534
|
+
nil
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Runs one phase (:after_create or :after_save) of an afterSave object's
|
|
538
|
+
# chained ActiveModel callbacks, swallowing and logging any StandardError
|
|
539
|
+
# so a post-persist callback failure can't crash the webhook endpoint or
|
|
540
|
+
# suppress the sibling phase. ActiveModel still halts the rest of *this*
|
|
541
|
+
# phase's chain on a raise -- only the cross-phase / endpoint blast radius
|
|
542
|
+
# is contained here. Note this also swallows a ResponseError/ValidationError
|
|
543
|
+
# raised from inside an after_save callback: afterSave is post-persist and
|
|
544
|
+
# Parse Server discards the response body, so an `error!` there cannot deny
|
|
545
|
+
# the (already-committed) write -- it is logged, not propagated.
|
|
546
|
+
# @param obj [Parse::Object] the persisted afterSave object.
|
|
547
|
+
# @param phase [Symbol] :after_create or :after_save.
|
|
548
|
+
# @return [void]
|
|
549
|
+
def run_after_save_phase(obj, phase)
|
|
550
|
+
case phase
|
|
551
|
+
when :after_create then obj.run_after_create_callbacks
|
|
552
|
+
when :after_save then obj.run_after_save_callbacks
|
|
553
|
+
end
|
|
554
|
+
nil
|
|
555
|
+
rescue => e
|
|
556
|
+
# Redact the exception message before logging: a callback error can echo
|
|
557
|
+
# record contents/tokens, and the rest of this file routes log output
|
|
558
|
+
# through the same redactor.
|
|
559
|
+
warn "[Parse::Webhooks] afterSave #{phase} callback raised for " \
|
|
560
|
+
"#{obj.class}##{obj.id} -- the object is already persisted; " \
|
|
561
|
+
"logging and continuing: #{e.class}: " \
|
|
562
|
+
"#{Parse::Middleware::BodyBuilder.redact(e.message)}"
|
|
563
|
+
nil
|
|
564
|
+
end
|
|
565
|
+
|
|
486
566
|
# Generates a success response for Parse Server.
|
|
487
567
|
# @param data [Object] the data to send back with the success.
|
|
488
568
|
# @return [Hash] a success data payload
|
|
@@ -688,6 +768,12 @@ module Parse
|
|
|
688
768
|
# call hooks subscribed to any class route
|
|
689
769
|
generic_result = Parse::Webhooks.call_route(payload.trigger_name, "*", payload)
|
|
690
770
|
result = generic_result if generic_result.present? && result.nil?
|
|
771
|
+
|
|
772
|
+
# Fire the chained ActiveModel after_save/after_create callbacks
|
|
773
|
+
# exactly once per delivery -- after BOTH route calls above -- so an
|
|
774
|
+
# app that registers both a class route and a `"*"` route doesn't
|
|
775
|
+
# double-fire them. No-op for every non-afterSave trigger.
|
|
776
|
+
Parse::Webhooks.run_after_save_chain(payload)
|
|
691
777
|
else
|
|
692
778
|
if self.logging.present?
|
|
693
779
|
puts "[Webhooks] --> Could not find mapping route for " \
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parse-stack-next
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adrian Curtin
|
|
@@ -319,8 +319,11 @@ files:
|
|
|
319
319
|
- lib/parse/clp_scope.rb
|
|
320
320
|
- lib/parse/console.rb
|
|
321
321
|
- lib/parse/embeddings.rb
|
|
322
|
+
- lib/parse/embeddings/batch_embedder.rb
|
|
323
|
+
- lib/parse/embeddings/cache.rb
|
|
322
324
|
- lib/parse/embeddings/cohere.rb
|
|
323
325
|
- lib/parse/embeddings/fixture.rb
|
|
326
|
+
- lib/parse/embeddings/image_fetch.rb
|
|
324
327
|
- lib/parse/embeddings/jina.rb
|
|
325
328
|
- lib/parse/embeddings/local_http.rb
|
|
326
329
|
- lib/parse/embeddings/openai.rb
|