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.
- checksums.yaml +4 -4
- data/.bundle/config +2 -0
- data/.env.sample +17 -3
- data/.github/workflows/codeql.yml +44 -0
- data/.github/workflows/docs.yml +39 -0
- data/.github/workflows/release.yml +32 -0
- data/.github/workflows/ruby.yml +8 -6
- data/.gitignore +4 -0
- data/.vscode/settings.json +3 -0
- data/CHANGELOG.md +305 -72
- data/Gemfile.lock +10 -3
- data/LICENSE.txt +1 -1
- data/README.md +190 -219
- data/Rakefile +1 -1
- data/SECURITY.md +30 -0
- data/assets/parse-stack-next-avatar.png +0 -0
- data/assets/parse-stack-next-avatar.svg +37 -0
- data/assets/parse-stack-next-banner.png +0 -0
- data/assets/parse-stack-next-banner.svg +45 -0
- data/assets/parse-stack-next-social-preview.png +0 -0
- data/docs/atlas_vector_search_guide.md +511 -0
- data/docs/client_sdk_guide.md +1320 -0
- data/docs/mcp_guide.md +225 -104
- data/docs/mongodb_direct_guide.md +21 -4
- data/docs/usage_guide.md +585 -0
- data/examples/transaction_example.rb +28 -28
- data/lib/parse/acl_scope.rb +2 -2
- data/lib/parse/agent/mcp_rack_app.rb +184 -16
- data/lib/parse/agent/metadata_dsl.rb +16 -16
- data/lib/parse/agent/pipeline_validator.rb +28 -1
- data/lib/parse/agent/prompts.rb +5 -5
- data/lib/parse/agent/tools.rb +287 -14
- data/lib/parse/agent.rb +209 -12
- data/lib/parse/api/analytics.rb +27 -5
- data/lib/parse/api/files.rb +6 -2
- data/lib/parse/api/push.rb +21 -4
- data/lib/parse/api/server.rb +59 -0
- data/lib/parse/api/users.rb +26 -2
- data/lib/parse/atlas_search/index_manager.rb +84 -0
- data/lib/parse/atlas_search.rb +37 -9
- data/lib/parse/cache/pool.rb +88 -0
- data/lib/parse/cache/redis.rb +249 -0
- data/lib/parse/client/body_builder.rb +94 -0
- data/lib/parse/client/caching.rb +109 -9
- data/lib/parse/client/response.rb +27 -0
- data/lib/parse/client.rb +74 -3
- data/lib/parse/console.rb +203 -0
- data/lib/parse/embeddings/cohere.rb +484 -0
- data/lib/parse/embeddings/fixture.rb +130 -0
- data/lib/parse/embeddings/jina.rb +454 -0
- data/lib/parse/embeddings/local_http.rb +492 -0
- data/lib/parse/embeddings/openai.rb +520 -0
- data/lib/parse/embeddings/provider.rb +264 -0
- data/lib/parse/embeddings/qwen.rb +431 -0
- data/lib/parse/embeddings/voyage.rb +550 -0
- data/lib/parse/embeddings.rb +225 -0
- data/lib/parse/graphql/scalars.rb +53 -0
- data/lib/parse/graphql/type_generator.rb +264 -0
- data/lib/parse/graphql.rb +48 -0
- data/lib/parse/live_query/client.rb +24 -5
- data/lib/parse/live_query/subscription.rb +17 -6
- data/lib/parse/live_query.rb +9 -4
- data/lib/parse/model/associations/collection_proxy.rb +2 -2
- data/lib/parse/model/associations/has_many.rb +32 -1
- data/lib/parse/model/associations/has_one.rb +17 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +3 -3
- data/lib/parse/model/classes/user.rb +307 -11
- data/lib/parse/model/clp.rb +1 -1
- data/lib/parse/model/core/create_lock.rb +14 -2
- data/lib/parse/model/core/embed_managed.rb +296 -0
- data/lib/parse/model/core/fetching.rb +4 -4
- data/lib/parse/model/core/indexing.rb +53 -14
- data/lib/parse/model/core/parse_reference.rb +3 -3
- data/lib/parse/model/core/properties.rb +70 -1
- data/lib/parse/model/core/querying.rb +57 -1
- data/lib/parse/model/core/vector_searchable.rb +285 -0
- data/lib/parse/model/file.rb +16 -4
- data/lib/parse/model/model.rb +26 -10
- data/lib/parse/model/object.rb +63 -6
- data/lib/parse/model/pointer.rb +16 -2
- data/lib/parse/model/shortnames.rb +2 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +3 -3
- data/lib/parse/model/vector.rb +102 -0
- data/lib/parse/mongodb.rb +90 -8
- data/lib/parse/pipeline_security.rb +59 -2
- data/lib/parse/query/constraints.rb +16 -14
- data/lib/parse/query/ordering.rb +1 -1
- data/lib/parse/query.rb +137 -64
- data/lib/parse/stack/generators/templates/model.erb +2 -2
- data/lib/parse/stack/generators/templates/model_installation.rb +1 -1
- data/lib/parse/stack/generators/templates/model_role.rb +1 -1
- data/lib/parse/stack/generators/templates/model_session.rb +1 -1
- data/lib/parse/stack/generators/templates/parse.rb +1 -1
- data/lib/parse/stack/generators/templates/webhooks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +375 -73
- data/lib/parse/two_factor_auth/user_extension.rb +5 -2
- data/lib/parse/vector_search.rb +341 -0
- data/parse-stack-next.gemspec +10 -9
- data/scripts/docker/docker-compose.test.yml +18 -0
- data/scripts/start-parse.sh +6 -0
- data/scripts/vector_prototype/create_vector_index.js +105 -0
- data/scripts/vector_prototype/fetch_embeddings.py +241 -0
- data/scripts/vector_prototype/fixture_manifest.json +9 -0
- data/scripts/vector_prototype/query_prototype.rb +84 -0
- data/scripts/vector_prototype/run.sh +34 -0
- metadata +77 -5
- data/parse-stack.png +0 -0
data/Rakefile
CHANGED
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>
|
|
Binary file
|
|
@@ -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.
|