Search Scoring Logic
This doc explains, in plain language, how search results are ordered and how RPV affects that order.
Quick picture
- Search starts with text relevance (how well the name/tags match the keyword).
- We then boost results using a few popularity and quality signals.
- These boosts change ordering, they do not remove results.
- RPV logic is implemented for both guest users and members.
- Metarank is used on the search page and for search suggestions to re-rank results for members only
- AWS Personalize is used in homepage sections and group landing pages to re-rank restaurant results for members only.
Signals we use
| Signal | Field | Factor | Modifier | Weight | What it means | When it applies |
|---|---|---|---|---|---|---|
| Revenue | top_gmv | 0.0001 | log1p | 200 | Restaurants that drive more revenue get a lift. | Always |
| Covers | total_covers | 0.001 | log1p | 100 | More bookings = more trust. | Always |
| Reviews count | reviews_count | 0.1 | log1p | 150 | Higher volume of reviews gets a lift. | Always |
| Review quality | bayesian_reviews_score | 50 | none | default | Strong ratings for established restaurants. | When reviews count is 5+ |
| New restaurant quality | google_review_score | 50 | none | default | A fair start for new restaurants. | When reviews count is below 5 or missing |
| Favorites | favorites_count | 0.5 | log1p | 50 | More favorites means more interest. | Always |
| RPV (multi-window) | bayesian_rpv_score_1d / 7d / 30d | 1.0 | none | 75 / 45 / 30 | Revenue per view compared to the average. | Always |
| Distance | coordinates | - | gauss | 150 | Closer restaurants get a lift. | When location is provided and sort is not nearest-first |
If a field is missing, it does not add any boost. We can adjust the weight to make a signal more or less dominant. RPV uses a fixed 50% / 30% / 20% split across 1d/7d/30d, which translates to weights 75 / 45 / 30.
What RPV means
RPV stands for Revenue Per View. We compute it for the last 1 day, 7 days, and 30 days, then blend the three windows in the scoring function. It answers:
When people view this restaurant, how much revenue does it generate compared to the average restaurant?
To keep it fair for low-traffic restaurants, we smooth the score:
- If a restaurant has very few views, we lean more on the global average.
- If it has many views, we trust its own data more.
The score is normalized:
1.0means about average> 1.0means better than average< 1.0means below average
RPV affects ranking for all search requests by default and always blends 1d/7d/30d scores with a fixed split.
RPV data source
RPV is computed from ClickHouse analytics data.
In Puma, the implementation reads from the ClickHouse table:
ecommerce_purchases_item_id_report(in the configured databaseCLICKHOUSE_DATABASE, default:google_analytics)
and uses these columns:
itemId→ restaurant id (stored/queried as a string)itemsViewed→ views in the time windowitemRevenue→ revenue in the time windowdate→ day partition used for the 1-day lookback
The query scans the last 30 days and uses conditional sums to derive the 1d/7d/30d windows. It groups by itemId and ignores invalid ids ('(not set)' or empty).
RPV formula
smoothed_rpv = (median_views * global_avg_rpv + revenue) / (median_views + views)
bayesian_rpv_score = global_avg_rpv > 0 ? smoothed_rpv / global_avg_rpv : 0
Where:
views= total views in the window (1d/7d/30d)revenue= total revenue in the window (1d/7d/30d)median_views= median views across restaurants with views > 0 in that windowglobal_avg_rpv= total revenue / total views across restaurants with views > 0 in that window
How RPV gets into search
Every day at 03:00 GMT+7, a scheduled job:
- Pulls the last 30 days of views and revenue from ClickHouse.
- Calculates 1d/7d/30d RPV for each restaurant.
- Writes the results into
bayesian_rpv_score_1d,bayesian_rpv_score_7d, andbayesian_rpv_score_30d.
When a restaurant is created or reindexed, Puma will reuse the existing score if it exists; otherwise it fetches it from ClickHouse.
Defaults and fallbacks
- If ClickHouse is disabled or a lookup fails, the system returns a neutral score for each window.
- If an RPV score is missing in OpenSearch, it adds no boost for that window.
Where to look (for engineers)
- Scoring logic:
apps/puma/src/services/common/query-utils.ts - Restaurant search:
apps/puma/src/services/restaurant/base-query-builder.ts - Suggestions search:
apps/puma/src/services/suggestion/search-query-builder.ts - RPV calculation:
apps/puma/src/services/rpv/rpv-metrics-service.ts - Scheduled job:
apps/puma/src/services/data-queue/worker-scheduled.ts