02 · How Atlas Search Scores a Vehicle
This file is the end-to-end mental model. If you understand this, every other file makes sense.
#The 3-layer architecture
┌─────────────────────────────────────────────────────────────┐│ MongoDB Atlas ││ ├── MongoDB (the database) ││ └── Atlas Search (a Lucene engine sidecar) ││ ││ When Odyssey sends a pipeline starting with $search, ││ Atlas routes execution to the Lucene engine instead of ││ the normal MongoDB query planner. │└─────────────────────────────────────────────────────────────┘
- Lucene is the search engine. It computes scores per document.
- BM25 is Lucene's text-relevance algorithm — only matters when the customer typed a search term. For "Sort by Lowest Price" with no query, BM25 contributes 0.
- Score blocks (NearBlock, gauss, etc.) are the numeric-scoring rules
YOU write in the
searchScoreWeightscollection. These are what create the sort order for non-text queries.
#What happens on every search request
1. Customer clicks "Sort by Lowest Price" on the SRP│▼2. Frontend GraphQL query → Odyssey search service│▼3. SearchPageQueries::search() calls PipelineBuilder│▼4. PipelineBuilder asks SearchOperatorBuilder for the $search stage│▼5. SearchOperatorBuilder reads the score weights doc fromSearchScoreWeightsCache (the doc with enabled=true forthe requested sortType)│▼6. SearchOperatorBuilder walks the doc's sortFactors and emitsBSON, swapping placeholders like #USER_STATE# with thecustomer's actual state (e.g. "CA")│▼7. Final pipeline: [$search, $project, $skip, $limit]│▼8. Reactive Mongo driver sends pipeline → Atlas → Lucene│▼9. Lucene scores every candidate vehicle, sorts by score│▼10. Result IDs come back → mongod fetches docs → GraphQL → SRP cards
The key thing: Odyssey never computes scores. It reads a recipe from
Mongo, makes one substitution (#USER_STATE#), and ships the recipe to
Atlas. Atlas does the math.
#How a single vehicle gets scored
For each vehicle, Lucene walks the staticFactors array of the score
weights doc. Every entry produces a number. Those numbers get added.
For the v3 LOW_PRICE_USED_CAR doc that's enabled in UAT today, there are
two staticFactors:
staticFactors = [Item 1: PRICE BLOCK(in-state vs out-of-state decision + Near scoring oninStatePriceWithFees or outStatePriceWithFees)Item 2: AVAILABILITY GATE(+10,000 constant bonus if all 4 availability flags are false)]
Final score = Item 1 score + Item 2 score.
#The "branches" pattern
Inside Item 1 (the price block), there's a CompoundBlock with
minimumShouldMatch: 1 and a should array containing 2 nested
CompoundBlocks. Each nested block is called a "branch."
CompoundBlock (outer, minimumShouldMatch: 1)│├── Branch A — In-State│ filter: dealer.state == '#USER_STATE#'│ must: NearBlock(inStatePriceWithFees, origin=0, pivot=30000) × 1000│└── Branch B — Out-of-StatemustNot: dealer.state == '#USER_STATE#'must: NearBlock(outStatePriceWithFees, origin=0, pivot=30000) × 1000
Every vehicle fires exactly one branch:
- Customer in CA + dealer in CA → Branch A fires
- Customer in CA + dealer in OR → Branch B fires
mustNot on the second branch is the mirror of filter on the first —
together they guarantee mutual exclusion.
#What this story adds — a third branch
The story inserts a NEW branch FIRST in the should array:
Branch 0 (NEW) — Frozen Price Winsmust: NearBlock(frozenPriceWithFeesInCents, origin=0, pivot=3000000) × 1000Branch A — In-State (UPDATED)filter: dealer.state == '#USER_STATE#'must: NearBlock(inStatePriceWithFees, ...)mustNot: ExistsBlock(frozenPriceWithFeesInCents) ← NEWBranch B — Out-of-State (UPDATED)must: NearBlock(outStatePriceWithFees, ...)mustNot: dealer.state == '#USER_STATE#'mustNot: ExistsBlock(frozenPriceWithFeesInCents) ← NEW
Now every vehicle still fires exactly one branch, but the priority is:
- If vehicle has a
frozenPriceWithFeesInCentsvalue → Branch 0 fires, scored by frozen price - Else if dealer state matches customer state → Branch A fires, scored by in-state price
- Else → Branch B fires, scored by out-of-state price
#The NearBlock score formula
distance = | field_value − origin |score = boost × pivot / (pivot + distance)
path: which numeric field on the vehicle to readorigin: the "ideal" value — score is maximum herepivot: the distance from origin at which score is exactly half of boostboost: the multiplier (viaBoostScoreBlock)
Examples for LOW_PRICE_USED_CAR (origin=0, pivot=30000, boost=1000):
| price field | distance | score |
|---|---|---|
| $0 (impossible) | 0 | 1000 |
| $15k | 15,000 | 666.7 |
| $30k | 30,000 | 500 |
| $60k | 60,000 | 333.3 |
| $100k | 100,000 | 230.8 |
Cheaper price → higher score → sorted higher up. That's "Lowest Price" sort.
#Why pivot is 100× bigger for frozen
The frozenPriceWithFeesInCents field is in cents. The
inStatePriceWithFees field is in dollars. Same dollar amount, different
unit, different number:
- $24,000 in
inStatePriceWithFees=24000 - $24,000 in
frozenPriceWithFeesInCents=2400000
To produce the same Near curve shape on a cents field, multiply pivot by 100:
- Existing pivot for
inStatePriceWithFees:30000(= $30k spread) - Frozen branch pivot for
frozenPriceWithFeesInCents:3000000(= $30k in cents, still a $30k spread)
Get this wrong by 100× and every vehicle scores near zero. The Near curve is exquisitely sensitive to scale.
#The availability gate
This is Item 2 of staticFactors. It's a flat-bonus block:
CompoundBlock (msm: 4)should: [EqualsBlock(availability.purchasePending, false),EqualsBlock(availability.salesPending, false),EqualsBlock(availability.salesBooked, false),EqualsBlock(availability.inventoryInTransit, false),]scoreBlock: ConstantScoreBlock(10000)
If all 4 flags are false → all 4 "should" items match → msm:4 satisfied →
ConstantScoreBlock fires → +10,000.
If even ONE flag is true → fewer than 4 match → msm:4 not satisfied → +0 contribution.
This is all-or-nothing. The 4 flags come from two different systems:
purchasePending— set by Driveway cart-service (real-time)salesPending,salesBooked,inventoryInTransit— set by the dealer's CSV import (30-min cron)
#Final composite score for a vehicle
Example: California customer, Honda Civic at an Oregon dealer.
inStatePriceWithFees = 24000 (dealer doesn't matter, same field)outStatePriceWithFees = 26500frozenPriceWithFeesInCents = null (not in any cart)availability flags = all falseBranch 0 (frozen):must: NearBlock(frozenPriceWithFeesInCents)→ field is null → branch doesn't fireBranch A (in-state):filter: dealer.state ("OR") == customer.state ("CA")→ false, branch excludedBranch B (out-of-state):mustNot: dealer.state ("OR") == "CA"→ satisfied (OR ≠ CA)must: NearBlock(outStatePriceWithFees=26500, origin=0, pivot=30000) × 1000→ score = 1000 × 30000 / 56500 = 531Item 1 total: 531Item 2 (availability):all 4 flags false → ConstantScoreBlock(10000) fires→ +10000FINAL = 531 + 10000 = 10531
Atlas sorts every vehicle by this number. The Civic at 10,531 ranks above more-expensive available cars (10,400) and below cheaper available cars (10,800).
#Why "Sort by Lowest Price" isn't a SQL ORDER BY
It's not. It's a score. Lucene's $search stage produces results in descending score order. "Lowest Price" means "give the lowest prices the highest score" (origin=0 for Near). "Highest Price" means "give the highest prices the highest score" (origin set to a high value like 1,000,000).
This is why Atlas Search can do things SQL can't easily — combine multiple
signals (price + availability + relevance) into one ranking with one
mathematical formula. The cost is that the config looks dense, but the
underlying model is just sum of contributions.