parse-stack-next 4.5.0 → 5.0.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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.env.sample +17 -3
  4. data/.github/workflows/codeql.yml +44 -0
  5. data/.github/workflows/docs.yml +39 -0
  6. data/.github/workflows/release.yml +32 -0
  7. data/.github/workflows/ruby.yml +8 -6
  8. data/.gitignore +4 -0
  9. data/.vscode/settings.json +3 -0
  10. data/CHANGELOG.md +305 -72
  11. data/Gemfile.lock +10 -3
  12. data/LICENSE.txt +1 -1
  13. data/README.md +190 -219
  14. data/Rakefile +1 -1
  15. data/SECURITY.md +30 -0
  16. data/assets/parse-stack-next-avatar.png +0 -0
  17. data/assets/parse-stack-next-avatar.svg +37 -0
  18. data/assets/parse-stack-next-banner.png +0 -0
  19. data/assets/parse-stack-next-banner.svg +45 -0
  20. data/assets/parse-stack-next-social-preview.png +0 -0
  21. data/docs/atlas_vector_search_guide.md +511 -0
  22. data/docs/client_sdk_guide.md +1320 -0
  23. data/docs/mcp_guide.md +225 -104
  24. data/docs/mongodb_direct_guide.md +21 -4
  25. data/docs/usage_guide.md +585 -0
  26. data/examples/transaction_example.rb +28 -28
  27. data/lib/parse/acl_scope.rb +2 -2
  28. data/lib/parse/agent/mcp_rack_app.rb +184 -16
  29. data/lib/parse/agent/metadata_dsl.rb +16 -16
  30. data/lib/parse/agent/pipeline_validator.rb +28 -1
  31. data/lib/parse/agent/prompts.rb +5 -5
  32. data/lib/parse/agent/tools.rb +287 -14
  33. data/lib/parse/agent.rb +209 -12
  34. data/lib/parse/api/analytics.rb +27 -5
  35. data/lib/parse/api/files.rb +6 -2
  36. data/lib/parse/api/push.rb +21 -4
  37. data/lib/parse/api/server.rb +59 -0
  38. data/lib/parse/api/users.rb +26 -2
  39. data/lib/parse/atlas_search/index_manager.rb +84 -0
  40. data/lib/parse/atlas_search.rb +37 -9
  41. data/lib/parse/cache/pool.rb +88 -0
  42. data/lib/parse/cache/redis.rb +249 -0
  43. data/lib/parse/client/body_builder.rb +94 -0
  44. data/lib/parse/client/caching.rb +109 -9
  45. data/lib/parse/client/response.rb +27 -0
  46. data/lib/parse/client.rb +74 -3
  47. data/lib/parse/console.rb +203 -0
  48. data/lib/parse/embeddings/cohere.rb +484 -0
  49. data/lib/parse/embeddings/fixture.rb +130 -0
  50. data/lib/parse/embeddings/jina.rb +454 -0
  51. data/lib/parse/embeddings/local_http.rb +492 -0
  52. data/lib/parse/embeddings/openai.rb +520 -0
  53. data/lib/parse/embeddings/provider.rb +264 -0
  54. data/lib/parse/embeddings/qwen.rb +431 -0
  55. data/lib/parse/embeddings/voyage.rb +550 -0
  56. data/lib/parse/embeddings.rb +225 -0
  57. data/lib/parse/graphql/scalars.rb +53 -0
  58. data/lib/parse/graphql/type_generator.rb +264 -0
  59. data/lib/parse/graphql.rb +48 -0
  60. data/lib/parse/live_query/client.rb +24 -5
  61. data/lib/parse/live_query/subscription.rb +17 -6
  62. data/lib/parse/live_query.rb +9 -4
  63. data/lib/parse/model/associations/collection_proxy.rb +2 -2
  64. data/lib/parse/model/associations/has_many.rb +32 -1
  65. data/lib/parse/model/associations/has_one.rb +17 -0
  66. data/lib/parse/model/associations/pointer_collection_proxy.rb +3 -3
  67. data/lib/parse/model/classes/user.rb +307 -11
  68. data/lib/parse/model/clp.rb +1 -1
  69. data/lib/parse/model/core/create_lock.rb +14 -2
  70. data/lib/parse/model/core/embed_managed.rb +296 -0
  71. data/lib/parse/model/core/fetching.rb +4 -4
  72. data/lib/parse/model/core/indexing.rb +53 -14
  73. data/lib/parse/model/core/parse_reference.rb +3 -3
  74. data/lib/parse/model/core/properties.rb +70 -1
  75. data/lib/parse/model/core/querying.rb +57 -1
  76. data/lib/parse/model/core/vector_searchable.rb +285 -0
  77. data/lib/parse/model/file.rb +16 -4
  78. data/lib/parse/model/model.rb +26 -10
  79. data/lib/parse/model/object.rb +63 -6
  80. data/lib/parse/model/pointer.rb +16 -2
  81. data/lib/parse/model/shortnames.rb +2 -0
  82. data/lib/parse/model/validations/uniqueness_validator.rb +3 -3
  83. data/lib/parse/model/vector.rb +102 -0
  84. data/lib/parse/mongodb.rb +90 -8
  85. data/lib/parse/pipeline_security.rb +59 -2
  86. data/lib/parse/query/constraints.rb +16 -14
  87. data/lib/parse/query/ordering.rb +1 -1
  88. data/lib/parse/query.rb +137 -64
  89. data/lib/parse/stack/generators/templates/model.erb +2 -2
  90. data/lib/parse/stack/generators/templates/model_installation.rb +1 -1
  91. data/lib/parse/stack/generators/templates/model_role.rb +1 -1
  92. data/lib/parse/stack/generators/templates/model_session.rb +1 -1
  93. data/lib/parse/stack/generators/templates/parse.rb +1 -1
  94. data/lib/parse/stack/generators/templates/webhooks.rb +1 -1
  95. data/lib/parse/stack/version.rb +1 -1
  96. data/lib/parse/stack.rb +375 -73
  97. data/lib/parse/two_factor_auth/user_extension.rb +5 -2
  98. data/lib/parse/vector_search.rb +341 -0
  99. data/parse-stack-next.gemspec +10 -9
  100. data/scripts/docker/docker-compose.test.yml +18 -0
  101. data/scripts/start-parse.sh +6 -0
  102. data/scripts/vector_prototype/create_vector_index.js +105 -0
  103. data/scripts/vector_prototype/fetch_embeddings.py +241 -0
  104. data/scripts/vector_prototype/fixture_manifest.json +9 -0
  105. data/scripts/vector_prototype/query_prototype.rb +84 -0
  106. data/scripts/vector_prototype/run.sh +34 -0
  107. metadata +77 -5
  108. data/parse-stack.png +0 -0
