Introduction
'Top search result is actually unpopular' — combine BM25 text relevance with user behavior (clicks/purchases) for personalized search ranking. Let Claude Code design this.
CLAUDE.md Rules
## Search Ranking Rules
- Text relevance (60%): Elasticsearch BM25
- Popularity (25%): 30-day clicks/purchases
- Freshness (10%): decay score from listing date
- Personalization (5%): user browse history
- Popular queries: Redis cache 10 minutes
- Learning loop: 1-hour click aggregation
Generated Implementation
const esQuery = {
query: {
function_score: {
query: {
multi_match: {
query,
fields: ['name^3', 'description^1', 'category^2'],
fuzziness: 'AUTO',
},
},
functions: [
// Popularity (log scale to normalize large differences)
{ field_value_factor: { field: 'popularityScore', modifier: 'log1p', factor: 2.5 }, weight: 0.25 },
// Freshness decay
{ gauss: { createdAt: { origin: 'now', scale: '30d', decay: 0.5 } }, weight: 0.10 },
// Personalization: prefer user's top categories
...userPrefs.topCategories.map(cat => ({ filter: { term: { category: cat.name } }, weight: 0.05 * cat.weight })),
],
boost_mode: 'multiply',
},
},
};
// Track clicks and update popularity
export async function trackSearchClick(params: { productId: string; userId?: string }) {
const hourKey = Math.floor(Date.now() / 3_600_000);
await redis.zincrby(`clicks:products:${hourKey}`, 1, params.productId);
if (params.userId) {
await redis.zadd(`user:history:${params.userId}`, Date.now(), params.productId);
}
}
// Hourly popularity score update (exponential moving average)
await elasticsearch.bulk({ body: [
{ update: { _index: 'products', _id: productId } },
{ script: { source: 'ctx._source.popularityScore = ctx._source.popularityScore * 0.9 + params.clicks * 0.1', params: { clicks: clickCount } } },
] });
Summary
- function_score: combine BM25 × popularity × freshness × personalization
- Redis Sorted Set: collect click events in 1-hour buckets
- Exponential moving average: 0.9 fade for old data, 0.1 weight for new
- Popular query cache: Redis 10min cache for top 100 queries
Review with **Code Review Pack (¥980)* at prompt-works.jp*
myouga (@myougatheaxo) — Axolotl VTuber.
Top comments (0)