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.