data/Rakefile CHANGED
@@ -568,6 +568,6 @@ end
568
568
 
569
569
  YARD::Rake::YardocTask.new do |t|
570
570
  t.files = ["lib/**/*.rb"] # optional
571
- t.options = ["-o", "doc/parse-stack"] # optional
571
+ t.options = ["-o", "doc/parse-stack-next"] # optional
572
572
  t.stats_options = ["--list-undoc"] # optional
573
573
  end
data/SECURITY.md ADDED
@@ -0,0 +1,30 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Security fixes are applied to the current major release line. Older majors
6
+ receive fixes only for critical vulnerabilities at the maintainer's discretion.
7
+
8
+ | Version | Supported |
9
+ | ------- | ------------------ |
10
+ | 5.x | :white_check_mark: |
11
+ | < 5.0 | :x: |
12
+
13
+ ## Reporting a Vulnerability
14
+
15
+ Please report suspected vulnerabilities privately by email to
16
+ **security@neurosynq.net**. Do not open a public GitHub issue for
17
+ security reports.
18
+
19
+ Include as much of the following as you can:
20
+
21
+ - A description of the issue and its impact
22
+ - Affected version(s) of `parse-stack-next`
23
+ - Steps to reproduce, or a minimal proof-of-concept
24
+ - Any suggested remediation
25
+
26
+ You can expect an initial acknowledgement within 5 business days. Once the
27
+ report is triaged, the maintainer will share a remediation plan and target
28
+ timeline. Accepted vulnerabilities will be fixed in a coordinated release and
29
+ credited in the changelog unless you request otherwise. Reports that fall
30
+ outside the project's threat model will be declined with an explanation.
Binary file
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400" role="img" aria-labelledby="title desc">
3
+ <title id="title">parse_stack_next avatar</title>
4
+ <desc id="desc">Square avatar mark for parse_stack_next: isometric three-tier stack with parse-blue, gray bridge, and faceted emerald-cut ruby gem on top.</desc>
5
+
6
+ <rect width="400" height="400" fill="#15151a" rx="56"/>
7
+
8
+ <g transform="translate(177.5, 95.9) scale(3)">
9
+ <polygon points="55,50 69.74,39.67 69.74,61.67 55,72" fill="#0E6EAB"/>
10
+ <polygon points="-55,50 55,50 69.74,39.67 -40.26,39.67" fill="#2BB1F3"/>
11
+ <polygon points="-55,50 55,50 55,72 -55,72" fill="#169CEE"/>
12
+
13
+ <polygon points="48,28 62.74,17.67 62.74,39.67 48,50" fill="#383841"/>
14
+ <polygon points="-48,28 48,28 62.74,17.67 -33.26,17.67" fill="#5d5d68"/>
15
+ <polygon points="-48,28 48,28 48,50 -48,50" fill="#4a4a55"/>
16
+
17
+ <polygon points="51.47,10.96 50.74,8.67 33.47,19.96 33.83,21.11" fill="#3B0A11"/>
18
+ <polygon points="51.47,10.96 50.74,8.67 50.74,4.67 51.47,6.96" fill="#401015"/>
19
+ <polygon points="51.47,6.96 50.74,4.67 37.29,-2.61 37.83,-0.89" fill="#5C1019"/>
20
+
21
+ <polygon points="43.28,16.70 51.47,10.96 33.83,21.11 28.91,24.56" fill="#5C131C"/>
22
+ <polygon points="43.28,16.70 51.47,10.96 51.47,6.96 43.28,12.70" fill="#6E141F"/>
23
+ <polygon points="43.28,12.70 51.47,6.96 37.83,-0.89 32.91,2.56" fill="#8A1A2A"/>
24
+
25
+ <polygon points="36,19 43.28,16.70 28.91,24.56 25.28,25.70" fill="#7B1822"/>
26
+ <polygon points="36,19 43.28,16.70 43.28,12.70 36,15" fill="#931C29"/>
27
+ <polygon points="36,15 43.28,12.70 32.91,2.56 27.46,4.28" fill="#C42535"/>
28
+
29
+ <polygon points="-36,19 36,19 25.28,25.70 -18.72,25.70" fill="#951C28"/>
30
+ <polygon points="-36,19 36,19 36,15 -36,15" fill="#B82230"/>
31
+ <polygon points="-36,15 36,15 27.46,4.28 -22.54,4.28" fill="#E63946"/>
32
+
33
+ <polygon points="-22.54,4.28 27.46,4.28 32.91,2.56 37.83,-0.89 37.29,-2.61 -12.71,-2.61 -18.17,-0.89 -23.09,2.56" fill="#FF7A88"/>
34
+
35
+ <polygon points="-12.07,3.075 20.43,3.075 23.98,1.955 27.17,-0.286 26.82,-1.404 -5.68,-1.404 -9.24,-0.286 -12.43,1.955" fill="#E64552"/>
36
+ </g>
37
+ </svg>
Binary file
@@ -0,0 +1,45 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 340" width="1200" height="340" role="img" aria-labelledby="title desc">
3
+ <title id="title">parse_stack_next</title>
4
+ <desc id="desc">The Ruby stack for Parse Server — wordmark with an isometric three-tier stack icon: a parse-blue slab, a gray bridge slab, and a faceted emerald-cut ruby gem on top.</desc>
5
+
6
+ <rect width="1200" height="340" fill="#15151a" rx="20"/>
7
+
8
+ <g transform="translate(195, 100) scale(2)">
9
+ <polygon points="55,50 69.74,39.67 69.74,61.67 55,72" fill="#0E6EAB"/>
10
+ <polygon points="-55,50 55,50 69.74,39.67 -40.26,39.67" fill="#2BB1F3"/>
11
+ <polygon points="-55,50 55,50 55,72 -55,72" fill="#169CEE"/>
12
+
13
+ <polygon points="48,28 62.74,17.67 62.74,39.67 48,50" fill="#383841"/>
14
+ <polygon points="-48,28 48,28 62.74,17.67 -33.26,17.67" fill="#5d5d68"/>
15
+ <polygon points="-48,28 48,28 48,50 -48,50" fill="#4a4a55"/>
16
+
17
+ <polygon points="51.47,10.96 50.74,8.67 33.47,19.96 33.83,21.11" fill="#3B0A11"/>
18
+ <polygon points="51.47,10.96 50.74,8.67 50.74,4.67 51.47,6.96" fill="#401015"/>
19
+ <polygon points="51.47,6.96 50.74,4.67 37.29,-2.61 37.83,-0.89" fill="#5C1019"/>
20
+
21
+ <polygon points="43.28,16.70 51.47,10.96 33.83,21.11 28.91,24.56" fill="#5C131C"/>
22
+ <polygon points="43.28,16.70 51.47,10.96 51.47,6.96 43.28,12.70" fill="#6E141F"/>
23
+ <polygon points="43.28,12.70 51.47,6.96 37.83,-0.89 32.91,2.56" fill="#8A1A2A"/>
24
+
25
+ <polygon points="36,19 43.28,16.70 28.91,24.56 25.28,25.70" fill="#7B1822"/>
26
+ <polygon points="36,19 43.28,16.70 43.28,12.70 36,15" fill="#931C29"/>
27
+ <polygon points="36,15 43.28,12.70 32.91,2.56 27.46,4.28" fill="#C42535"/>
28
+
29
+ <polygon points="-36,19 36,19 25.28,25.70 -18.72,25.70" fill="#951C28"/>
30
+ <polygon points="-36,19 36,19 36,15 -36,15" fill="#B82230"/>
31
+ <polygon points="-36,15 36,15 27.46,4.28 -22.54,4.28" fill="#E63946"/>
32
+
33
+ <polygon points="-22.54,4.28 27.46,4.28 32.91,2.56 37.83,-0.89 37.29,-2.61 -12.71,-2.61 -18.17,-0.89 -23.09,2.56" fill="#FF7A88"/>
34
+
35
+ <polygon points="-12.07,3.075 20.43,3.075 23.98,1.955 27.17,-0.286 26.82,-1.404 -5.68,-1.404 -9.24,-0.286 -12.43,1.955" fill="#E64552"/>
36
+ </g>
37
+
38
+ <text x="400" y="178" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="64" font-weight="500" letter-spacing="-1">
39
+ <tspan fill="#f0f0f0">parse_stack</tspan><tspan fill="#E63946">_next</tspan>
40
+ </text>
41
+
42
+ <text x="402" y="215" font-family="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" font-size="20" fill="#8a8a92" letter-spacing="0.6">
43
+ The Ruby stack for Parse Server
44
+ </text>
45
+ </svg>
@@ -0,0 +1,511 @@
1
+ # Atlas Vector Search Guide
2
+
3
+ Parse Stack v5.0 ships first-class support for MongoDB Atlas
4
+ `$vectorSearch` against Parse classes. This guide covers the full
5
+ surface: declaring `:vector` properties, registering embedding
6
+ providers, running `find_similar` queries, the `embed` write-side
7
+ macro, Atlas index management, AS::N telemetry, and the constraint and
8
+ logging behavior callers need to know about.
9
+
10
+ For the underlying mongo-direct enforcement model that vector search
11
+ inherits, see [mongodb_direct_guide.md](./mongodb_direct_guide.md).
12
+
13
+ ---
14
+
15
+ ## When to use vector search
16
+
17
+ Use Atlas vector search when:
18
+
19
+ * You need semantic similarity ("articles about X" where "about X" is
20
+ a meaning, not a substring) rather than substring / token matches.
21
+ * Your records have a natural text or image embedding source (title +
22
+ body, transcript, caption, etc.).
23
+ * You are already running on MongoDB Atlas, or on a self-managed
24
+ cluster with the search/vectorSearch extension available. Atlas
25
+ Local works for development and integration tests.
26
+
27
+ Do NOT use vector search for:
28
+
29
+ * Exact / substring matching — use Parse's normal query operators or
30
+ Atlas `$search` text indexes (see Atlas Search docs).
31
+ * Tiny corpora (< a few hundred docs) where a brute-force cosine in
32
+ application code would be cheaper than maintaining an index.
33
+
34
+ ---
35
+
36
+ ## Declaring a `:vector` property
37
+
38
+ `:vector` is a first-class Parse property type. The declaration
39
+ captures the vector's width, the provider that produces it, the model
40
+ name, and the similarity function the Atlas index will use.
41
+
42
+ ```ruby
43
+ class Document < Parse::Object
44
+ property :title, :string
45
+ property :body, :string
46
+
47
+ property :body_embedding, :vector,
48
+ dimensions: 1536,
49
+ provider: :openai,
50
+ model: "text-embedding-3-small",
51
+ similarity: :cosine
52
+ end
53
+ ```
54
+
55
+ * `dimensions:` (required) — fixed output width. Must match what the
56
+ registered provider returns and what the Atlas vectorSearch index
57
+ declares. Mismatches raise `Parse::Embeddings::InvalidResponseError`
58
+ on write or `Parse::VectorSearch::InvalidQueryVector` on read.
59
+ * `provider:` — name registered via `Parse::Embeddings.register` or
60
+ `Parse::Embeddings.configure`. Required for the `embed` macro and for
61
+ the `find_similar(text:)` overload; optional if you only ever pass
62
+ pre-computed `vector:` Arrays.
63
+ * `model:` — stable identifier, persisted to `embedding_meta` and used
64
+ in cache keys. Changing this on an existing field is a migration —
65
+ see the §Re-embedding section below.
66
+ * `similarity:` — one of `:cosine`, `:dotProduct`, `:euclidean`.
67
+ Determines how the Atlas index ranks. Pick `:cosine` for normalized
68
+ text embeddings; `:dotProduct` for raw OpenAI/Cohere output if you
69
+ want to skip the unit-normalize step.
70
+
71
+ ### Storage shape
72
+
73
+ `:vector` properties serialize as plain BSON arrays of floats. There
74
+ is no Parse-side wrapper class on the wire. In memory they are
75
+ `Parse::Vector` instances which respond to `to_a`, `dimensions`, and
76
+ arithmetic helpers.
77
+
78
+ ### Constraint refusal
79
+
80
+ Vector fields are NOT general-purpose query targets. The query builder
81
+ refuses every operator on `:vector` columns except `:exists` and
82
+ `:null`. Attempting `where(body_embedding: <Array>)` or
83
+ `where(:body_embedding.gt => 0.5)` raises at query build time — semantic
84
+ similarity must go through `find_similar`, not the normal `where` DSL.
85
+
86
+ ### Body builder compaction
87
+
88
+ When `Parse::Object#inspect` or the request logger has to print a
89
+ record carrying a vector, the formatter replaces the array with a
90
+ compact `<vector dims=N>` placeholder once the length is ≥ 32. This
91
+ keeps multi-thousand-dim arrays out of error trackers and stack
92
+ traces. The wire payload itself is unchanged.
93
+
94
+ ---
95
+
96
+ ## Registering an embedding provider
97
+
98
+ `Parse::Embeddings` is a pluggable registry. v5.0 ships seven built-in
99
+ providers:
100
+
101
+ * `Parse::Embeddings::OpenAI` — text-only. `text-embedding-3-small`
102
+ (1536-dim, default), `text-embedding-3-large` (3072-dim, Matryoshka via
103
+ `dimensions:`), legacy `text-embedding-ada-002`. Forwards
104
+ `OpenAI-Organization` / `OpenAI-Project` headers when supplied.
105
+ * `Parse::Embeddings::Cohere` — v3 family (`embed-english-v3.0`,
106
+ `embed-multilingual-v3.0`, and `-light-v3.0` siblings; 1024 / 384 dim)
107
+ plus `embed-v4.0` (1536 native, 128k token context, Matryoshka-
108
+ truncatable to {256, 512, 1024, 1536} via `dimensions:`). `embed-v4.0`
109
+ is Cohere's text+image multimodal endpoint at the network boundary;
110
+ this release wires the **text path only** — `embed_image` lands in
111
+ v5.1.
112
+ * `Parse::Embeddings::Voyage` — voyage-4 family (`voyage-4-large` 2048,
113
+ Matryoshka; `voyage-4` 1024; `voyage-4-lite` 512; `voyage-4-nano` 256),
114
+ voyage-3 family, domain models (`voyage-code-3`, `voyage-finance-2`,
115
+ `voyage-law-2`), and `voyage-multimodal-3` (1024-dim, 32k token
116
+ context, routes to `/v1/multimodalembeddings` with the wrapped
117
+ `{inputs: [{content: [{type: "text", text: ...}]}]}` envelope). Text
118
+ inputs only in v5.0 — image content rows land in v5.1.
119
+ * `Parse::Embeddings::Jina` — `jina-embeddings-v3` (1024, Matryoshka
120
+ 32–1024), `jina-embeddings-v4` (2048, Matryoshka), v5 family
121
+ (`jina-embeddings-v5-text-{small,nano}`,
122
+ `jina-embeddings-v5-omni-{small,nano}` — omni accepts plain-text
123
+ here), and `jina-code-embeddings-{0.5b,1.5b}`. Distinguishes
124
+ `input_type:` via Jina's `task` field
125
+ (`retrieval.query` / `retrieval.passage` / `classification` /
126
+ `separation`). Rerankers and image-only models are out of scope.
127
+ * `Parse::Embeddings::Qwen` — `qwen3-embedding-0.6b` (1024),
128
+ `qwen3-embedding-4b` (2560), `qwen3-embedding-8b` (4096), all
129
+ Matryoshka. Targets Alibaba Cloud DashScope's OpenAI-compatible
130
+ endpoint; operators in mainland China override `base_url:` to
131
+ `https://dashscope.aliyuncs.com/compatible-mode/v1`. Same checkpoints
132
+ are open-weight on Hugging Face (Apache 2.0) — self-host with
133
+ `LocalHTTP`.
134
+ * `Parse::Embeddings::LocalHTTP` — generic OpenAI-compatible client for
135
+ self-hosted gateways (Ollama, LM Studio, vLLM, Text Embeddings
136
+ Inference, llama.cpp). Configure-time SSRF gate refuses loopback /
137
+ RFC1918 / link-local / cloud-metadata bases unless opted in with
138
+ `allow_private_endpoint: true` (emits a `Kernel#warn` audit line).
139
+ * `Parse::Embeddings::Fixture` — deterministic, zero-network. Used by
140
+ the test suite. Auto-registered under `:fixture`, no setup required.
141
+
142
+ ### Production: OpenAI
143
+
144
+ ```ruby
145
+ Parse::Embeddings.configure do |c|
146
+ c.providers[:openai] = Parse::Embeddings::OpenAI.new(
147
+ api_key: ENV.fetch("OPENAI_API_KEY"),
148
+ model: "text-embedding-3-small",
149
+ )
150
+ end
151
+ ```
152
+
153
+ The OpenAI provider self-bounds at 30 s read / 5 s connect with
154
+ capped exponential retry on 429 and 5xx. There is no implicit
155
+ wall-clock deadline imposed by `find_similar` or by the `embed`
156
+ macro — the provider is responsible for bounding its own request
157
+ time. Custom providers MUST follow the same convention.
158
+
159
+ ### Tests: Fixture
160
+
161
+ ```ruby
162
+ provider = Parse::Embeddings.provider(:fixture) # zero-config
163
+ vec = provider.embed_text(["hello"]).first # deterministic
164
+ ```
165
+
166
+ Vectors are derived from SHA-256 over `(model_name, input_type, input)`
167
+ and unit-normalized. Same input always yields the same vector;
168
+ `:search_query` and `:search_document` yield different vectors for the
169
+ same string, so cache-key bugs and input-type confusion in higher
170
+ layers surface in tests rather than only against real providers in
171
+ production.
172
+
173
+ ### Custom providers
174
+
175
+ Subclass `Parse::Embeddings::Provider` and override `embed_text`,
176
+ `dimensions`, and `model_name`. Call `instrument_embed(input_count,
177
+ input_type) { ... }` inside `embed_text` to emit the standard AS::N
178
+ event (see §Telemetry below). Always call `validate_response!` before
179
+ returning so off-by-one batches and NaN/±Inf poisoning surface as
180
+ typed `InvalidResponseError` at the provider boundary, not deep inside
181
+ a later `$vectorSearch` call.
182
+
183
+ ---
184
+
185
+ ## Creating the Atlas vectorSearch index
186
+
187
+ `find_similar` requires a deployed Atlas vectorSearch index covering
188
+ the target field. Create one via `Parse::AtlasSearch::IndexCatalog`:
189
+
190
+ ```ruby
191
+ Parse::AtlasSearch::IndexCatalog.create_index(
192
+ "Document", # Parse class / collection name
193
+ "body_embedding_v1", # index name (your choice)
194
+ {
195
+ type: "vectorSearch",
196
+ fields: [
197
+ {
198
+ type: "vector",
199
+ path: "body_embedding",
200
+ numDimensions: 1536,
201
+ similarity: "cosine",
202
+ },
203
+ # Optional: filter fields for pre-search $match acceleration.
204
+ { type: "filter", path: "tag" },
205
+ { type: "filter", path: "_rperm" },
206
+ ],
207
+ },
208
+ )
209
+ ```
210
+
211
+ Including `_rperm` as a filter field lets the per-row ACL match
212
+ short-circuit at the index level — strongly recommended for any
213
+ field that ACL-scoped agents will search against.
214
+
215
+ Index creation runs asynchronously. Use `wait_for_ready` to block
216
+ until the index is queryable:
217
+
218
+ ```ruby
219
+ Parse::AtlasSearch::IndexCatalog.wait_for_ready(
220
+ "Document", "body_embedding_v1", timeout: 600,
221
+ )
222
+ # => :ready | :failed | :timeout
223
+ ```
224
+
225
+ Auto-discovery: when `find_similar` is called without an explicit
226
+ `index:` kwarg, the catalog scans the collection's vectorSearch
227
+ indexes for one whose definition covers the requested `path`. The
228
+ first match wins; pass `index:` explicitly when you have more than
229
+ one covering index and want a specific one.
230
+
231
+ ---
232
+
233
+ ## Running similarity queries: `find_similar`
234
+
235
+ ```ruby
236
+ # Pre-computed vector
237
+ hits = Document.find_similar(vector: query_embedding, k: 10)
238
+
239
+ # Auto-embed query text using the field's declared provider
240
+ hits = Document.find_similar(text: "ruby parse stack", k: 10)
241
+
242
+ hits.first.vector_score # => Float, Atlas vectorSearchScore
243
+ hits.first.title # => String, normal Parse attribute
244
+ ```
245
+
246
+ Full kwarg surface:
247
+
248
+ * `vector:` — `Array<Float>` or `Parse::Vector`. Mutually exclusive
249
+ with `text:`.
250
+ * `text:` — `String`. Embedded with `input_type: :search_query` using
251
+ the field's declared `provider:`. Capped at 256 KiB; chunk client-
252
+ side before calling if larger.
253
+ * `k:` — number of hits to return (default 10).
254
+ * `field:` — explicit `:vector` property. Auto-resolves when the class
255
+ has exactly one; required when multiple are declared.
256
+ * `filter:` — post-`$vectorSearch` `$match`. Use for ordinary Parse-
257
+ side filtering (e.g. `{ status: "published" }`).
258
+ * `vector_filter:` — Atlas-native pre-search filter. Fields must be
259
+ declared `type: "filter"` in the index. Faster than `filter:` when
260
+ the field is filter-indexed.
261
+ * `index:` — explicit vectorSearch index name. Skips auto-discovery.
262
+ * `num_candidates:` — HNSW search width hint. Higher = better recall,
263
+ slower. Default ~10×k.
264
+ * `max_time_ms:` — server-side timeout; translates to
265
+ `Parse::MongoDB::ExecutionTimeout` on cancel.
266
+ * `raw:` — when true, return raw `BSON::Document` hashes (each carries
267
+ `_vscore`). When false (default), build `Parse::Object` instances.
268
+ * `session_token:` / `master:` / `acl_user:` / `acl_role:` — scope
269
+ kwargs forwarded to the underlying `Parse::MongoDB.aggregate` so the
270
+ 5-layer enforcement (denylist, ACL `_rperm` match, CLP,
271
+ protectedFields, master-key escape) runs against the result rows.
272
+
273
+ ### Dimension validation
274
+
275
+ `find_similar` compares the query vector's length to the property's
276
+ declared `dimensions:` before sending the pipeline. A mismatch raises
277
+ `Parse::VectorSearch::InvalidQueryVector` locally, before Atlas sees
278
+ it — callers get "expected 1536, got 768" instead of a server-side
279
+ error after a round-trip.
280
+
281
+ ### ACL/CLP inheritance
282
+
283
+ Vector search routes through `Parse::MongoDB.aggregate`. Every layer
284
+ documented in [mongodb_direct_guide.md §Security](./mongodb_direct_guide.md#security)
285
+ applies to vector search result rows too:
286
+
287
+ 1. Pipeline-security denylist (always on).
288
+ 2. Row-level ACL `_rperm` match — scoped agents only.
289
+ 3. CLP read enforcement — scoped agents only.
290
+ 4. `protectedFields` stripping — scoped agents only.
291
+ 5. Master-key escape hatch.
292
+
293
+ **REST `/aggregate` is NOT a valid path for vector search with a
294
+ scoped caller.** Parse Server's REST aggregate endpoint is master-
295
+ key-only and would bypass every per-row ACL and CLP check. The built-
296
+ in agent tools auto-promote `mongo_direct: false` to `true` for any
297
+ agent carrying `session_token`, `acl_user`, `acl_role`, or a non-
298
+ master scope so this enforcement always runs.
299
+
300
+ ---
301
+
302
+ ## Managing embeddings on write: `embed` macro
303
+
304
+ The `embed` class macro declares which source fields feed a managed
305
+ vector. The embedding is recomputed automatically on save whenever
306
+ the source fields change.
307
+
308
+ ```ruby
309
+ class Document < Parse::Object
310
+ property :title, :string
311
+ property :body, :string
312
+ property :body_embedding, :vector, dimensions: 1536, provider: :openai
313
+
314
+ embed :title, :body, into: :body_embedding
315
+ end
316
+
317
+ doc = Document.new(title: "hello", body: "world")
318
+ doc.save # provider :openai called once; body_embedding populated
319
+
320
+ doc.body = "updated body"
321
+ doc.save # provider called again; new embedding written
322
+
323
+ doc.save # no source field changed → zero provider calls
324
+ ```
325
+
326
+ Mechanics:
327
+
328
+ * A `<into>_digest` `:string` sibling field is auto-declared (override
329
+ with `digest_field:`). The before_save callback computes SHA-256 over
330
+ the concatenated source text; if it matches the stored digest AND
331
+ the target vector is non-nil, the callback returns without
332
+ contacting the provider.
333
+ * The target `:vector` property is **write-protected**. Direct
334
+ assignment (`doc.body_embedding = some_vector`) raises
335
+ `ProtectedFieldError`. The guard lifts only inside the managed
336
+ write path. This prevents silent desync between the stored vector
337
+ and the digest.
338
+ * Source fields are concatenated with `"\n\n"`, `nil` and blank values
339
+ skipped. If every source is blank, the target and digest are both
340
+ cleared on save.
341
+
342
+ ### Single vector per record (v5.0)
343
+
344
+ `embed` produces exactly one vector per record. There is no built-in
345
+ chunker. Long source text whose concatenation exceeds the provider's
346
+ per-call token budget will be truncated provider-side, and the
347
+ resulting vector will represent only the leading portion of the
348
+ document.
349
+
350
+ For long-form content in v5.0, two options:
351
+
352
+ 1. **Pre-chunk client-side** and write each chunk as its own
353
+ `Parse::Object` record with its own `embed` declaration.
354
+ 2. **Dedicated `Chunk` subclass** that `belongs_to` the parent, with
355
+ `embed :content, into: :embedding` on the chunk class itself. Run
356
+ similarity search against the chunk collection, then hydrate
357
+ parents as needed.
358
+
359
+ A built-in chunker plus a `semantic_search` agent tool are scheduled
360
+ for v5.1.
361
+
362
+ ### Re-embedding existing rows
363
+
364
+ Changing `model:`, `dimensions:`, or `provider:` on an existing
365
+ `:vector` property is a migration. Workflow:
366
+
367
+ 1. Add the new property alongside the old one
368
+ (`property :body_embedding_v2, :vector, ...`) and an `embed` block
369
+ targeting it.
370
+ 2. Backfill: iterate existing rows, force a save (or null+save) to
371
+ trigger the new directive. The old field stays valid for reads.
372
+ 3. Once backfill completes, deploy a new vectorSearch index covering
373
+ the new field and migrate `find_similar` callers.
374
+ 4. Drop the old property.
375
+
376
+ Do NOT mutate the model in place — the digest mechanism will see
377
+ unchanged source text and skip recompute, leaving stale vectors.
378
+
379
+ ---
380
+
381
+ ## Telemetry: `parse.embeddings.embed` AS::N
382
+
383
+ Every provider emits `parse.embeddings.embed` via
384
+ `ActiveSupport::Notifications.instrument`. Subscribe to track cost,
385
+ latency, and error rate across all embedding spend:
386
+
387
+ ```ruby
388
+ ActiveSupport::Notifications.subscribe("parse.embeddings.embed") do |*args|
389
+ event = ActiveSupport::Notifications::Event.new(*args)
390
+ StatsD.increment(
391
+ "parse.embeddings.embed",
392
+ tags: [
393
+ "provider:#{event.payload[:provider]}",
394
+ "model:#{event.payload[:model]}",
395
+ "input_type:#{event.payload[:input_type]}",
396
+ "error:#{event.payload[:error] || 'none'}",
397
+ ],
398
+ )
399
+ StatsD.histogram("parse.embeddings.tokens", event.payload[:total_tokens]) if event.payload[:total_tokens]
400
+ StatsD.timing("parse.embeddings.duration_ms", event.duration)
401
+ end
402
+ ```
403
+
404
+ Payload contract (keys always present; values may be nil):
405
+
406
+ | Key | Type | Notes |
407
+ | -------------- | ------------- | ----------------------------------------------------------------------- |
408
+ | `:provider` | `String` | `provider.class.name` (e.g. `"Parse::Embeddings::OpenAI"`) |
409
+ | `:model` | `String` | `provider.model_name` |
410
+ | `:dimensions` | `Integer` | `provider.dimensions` |
411
+ | `:input_count` | `Integer` | batch size |
412
+ | `:input_type` | `Symbol` | `:search_query` / `:search_document` |
413
+ | `:total_tokens`| `Integer`/nil | provider-reported usage; nil for Fixture and providers without usage |
414
+ | `:cached` | `Boolean` | always false in v5.0; reserved for v5.1 embed cache |
415
+ | `:error` | `String`/nil | `exception.class.name` when the block raised — class name only |
416
+
417
+ Notes:
418
+
419
+ * `:error` is the **class name**, never the message. Provider
420
+ exceptions can contain user-supplied text from the API; surfacing
421
+ only the class name keeps PII out of operator dashboards.
422
+ * Pre-validation failures (`embed_text` called with non-Array, or
423
+ with non-String elements) do **not** emit an event. The validation
424
+ runs before the instrument block so caller-shape errors aren't
425
+ recorded as embed attempts.
426
+ * Subscribers run **synchronously on the request thread**. A slow
427
+ subscriber blocks every embed call. Push to non-blocking sinks
428
+ (StatsD-over-UDP, batched OTel exporters) rather than doing
429
+ filesystem or HTTP I/O inside the subscriber.
430
+
431
+ ---
432
+
433
+ ## Logging and PII considerations
434
+
435
+ When `find_similar(text:)` is called, the query text is sent over the
436
+ wire to the embedding provider. Operators with global Faraday request
437
+ logging enabled on the embedding connection will capture the full
438
+ query text in the JSON request body. Treat `text:` as user-visible
439
+ content for log-handling purposes; redact at the Faraday middleware
440
+ layer if your logging pipeline retains payloads.
441
+
442
+ The vector itself never appears in OpenAI request bodies (text in,
443
+ floats out). Vectors only flow through the Parse↔Mongo path, where
444
+ the body builder's `<vector dims=N>` compaction prevents them from
445
+ landing in stdout / error trackers.
446
+
447
+ ---
448
+
449
+ ## Troubleshooting
450
+
451
+ **`NoVectorProperty: no :vector property declared on this class`**
452
+ The class has no field declared as `:vector`. Add one.
453
+
454
+ **`AmbiguousVectorField: class declares multiple :vector properties`**
455
+ Pass `field: :which_one` to disambiguate.
456
+
457
+ **`IndexNotResolved: no vectorSearch index found covering Class.field`**
458
+ Create the index (see §Creating the Atlas vectorSearch index) or pass
459
+ `index:` explicitly.
460
+
461
+ **`InvalidQueryVector: expected 1536, got 768`**
462
+ The query vector's length doesn't match the declared `dimensions:`.
463
+ Almost always means the query embedding came from a different model
464
+ than the stored embeddings.
465
+
466
+ **`EmbedderNotConfigured`**
467
+ The `:vector` property has no `provider:` declared but `find_similar`
468
+ was called with `text:`. Either declare a provider on the property, or
469
+ pass an explicit `vector:` Array.
470
+
471
+ **`ProtectedFieldError: <Class>#<field> is managed by 'embed'`**
472
+ User code tried to assign directly to a managed vector field. Update
473
+ the declared source fields instead and save.
474
+
475
+ **`InvalidResponseError: response length 5 != input count 4`**
476
+ The provider returned a different number of vectors than inputs. The
477
+ provider has a bug — the validation in
478
+ `Parse::Embeddings::Provider#validate_response!` caught it before the
479
+ misaligned vectors could be stored.
480
+
481
+ **Atlas Local: index stays `BUILDING` forever**
482
+ Atlas Local's internal supervisor periodically restarts `mongod`
483
+ during replica-set sync. Use `IndexCatalog.wait_for_ready` (which
484
+ bypasses the IndexManager's 300-second cache via `force_refresh: true`
485
+ on every poll) rather than a `until index_ready?; sleep` loop.
486
+
487
+ ---
488
+
489
+ ## Reference
490
+
491
+ Key files:
492
+
493
+ * `lib/parse/embeddings.rb` — registry, `Configuration`, `register`,
494
+ `provider`, `configure`.
495
+ * `lib/parse/embeddings/provider.rb` — abstract base, `validate_response!`,
496
+ `instrument_embed`, AS::N payload contract.
497
+ * `lib/parse/embeddings/openai.rb` — OpenAI provider.
498
+ * `lib/parse/embeddings/cohere.rb` — Cohere v3 + v4.0 text-mode provider.
499
+ * `lib/parse/embeddings/voyage.rb` — Voyage text + multimodal-3
500
+ text-mode provider.
501
+ * `lib/parse/embeddings/jina.rb` — Jina v3 / v4 / v5 / code provider.
502
+ * `lib/parse/embeddings/qwen.rb` — Qwen3-Embedding via DashScope.
503
+ * `lib/parse/embeddings/local_http.rb` — generic OpenAI-compatible
504
+ local-gateway client.
505
+ * `lib/parse/embeddings/fixture.rb` — deterministic test provider.
506
+ * `lib/parse/model/core/vector_searchable.rb` — `find_similar`.
507
+ * `lib/parse/model/core/embed_managed.rb` — `embed` macro.
508
+ * `lib/parse/vector_search.rb` — low-level `Parse::VectorSearch.search`.
509
+ * `lib/parse/atlas_search/index_manager.rb` — `IndexCatalog.create_index`,
510
+ `find_vector_index`, `wait_for_ready`.
511
+ * `lib/parse/mongodb.rb` — direct MongoDB access, 5-layer enforcement.