Uh oh!
There was an error while loading. Please reload this page.
Document $vectorSearch, $graphLookup, $project, and $limit - #59
Conversation
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.
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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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.
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.
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>
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.
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_documentdbregisters 40 public ones (StageDefinitions[]inbson_aggregation_pipeline.cholds 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/ailanding page, andgetting-started/python-setup.mdships a working$vectorSearchpipeline 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$graphLookupin one query pipeline".$projectand$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$vectorSearchfields, thenumCandidatesbounds (HNSW_MIN_EF_SEARCH..HNSW_MAX_EF_SEARCH= 1..1000), the 16000-element cap onqueryVector, and the fact thatindexis parsed but ignored.ParseGraphLookupStage(bson_aggregation_nested_pipeline.c) — the eight accepted$graphLookupparameters, which five are required, and the unboundedmaxDepthdefault.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
$limitstages.HandleLimitsetsrequiresSubQuery, 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_idexemption.Verification
Every example and every documented error message was executed against
documentdb-localand 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:Every output block was produced by the engine, with one exception added during the review round: the unprojected
$vectorSearchresult showing__cosmos_meta__is derived fromAddScoreFieldToDocumentEntryand 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
$projectExample 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.filterrequires 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
/ailanding page's$vectorSearchsample includesfilter: { category: "technical" }with no mention of this, so it is the kind of thing a reader will hit immediately. The$vectorSearchpage creates the filter index before using it, and documents the error.Also,
numCandidatesis documented as choosing a default from the index'sefConstructionand the collection size rather than a flat 40 —GetDefaultSearchParamForIndexpicks between the two depending onEnableVectorCalculateDefaultSearchParameterand whether the collection is under the small-collection row threshold.Sharpened in review: that threshold is a cliff, not a curve.
efConstructionfeeds the default only belowVECTOR_SEARCH_SMALL_COLLECTION_ROWS(10,000); at or above it the default is a flatHNSW_DEFAULT_EF_SEARCHof 40, so an index built withefConstruction: 512silently searches at 40 on a 50,000-document collection. The page now states the threshold and recommends settingnumCandidatesby hand on production-sized collections.Review round
A source-validated review against
documentdbupstream 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:
$vectorSearchreturns 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.filterwas described as a true pre-filter with alimitguarantee. The engine intersects an ANN scan with a separate filter query and applieslimitto the join, so reachinglimitis best-effort and depends on pgvector ≥ 0.8.0 iterative scan.$projectExample 3's output was wrong, because the sample document invented atagsfield and droppedpromotionEvents. Now uses the canonical fixture.$graphLookupclaimed theasarray order is undefined while publishing three exact arrays as results. The page now documents the dedup-to-lowest-depth guarantee it was hiding.$graphLookupfromcannot be sharded,$vectorSearchfilteris rejected on sharded collections, andrestrictSearchWithMatchrejects$near/$nearSphere/$geoNear.dimensionsis capped at 2000 withoutcompression, not 16000 — which is exactly the wall a reader with 3072-dimension embeddings hits; andefConstructionmust be at least2 * m.numCandidatesonvector-ivfdoes nothing — accepted and validated, then dropped, because the IVF path reads onlynProbes.Also corrected outside the original diff:
$meta.mdclaimed{ $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.mdstructure — frontmatter, intro,## Syntax,## Parameters,## Exampleswith 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.mdform taken from the$bucketauto.mdprecedent. 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$listSessionsare registered with.mutateFunc = NULLand error unconditionally, and$listSearchIndexesis gated off by default (DEFAULT_ENABLE_EXTENDED_INDEXESisfalse).Note also that
$replaceWithis documented while$replaceRootis not — in MongoDB$replaceWithis 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.