Skip to content

Document $vectorSearch, $graphLookup, $project, and $limit - #59

Merged
guanzhousongmicrosoft merged 5 commits into
documentdb:mainfrom
GuanzhouSong:add-missing-aggregation-stages
Aug 3, 2026
Merged

Document $vectorSearch, $graphLookup, $project, and $limit#59
guanzhousongmicrosoft merged 5 commits into
documentdb:mainfrom
GuanzhouSong:add-missing-aggregation-stages

Conversation

@GuanzhouSong

@GuanzhouSongGuanzhouSong commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Relates to documentdb/documentdb.github.io#130. First slice of the missing-stage gap; the remaining 10 stages are still undocumented.

Why these four

The MQL reference documents 25 aggregation stages. pg_documentdb registers 40 public ones (StageDefinitions[] in bson_aggregation_pipeline.c holds 41 entries, one of which is $_internalInhibitOptimization), so 14 have no page at all.

These four are the ones a reader hits first:

  • $vectorSearch — promoted on the documentdb.io /ai landing page, and getting-started/python-setup.md ships a working $vectorSearch pipeline for a stage that has no reference page. A reader who follows the AI page into getting-started and then wants the operator's parameters currently has nowhere to land.
  • $graphLookup — also promoted on /ai: "traverse related entities with $graphLookup in one query pipeline".
  • $project and $limit — core stages.

How the content was derived

Parameters, defaults, and constraints come from the upstream implementation, not from MongoDB's docs:

  • ParseAndValidateNativeVectorSearchSpec (bson_aggregation_search.c) — the accepted $vectorSearch fields, the numCandidates bounds (HNSW_MIN_EF_SEARCH..HNSW_MAX_EF_SEARCH = 1..1000), the 16000-element cap on queryVector, and the fact that index is parsed but ignored.
  • ParseGraphLookupStage (bson_aggregation_nested_pipeline.c) — the eight accepted $graphLookup parameters, which five are required, and the unbounded maxDepth default.
  • HandleLimit (bson_aggregation_pipeline.c) — must be numeric, 64-bit representable, non-negative, non-zero.
    Corrected in review: the minimum rule holds only for adjacent $limit stages. HandleLimit sets requiresSubQuery, so stages nest rather than merge, and a document-multiplying stage between two $limits breaks it.
  • bson_projection_tree.c — the inclusion/exclusion mixing rule and the top-level _id exemption.

Verification

Every example and every documented error message was executed against documentdb-local and its real output recorded. A clean-room replay then re-ran the whole set on a fresh database, asserting each documented result and error string:

--- $project --- 7 assertions
--- $limit --- 6 assertions
--- $graphLookup --- 8 assertions
--- $vectorSearch --- 14 assertions
RESULT: 35 passed, 0 failed

Every output block was produced by the engine, with one exception added during the review round: the unprojected $vectorSearch result showing __cosmos_meta__ is derived from AddScoreFieldToDocumentEntry and the upstream expected-output files rather than from a run.

Two caveats on the numbers above. The suite predates the review round and has not been re-run against the current text. And it did not catch the $project Example 3 defect, because the assertion was written against the same fabricated sample document as the page — a replay is only as good as its fixture.

One finding worth flagging

$vectorSearch.filter requires an index on each field used in the filter, not just on the embedding path. Without it the query fails outright rather than falling back to a scan:

The index for filter path 'category' was not found, please check whether the index is created.

The /ai landing page's $vectorSearch sample includes filter: { category: "technical" } with no mention of this, so it is the kind of thing a reader will hit immediately. The $vectorSearch page creates the filter index before using it, and documents the error.

Also, numCandidates is documented as choosing a default from the index's efConstruction and the collection size rather than a flat 40 — GetDefaultSearchParamForIndex picks between the two depending on EnableVectorCalculateDefaultSearchParameter and whether the collection is under the small-collection row threshold.

Sharpened in review: that threshold is a cliff, not a curve. efConstruction feeds the default only below VECTOR_SEARCH_SMALL_COLLECTION_ROWS (10,000); at or above it the default is a flat HNSW_DEFAULT_EF_SEARCH of 40, so an index built with efConstruction: 512 silently searches at 40 on a 50,000-document collection. The page now states the threshold and recommends setting numCandidates by hand on production-sized collections.

Review round

A source-validated review against documentdb upstream raised 22 findings; all are addressed in 21a7838, 8ee5fba and 0daa22a, and every thread is resolved with the specific change noted inline.

