[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build AI-Powered Search Ranking

Build AI-Powered Search Ranking

Build an AI-powered search ranking system with learning-to-rank models, click-through feedback, query understanding, personalized results, and A/B tested ranking algorithms for search relevance.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Kai leads search at a 25-person e-commerce with 200K products. Search uses BM25 text matching — it finds relevant products but ranks them poorly. Searching "laptop" shows a $50 case before a $1000 laptop because the case description mentions "laptop" more times. Best-sellers are buried. New products with no interaction data rank last forever. Click-through rate on position 1 is 15% (should be 30%+). They need AI ranking: combine text relevance with business signals (sales, reviews, margin), learn from clicks, personalize by user history, and A/B test ranking models.

Step 1: Build the Ranking Engine

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
import { createHash } from "node:crypto";
const redis = new Redis(process.env.REDIS_URL!);

interface SearchResult {
  productId: string;
  textScore: number;
  features: RankingFeatures;
  finalScore: number;
  position: number;
}

interface RankingFeatures {
  textRelevance: number;
  salesRank: number;
  reviewScore: number;
  reviewCount: number;
  priceCompetitiveness: number;
  recency: number;
  clickThroughRate: number;
  conversionRate: number;
  personalizationScore: number;
  margin: number;
}

interface RankingModel {
  id: string;
  name: string;
  weights: Record<keyof RankingFeatures, number>;
  active: boolean;
}

const DEFAULT_MODEL: RankingModel = {
  id: "default", name: "Default LTR", active: true,
  weights: {
    textRelevance: 0.25, salesRank: 0.15, reviewScore: 0.1,
    reviewCount: 0.05, priceCompetitiveness: 0.05, recency: 0.05,
    clickThroughRate: 0.15, conversionRate: 0.1,
    personalizationScore: 0.05, margin: 0.05,
  },
};

// Rank search results using learning-to-rank
export async function rankResults(
  query: string,
  candidates: Array<{ productId: string; textScore: number }>,
  userId?: string,
  modelId?: string
): Promise<SearchResult[]> {
  const model = modelId ? await getModel(modelId) : DEFAULT_MODEL;

  // Compute features for each candidate
  const scored: SearchResult[] = [];
  for (const candidate of candidates) {
    const features = await computeFeatures(candidate.productId, candidate.textScore, query, userId);
    const finalScore = computeFinalScore(features, model.weights);
    scored.push({ productId: candidate.productId, textScore: candidate.textScore, features, finalScore, position: 0 });
  }

  // Sort by final score
  scored.sort((a, b) => b.finalScore - a.finalScore);
  scored.forEach((r, i) => r.position = i + 1);

  // Log for model training
  const searchId = createHash("md5").update(query + Date.now()).digest("hex").slice(0, 12);
  await redis.setex(`search:${searchId}`, 86400, JSON.stringify({
    query, userId, modelId: model.id, results: scored.slice(0, 20).map((r) => r.productId),
  }));

  return scored;
}

async function computeFeatures(productId: string, textScore: number, query: string, userId?: string): Promise<RankingFeatures> {
  // Batch fetch product data
  const cacheKey = `features:${productId}`;
  const cached = await redis.get(cacheKey);
  if (cached) {
    const base = JSON.parse(cached);
    base.textRelevance = textScore;
    if (userId) base.personalizationScore = await getPersonalizationScore(productId, userId);
    return base;
  }

  const { rows: [product] } = await pool.query(
    `SELECT p.*, 
       COALESCE(AVG(r.rating), 0) as avg_rating, COUNT(r.id) as review_count,
       COALESCE(SUM(oi.quantity), 0) as total_sales
     FROM products p
     LEFT JOIN reviews r ON p.id = r.product_id AND r.created_at > NOW() - INTERVAL '1 year'
     LEFT JOIN order_items oi ON p.id = oi.product_id AND oi.created_at > NOW() - INTERVAL '90 days'
     WHERE p.id = $1 GROUP BY p.id`,
    [productId]
  );

  if (!product) return defaultFeatures(textScore);

  // CTR and conversion from click logs
  const impressions = parseInt(await redis.get(`product:impressions:${productId}`) || "1");
  const clicks = parseInt(await redis.get(`product:clicks:${productId}`) || "0");
  const conversions = parseInt(await redis.get(`product:conversions:${productId}`) || "0");

  const features: RankingFeatures = {
    textRelevance: textScore,
    salesRank: normalize(parseInt(product.total_sales), 0, 1000),
    reviewScore: parseFloat(product.avg_rating) / 5,
    reviewCount: normalize(parseInt(product.review_count), 0, 100),
    priceCompetitiveness: product.compare_at_price ? Math.min(1, product.price / product.compare_at_price) : 0.5,
    recency: normalize(daysSince(product.created_at), 0, 365, true),
    clickThroughRate: impressions > 10 ? clicks / impressions : 0.1,
    conversionRate: clicks > 10 ? conversions / clicks : 0.02,
    personalizationScore: userId ? await getPersonalizationScore(productId, userId) : 0.5,
    margin: product.margin ? normalize(product.margin, 0, 100) : 0.5,
  };

  await redis.setex(cacheKey, 300, JSON.stringify(features));
  return features;
}

function computeFinalScore(features: RankingFeatures, weights: Record<string, number>): number {
  let score = 0;
  for (const [key, weight] of Object.entries(weights)) {
    score += (features[key as keyof RankingFeatures] || 0) * weight;
  }
  return Math.round(score * 10000) / 10000;
}

async function getPersonalizationScore(productId: string, userId: string): Promise<number> {
  // Check user's category affinity
  const { rows: [product] } = await pool.query("SELECT category FROM products WHERE id = $1", [productId]);
  if (!product) return 0.5;
  const affinity = await redis.hget(`user:affinity:${userId}`, product.category);
  return affinity ? Math.min(1, parseFloat(affinity) / 10) : 0.5;
}

// Record click (for CTR learning)
export async function recordClick(productId: string, searchId: string, position: number, userId?: string): Promise<void> {
  await redis.incr(`product:clicks:${productId}`);
  await redis.expire(`product:clicks:${productId}`, 86400 * 30);

  // Update user category affinity
  if (userId) {
    const { rows: [product] } = await pool.query("SELECT category FROM products WHERE id = $1", [productId]);
    if (product) await redis.hincrbyfloat(`user:affinity:${userId}`, product.category, 1);
  }

  // Log for model training (position bias correction)
  await pool.query(
    "INSERT INTO click_logs (product_id, search_id, position, user_id, created_at) VALUES ($1, $2, $3, $4, NOW())",
    [productId, searchId, position, userId]
  );
}

// Record conversion
export async function recordConversion(productId: string): Promise<void> {
  await redis.incr(`product:conversions:${productId}`);
  await redis.expire(`product:conversions:${productId}`, 86400 * 30);
}

function normalize(value: number, min: number, max: number, inverse: boolean = false): number {
  const normalized = Math.max(0, Math.min(1, (value - min) / (max - min)));
  return inverse ? 1 - normalized : normalized;
}

function daysSince(date: string): number {
  return Math.floor((Date.now() - new Date(date).getTime()) / 86400000);
}

function defaultFeatures(textScore: number): RankingFeatures {
  return { textRelevance: textScore, salesRank: 0, reviewScore: 0.5, reviewCount: 0, priceCompetitiveness: 0.5, recency: 0.5, clickThroughRate: 0.1, conversionRate: 0.02, personalizationScore: 0.5, margin: 0.5 };
}

async function getModel(id: string): Promise<RankingModel> {
  const { rows: [row] } = await pool.query("SELECT * FROM ranking_models WHERE id = $1", [id]);
  return row ? { ...row, weights: JSON.parse(row.weights) } : DEFAULT_MODEL;
}

Results

  • CTR position 1: 15% → 32% — AI ranking puts best-selling, well-reviewed laptops first instead of accessories; users find what they want immediately
  • Revenue per search +18% — high-margin, high-converting products boosted; $1000 laptop ranks above $50 case; search drives more revenue
  • Click feedback loop — users click → CTR updates → ranking improves → better results → more clicks; self-improving system
  • New product cold-start — recency feature boosts new products; initial visibility while they accumulate clicks and reviews; no eternal bottom ranking
  • Personalization — user who browses gaming laptops sees gaming laptops first for "laptop" query; different user sees business laptops; same query, personalized results