The substantive corrections, in rough order of how badly they would have misled a reader:

  • $vectorSearch returns an undocumented __cosmos_meta__ field on every document. Every example happened to hide it behind an inclusion $project, and an exclusion projection does not strip it — so an application round-tripping results would have persisted it silently.
  • filter was described as a true pre-filter with a limit guarantee. The engine intersects an ANN scan with a separate filter query and applies limit to the join, so reaching limit is best-effort and depends on pgvector ≥ 0.8.0 iterative scan.
  • $project Example 3's output was wrong, because the sample document invented a tags field and dropped promotionEvents. Now uses the canonical fixture.
  • $graphLookup claimed the as array order is undefined while publishing three exact arrays as results. The page now documents the dedup-to-lowest-depth guarantee it was hiding.
  • Undocumented hard failures:$graphLookupfrom cannot be sharded, $vectorSearchfilter is rejected on sharded collections, and restrictSearchWithMatch rejects $near / $nearSphere / $geoNear.
  • Index constraints that were never stated:dimensions is capped at 2000 without compression, not 16000 — which is exactly the wall a reader with 3072-dimension embeddings hits; and efConstruction must be at least 2 * m.
  • numCandidates on vector-ivf does nothing — accepted and validated, then dropped, because the IVF path reads only nProbes.

Also corrected outside the original diff: $meta.md claimed { $meta: "indexKey" } returns nothing without an index; it is rejected unconditionally (0daa22a).

One review candidate was refuted rather than applied — a claimed inclusion/exclusion message swap, which traces to an unreachable ternary arm in bson_projection_tree.c.

Conventions

Pages follow the existing $addfields.md structure — frontmatter, intro, ## Syntax, ## Parameters, ## Examples with sample data, and a closing ## Related. Filenames are lowercase to match the existing slugs. Every cross-link target was checked to exist. Corrected in review: the targets existed, but the link form was wrong — ../%24limit/ resolves one directory too high, into a path that holds only category folders. All 9 now use the same-directory ./%24name.md form taken from the $bucketauto.md precedent. The repo has no link-checking CI, so this would have shipped silently.

Still missing after this PR

$replaceRoot, $setWindowFields, $unionWith, $search, $currentOp, $inverseMatch, plus four that arguably do not warrant public pages: $searchMeta, $listLocalSessions, and $listSessions are registered with .mutateFunc = NULL and error unconditionally, and $listSearchIndexes is gated off by default (DEFAULT_ENABLE_EXTENDED_INDEXES is false).

Note also that $replaceWith is documented while $replaceRoot is not — in MongoDB $replaceWith is the alias, so the reference currently documents the alias and omits the canonical form. Upstream gives them separate handlers rather than treating them as aliases.

The MQL reference documents 25 aggregation stages; pg_documentdb
registers 40 public ones, so 14 have no page at all. This adds the four
highest-value: $vectorSearch and $graphLookup are both marketed on the
documentdb.io /ai landing page, and getting-started/python-setup.md ships
a working $vectorSearch pipeline for a stage with nowhere to land.
$project and $limit are core stages a reader will look for first.
Parameters, defaults, and constraints come from the upstream
implementation - ParseAndValidateNativeVectorSearchSpec and
ParseGraphLookupStage for the accepted-field lists, HandleLimit for the
limit rules, and bson_projection_tree.c for the inclusion/exclusion
rule. Every example and every documented error message was executed
against documentdb-local and its real output recorded: 35 assertions,
all passing on a clean database.
Worth calling out because it is easy to get wrong and the /ai landing
page glosses over it: $vectorSearch.filter requires an index on each
field used in the filter, not just on the embedding path. Without it the
query fails rather than falling back to a scan, so the $vectorSearch
page creates the filter index before using it.
numCandidates is documented as choosing a default from the index
efConstruction and collection size rather than a flat 40 -
GetDefaultSearchParamForIndex picks between the two.
Relates to documentdb/documentdb.github.io#130; the remaining 10 stages
are still undocumented.
@GuanzhouSongGuanzhouSong changed the title Document \, \, \, and \Document $vectorSearch, $graphLookup, $project, and $limitAug 3, 2026
The parameter table said the query vector length must match the index
dimensions, but that was the one claim on the page not backed by an
executed check. Verified against documentdb-local - a 2- or 4-element
vector against a 3-dimension index fails with "expected 3 dimensions,
not 2" - and added the message to the error table.
Comment threadapi-reference/operators/aggregation/$project.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$limit.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$graphlookup.md Outdated
Comment threadapi-reference/operators/aggregation/$graphlookup.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$graphlookup.md Outdated
Comment threadapi-reference/operators/aggregation/$graphlookup.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md
Comment threadapi-reference/operators/aggregation/$project.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md
Comment threadapi-reference/operators/aggregation/$vectorsearch.md
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$vectorsearch.md Outdated
Comment threadapi-reference/operators/aggregation/$graphlookup.md Outdated
Comment threadapi-reference/operators/aggregation/$project.md
Correct claims that the upstream implementation contradicts, and document
constraints and behaviors that the pages omitted.
$vectorSearch:
- Rewrite the filter description: the engine intersects an ANN scan with a
separate filter query rather than restricting the search beforehand, so
reaching limit is best-effort and depends on pgvector iterative scan.
- Document the __cosmos_meta__ field attached to every result, and that an
exclusion projection does not strip it.
- Document the numCandidates default cliff at 10000 rows, and that the
parameter has no effect on vector-ivf indexes.
- Document the compression-dependent dimensions cap, the m/efConstruction/
numLists ranges, and the efConstruction >= 2 * m coupling.
- Raise Example 4 numCandidates above the default it was meant to exceed.
- Note the BSON int32 wire-type requirement on limit and numCandidates.
- Add missing error rows and the vectorSearchScore alias.
$graphLookup:
- Extend the $-prefix rule to connectFromField and connectToField.
- Document the sharded from restriction and the rejection of $near,
$nearSphere, and $geoNear in restrictSearchWithMatch.
- Replace the as-array ordering disclaimer, which contradicted both the
implementation and the page's own outputs, with the dedup-by-lowest-depth
guarantee the engine actually provides.
- Restore the non-breaking hyphen in the maxDepth error string.
$project:
- Replace the fabricated tags field with the canonical promotionEvents so
Example 3's output matches the real document.
- Document $$REMOVE, the top-level-only _id exemption, and the DBRef
exception to the $-prefix rule.
$limit:
- Scope the smallest-value-wins rule to adjacent stages.
$meta:
- Add the searchScore and vectorSearchScore keywords and an aggregation
example, resolving the contradiction with the $vectorSearch page.
Fix nine Related links that resolved one directory too high.
Both were nested under sections they do not belong to: the numCandidates
guidance sat inside Examples, and $graphLookup's sharded-collection
restriction sat after Error cases rather than before it. Section anchors
are unchanged.
The page said indexKey returns nothing when no index is used. There is a
single $meta handler and it rejects indexKey unconditionally, so the
keyword is simply unsupported. State that, with the error the server
raises.
@guanzhousongmicrosoft
guanzhousongmicrosoft merged commit 0f40f45 into documentdb:mainAug 3, 2026
guanzhousongmicrosoft pushed a commit to richardsimmonds/docs that referenced this pull request Aug 3, 2026
Rebasing this branch onto main will conflict: $graphLookup, $limit,
$project, and $vectorSearch were all documented in documentdb#59 while this pull
request was open, so those four files are add/add conflicts against
pages that already exist. Removed them here. The remaining six stages
are still missing from the reference and are what this branch adds.
$search needed more than a rebase - it documents the wrong feature. The
page describes full-text search over a text index, with an example
built on a text operator:
$search: { text: { query: "Beverage", path: "name" } }
DocumentDB's $search is a vector search stage. Its operator registry has
exactly two entries, cosmosSearch and knnBeta (deprecated), both vector
operators, and there is no text operator anywhere in the search sources.
The example above does not return no results - it fails outright with
"Unrecognized $search option: text", and the page's stated requirement
of a text index points at the wrong index type entirely.
Rewritten against the stage's actual parsing: the two operators, the
options carried alongside them (index, count, returnStoredSource), and
the operator spec itself (path, vector, k, filter, exact, oversampling,
score), including which of those knnBeta rejects and which require
server-side pre-filtering to be enabled. count is documented as having
no observable effect, since the engine parses it but does not yet emit
the metadata. The page now points at $vectorSearch as the stage to
prefer for new queries, and notes $text as the answer for the full-text
case the original page was reaching for.
The other five pages were checked against the engine and left alone.
$currentOp in particular is accurate: its five options are real, and its
first-stage and admin-database requirements match the handler.
guanzhousongmicrosoft pushed a commit that referenced this pull request Aug 3, 2026
Adds reference pages for $currentOp, $replaceRoot, $search, $setWindowFields, and $unionWith, which had none.
Continues the work in #47. The claims on these pages are derived from the
engine source rather than from MongoDB's documentation, which is where the
original divergences came from: $search documented Atlas full-text search
where DocumentDB's stage is vector-only, and $setWindowFields showed a
window template that the parser rejects. $count was dropped as a duplicate
of accumulators/$count.md, and $graphLookup, $limit, $project, and
$vectorSearch were dropped after #59 documented them.
Co-authored-by: richardsimmonds <richardsimmonds314@gmail.com>
guanzhousongmicrosoft pushed a commit that referenced this pull request Aug 3, 2026
Seventeen links across six pages point at relative .md paths and every
one of them 404s. On $search the link to $vectorSearch renders as
href="./%24vectorsearch.md", which resolves against the page's own
directory - documentdb.io serves these with a trailing slash - and lands
at /operators/aggregation/$search/%24vectorsearch.md. Wrong depth and a
leaked extension, the two failure modes #57 catalogued, in the same
href.
They were introduced together. $vectorSearch, $project, $limit and
$graphLookup arrived in #59, $search in #64 following the convention it
found on the page next to it, and $meta links back to $vectorSearch the
same way. None of them render, so the pages read as cross-linked while
every cross-link is dead.
Rewritten to the absolute form the rest of the reference already uses -
https://documentdb.io/docs/reference/operators/aggregation/%24bucket/ -
which is what #57 settled on for exactly this reason: it does not depend
on how the site resolves a relative path, and it survives a page moving
between directories.
All thirteen distinct targets were requested against the live site and
return 200, and no relative .md link remains anywhere in the repository.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@GuanzhouSong@guanzhousongmicrosoft