Skip to content

Building Production Rank Systems: Enterprise Search and Recommendation Ranking

The Production-grade enterprise search and recommendation platforms using XGBoost Learning-to-Rank. Leverage pairwise LambdaMART gradient optimization to elevate item ranking relevance, slash serving latency, maximize click-through conversion rates, and deliver high-throughput personalized discovery at enterprise scale.

Building Production Rank Systems: Enterprise Search and Recommendation Ranking

1. Business Problem Introduction

Enterprise Search and Recommendation Ranking Using XGBoost Learning-to-Rank + LambdaMART, A production engineering deep-dive for ML Engineers and Search Infrastructure Architects building unified search-and-recommendation ranking systems at enterprise scale.

The AutoParts.id Unified Search-Recommendation Problem

AutoParts.id is Indonesia’s largest automotive aftermarket marketplace, connecting 1.2 million registered workshops and car owners with 2,800 verified spare parts distributors across 47 cities. The platform catalogs 890,000 unique SKUs across engine components, body parts, electrical systems, tires, and accessories — with strict part-vehicle compatibility constraints (a Honda Jazz 2018 brake pad is explicitly incompatible with a Toyota Avanza 2015).

The engineering challenge is structurally different from a general e-commerce recommendation problem:

Challenge 1 — Query-Dependent Ranking: Unlike Netflix (no search query) or Spotify (implicit taste matching), 78% of AutoParts.id sessions begin with a structured search query (“kampas rem honda brio 2019”, “timing belt camry”). The ranking model must integrate the search query signal alongside the collaborative personalization signal — this is the unified search-recommendation problem.

Challenge 2 — Compatibility Hard Constraints: A wrong spare part recommendation is not merely irrelevant — it is actively harmful. A brake pad from a different vehicle model might cause brake failure. This introduces hard constraint filtering that must run before any ranking logic.

Challenge 3 — Long-Tail SKU Coverage: 67% of SKUs have fewer than 10 historical purchases. Pure collaborative filtering is blind to this long tail. But professional mechanics (workshops) know exactly what they need — they benefit more from precise search ranking than from discovery recommendations.

Challenge 4 — Dual User Segments: The platform serves two radically different segments — professional workshops (B2B, high volume, price-sensitive, search-driven) and individual car owners (B2C, low volume, discovery-driven, trust-seeking). A single ranking model must serve both.

Business Impact Assessment

ProblemCurrent KPITarget KPIRevenue Risk
Search relevance for long-tail queriesP@5: 31%P@5: 55%+Rp 6.8B/month lost search revenue
No personalization for returning workshopsRepeat purchase rate: 41%58%+B2B revenue per account
Compatibility filtering failuresWrong part return rate: 4.2%< 1.0%Operational cost + trust erosion
Cold-start for new SKUs (67% < 10 purchases)New SKU visibility: 5%15%+Distributor satisfaction
Dual-segment ranking qualityB2B CTR: 18%, B2C CTR: 3.2%B2B: 28%, B2C: 5.1%Session value

Sample Web for Autopart business

Strategic KPIs

MetricBaselineTargetPriority
Search NDCG@50.420.61+Critical
Recommendation CTR3.2%5.1%+High
Compatible Part Accuracy95.8%99.0%+Critical (safety)
Workshop Reorder Rate41%58%+High
Revenue Per Search SessionRp 38,000Rp 61,000+High
New SKU Conversion in First 30 Days0.8%2.1%+Medium

2. Recommendation Solution Evolution

2.1 The Unique Challenges of Automotive Marketplace Ranking

General recommendation system literature focuses primarily on implicit collaborative filtering. The automotive aftermarket domain introduces constraints that require a fundamentally different architecture:

Constraint 1 — Hard Compatibility Filtering:

∀ item i in recommendation set:
    compatible(i, user_vehicle_profile) = True

This eliminates 94–99% of the catalog for any given vehicle.
Remaining eligible items per user: 5,000–45,000 (depending on vehicle)

Constraint 2 — Multi-Intent Disambiguation: A query “kampas rem” (brake pad) could mean:

  • Front brake pad
  • Rear brake pad
  • Brake pad kit (all 4)
  • Racing brake pad
  • OEM vs. aftermarket preference

Ranking must integrate query intent classification alongside compatibility and collaborative signals.

2.2 Evolution from BM25 to Neural-Hybrid Ranking

StageTechnologyWhat It AddedLimitation
V1BM25 text searchKeyword relevanceNo personalization, compatibility
V2BM25 + Compatibility FilterSafety constraintNo quality ranking
V3BM25 + CF RerankPersonalizationQuery-CF integration poor
V4XGBoost LambdaMARTUnified query+collab rankingCurrent architecture
V5Two-Tower + XGBoostDeep embedding retrievalPlanned

2.3 Why XGBoost LTR for This Problem

XGBoost LambdaMART offers specific advantages over LightGBM for the AutoParts.id use case:

XGBoost-specific strengths:

  • Level-wise tree growth (vs. LightGBM leaf-wise): more stable with small group sizes (compatibility-filtered groups have 50–200 items vs. 500 in general e-commerce)
  • Exact greedy split finding (vs. GOSS): more accurate when feature distributions are highly non-normal (automotive part prices span 3 orders of magnitude)
  • Built-in monotone constraints: enforce business rules (e.g., “items with higher compatibility scores must rank higher, all else equal”)
  • Dart booster: dropout regularization prevents the model from over-relying on a few dominant features

Pairwise (RankNet objective in XGBoost):

L_pairwise = Σ(i,j): rel(i) > rel(j) log(1 + e^(-(sᵢ - sⱼ)))

Optimizes: P(item i ranked above item j) → 1 when rel(i) > rel(j)

Listwise (LambdaMART objective in XGBoost):

L_lambda = Σ queries Σ(i,j) |ΔNDCG(i,j)| × log(1 + e^(-(sᵢ - sⱼ)))

Key difference: |ΔNDCG(i,j)| weighting ensures the model
focuses its gradient effort on swaps that most improve NDCG@K.

For AutoParts.id, LambdaMART wins because:

  • Small group sizes (50–200 items post-compatibility filter) make list-level NDCG optimization more tractable
  • Automotive part decisions are heavily top-K biased (workshops click the first 3 results)
  • Business requirement is NDCG@5 (top 5 results quality), not pairwise accuracy

Model Comparison Overview

ModelNDCG@5Query IntegrationCompat. ConstraintB2B/B2C SeparationInterpretability
BM25 Pure Search0.42✅ Native❌ Post-filter❌ Single model✅ Full
ALS CF (collaborative)0.31❌ No query❌ Ignored❌ Partial❌ Latent
XGBoost Pairwise LTR0.55✅ As features✅ As features✅ Segment features✅ SHAP
XGBoost LambdaMART0.61✅ Best✅ Best✅ Best✅ SHAP

3. Recommendation System Workflow Diagram

flowchart TD
    subgraph QUERY_LAYER["🔍 Query Processing Layer"]
        Q1["Raw Search Query\n'kampas rem brio 2019'"]
        Q2["NLP Pipeline\nEntity extraction · Intent classification"]
        Q3["Vehicle Profile Lookup\nFrom user account / cookie"]
        Q4["Compatibility Filter\nEliminate incompatible parts"]
        Q1 --> Q2 --> Q3 --> Q4
    end

    subgraph RETRIEVAL["🎯 Multi-Source Retrieval"]
        R1["BM25 Text Retrieval\nTop-200 by text relevance"]
        R2["ALS CF Retrieval\nTop-200 by user affinity"]
        R3["Popularity Fallback\nTop-50 by category sales"]
        Q4 --> R1 & R2 & R3
        R1 & R2 & R3 --> R4["Candidate Merging\nDeduplicate · Max 300 candidates"]
    end

    subgraph FEATURE_ENGINE["⚙️ Feature Assembly Engine"]
        R4 --> FA1["Query Features\nBM25 score · TF-IDF · intent"]
        R4 --> FA2["User Features\nRFM · segment · vehicle history"]
        R4 --> FA3["Item Features\ncompatibility · quality · price"]
        R4 --> FA4["Interaction Features\nALS score · co-purchase · historical CVR"]
        FA1 & FA2 & FA3 & FA4 --> FA5["Feature Vector\n94 dimensions"]
    end

    subgraph XGB_RANKER["🧠 XGBoost LambdaMART Ranker"]
        FA5 --> X1["Monotone Constraint Enforcement\nCompatibility scores monotone+"]
        X1 --> X2["LambdaMART Scoring\nNDCG@5 optimized"]
        X2 --> X3["Segment-Aware Output\nB2B vs. B2C score adjustment"]
    end

    subgraph SERVING["🚀 Online Serving"]
        X3 --> SV1["Top-5 Search Results\nFastAPI < 85ms"]
        X3 --> SV2["Recommendation Strip\nPersonalized for logged-in users"]
    end

    subgraph MONITORING["📊 MLOps Monitoring"]
        SV1 & SV2 --> M1["Event Logging\nClick · Add-to-cart · Purchase · Return"]
        M1 --> M2["Offline Metrics\nNDCG · MAP · MRR"]
        M1 --> M3["Online Metrics\nCTR · CVR · Return Rate"]
        M2 & M3 --> M4{"PSI / NDCG\nDrift?"}
        M4 -->|"Alert"| M5["Retrain Pipeline\nGitHub Actions + MLflow"]
    end

    style QUERY_LAYER fill:#0c1445,stroke:#3b82f6,color:#e0f2fe
    style RETRIEVAL fill:#0c2340,stroke:#0ea5e9,color:#e0f2fe
    style FEATURE_ENGINE fill:#0c2e1f,stroke:#10b981,color:#d1fae5
    style XGB_RANKER fill:#1c0c2e,stroke:#8b5cf6,color:#ede9fe
    style SERVING fill:#1c1708,stroke:#f59e0b,color:#fef3c7
    style MONITORING fill:#1c0808,stroke:#ef4444,color:#fee2e2

4. System Design Architecture

4.1 Unified Search-Recommendation Architecture

flowchart LR
    subgraph CLIENTS["Client Layer"]
        C1["Workshop Mobile App\nB2B heavy search"]
        C2["Car Owner Web\nB2C discovery"]
        C3["Distributor Dashboard\nInventory visibility"]
    end

    subgraph GATEWAY["API Gateway + Cache"]
        G1["Kong API Gateway\nAuth · Rate Limit · Circuit Breaker"]
        G2["Redis Cache\nUser profile · Vehicle compat cache · 5min TTL"]
    end

    subgraph RETRIEVAL_LAYER["Retrieval Layer"]
        RL1["Elasticsearch 8.x\nBM25 + Vector Search"]
        RL2["FAISS ALS Index\nUser embedding retrieval"]
        RL3["Compatibility Engine\nVehicle-part graph"]
    end

    subgraph RANKING_LAYER["XGBoost Ranking Layer"]
        RK1["Feature Server\nGo microservice · < 5ms"]
        RK2["XGBoost LambdaMART\nLoaded in-process"]
        RK3["Monotone Constraint Layer\nRule enforcement"]
        RK4["Segment Router\nB2B vs. B2C adjustment"]
    end

    subgraph DATA_LAYER["Data Layer"]
        D1[("PostgreSQL\nUser profiles · Orders")]
        D2[("DuckDB\nFeature computation · Analytics")]
        D3[("Redis\nReal-time feature cache")]
        D4[("S3 / MinIO\nParquet features · Model artifacts")]
    end

    subgraph MLOPS["MLOps Infrastructure"]
        M1["MLflow Server\nModel Registry + Experiments"]
        M2["GitHub Actions\nCI/CD · Auto-retrain"]
        M3["Grafana + Prometheus\nReal-time monitoring"]
        M4["Great Expectations\nData quality gates"]
    end

    C1 & C2 & C3 --> G1
    G1 <--> G2
    G1 --> RL1 & RL2
    RL3 --> RL1
    RL1 & RL2 --> RK1
    D3 --> RK1
    RK1 --> RK2
    M1 --> RK2
    RK2 --> RK3 --> RK4
    RK4 --> G1
    D1 & D2 --> D3
    D4 --> M1
    M2 --> M1
    RK2 --> M3

    style CLIENTS fill:#0f172a,stroke:#64748b,color:#e2e8f0
    style GATEWAY fill:#0f172a,stroke:#3b82f6,color:#e2e8f0
    style RETRIEVAL_LAYER fill:#0f172a,stroke:#0ea5e9,color:#e2e8f0
    style RANKING_LAYER fill:#0f172a,stroke:#8b5cf6,color:#e2e8f0
    style DATA_LAYER fill:#0f172a,stroke:#10b981,color:#e2e8f0
    style MLOPS fill:#0f172a,stroke:#f59e0b,color:#e2e8f0

4.2 Feature Engineering: 94 Dimensions

The 94-dimensional feature vector for AutoParts.id integrates search-specific signals that general recommendation systems lack:

Query Features (18 dimensions):

QUERY_FEATURES = {
    # BM25 and text similarity
    'bm25_score': 'float',                  # Elasticsearch BM25 score
    'title_exact_match': 'binary',          # Exact title match
    'title_partial_match_score': 'float',   # Partial title match
    'oem_number_match': 'binary',           # OEM part number match
    'brand_query_match': 'binary',          # Brand extracted from query
    'category_query_match_score': 'float',  # Category intent alignment

    # Semantic similarity
    'embedding_cosine_similarity': 'float', # Query-item embedding similarity
    'description_match_score': 'float',

    # Intent features
    'query_intent_search': 'binary',        # Pure search intent
    'query_intent_discover': 'binary',      # Discovery intent
    'query_intent_oem_preferred': 'binary', # OEM preference signal
    'query_intent_aftermarket': 'binary',   # Aftermarket preference
    'query_has_year_entity': 'binary',      # Year mentioned in query
    'query_has_brand_entity': 'binary',     # Brand in query
    'query_has_part_type': 'binary',        # Part type identified
    'query_length_tokens': 'int',           # Query length
    'query_specificity_score': 'float',     # How specific is the query
    'query_vehicle_match_score': 'float',   # Vehicle match to user profile
}

User Features (32 dimensions):

USER_FEATURES = {
    # Segment
    'user_segment_b2b_workshop': 'binary',
    'user_segment_b2c_owner': 'binary',
    'user_city_tier': 'categorical_encoded',  # Tier 1/2/3 city

    # Vehicle profile
    'user_primary_vehicle_brand': 'categorical_encoded',
    'user_primary_vehicle_year': 'int',
    'user_n_registered_vehicles': 'int',
    'user_vehicle_age_years': 'float',

    # Purchase behavior (B2B-specific)
    'user_avg_order_value_idr': 'float',
    'user_purchase_frequency_30d': 'int',
    'user_preferred_brand_tier': 'categorical_encoded',  # OEM/premium/economy
    'user_price_negotiation_rate': 'float',    # B2B specific
    'user_bulk_order_rate': 'float',           # B2B specific
    'user_return_rate_90d': 'float',
    'user_search_to_purchase_rate': 'float',

    # Embeddings (16 dims from ALS)
    **{f'user_als_emb_{i}': 'float' for i in range(16)},

    # Temporal
    'hour_of_day': 'int',
    'day_of_week': 'int',
    'is_peak_workshop_hour': 'binary',  # 8-11am and 1-4pm weekdays
}

Item Features (30 dimensions):

ITEM_FEATURES = {
    # Identity
    'item_category_l1': 'categorical_encoded',
    'item_category_l2': 'categorical_encoded',
    'item_brand_tier': 'categorical_encoded',  # OEM/premium/economy

    # Pricing
    'item_price_idr': 'float_log',
    'item_price_vs_category_median': 'float',
    'item_discount_rate': 'float',
    'item_b2b_discount_available': 'binary',

    # Compatibility
    'item_compatibility_score': 'float',       # [0,1] — 1 = perfect match
    'item_n_compatible_vehicles': 'int_log',   # Breadth of compatibility
    'item_oem_certified': 'binary',
    'item_aftermarket_grade': 'categorical_encoded',  # A/B/C quality grade

    # Quality
    'item_avg_review_score': 'float',
    'item_review_count': 'int_log',
    'item_return_rate_30d': 'float',
    'item_defect_complaint_rate': 'float',
    'item_seller_rating': 'float',
    'item_fulfillment_speed_days': 'float',
    'item_stock_availability': 'float',

    # Popularity
    'item_search_click_rate_7d': 'float',
    'item_purchase_rate_7d': 'float',
    'item_b2b_purchase_rate_7d': 'float',
    'item_b2c_purchase_rate_7d': 'float',
    'item_days_since_listing': 'int',
    'item_last_restock_days': 'int',

    # Embeddings (4 dims)
    'item_als_emb_norm': 'float',
    'item_visual_category_score': 'float',
    'item_text_emb_cluster': 'categorical_encoded',
    'item_part_hierarchy_level': 'int',  # OEM assembly depth
}

Interaction Features (14 dimensions):

INTERACTION_FEATURES = {
    # Retrieval scores
    'bm25_rank_in_pool': 'int',
    'als_rank_in_pool': 'int',
    'als_score_normalized': 'float',

    # Historical user-item affinity
    'user_item_historical_clicks': 'int',
    'user_item_historical_purchases': 'int',
    'user_category_historical_cvr': 'float',
    'user_brand_affinity_score': 'float',
    'user_price_range_affinity': 'float',

    # Cross-shop signals
    'item_co_purchase_score_with_user_history': 'float',
    'workshop_peer_purchase_score': 'float',   # What similar workshops bought

    # Context
    'session_search_refinement_count': 'int',  # How many times query was refined
    'session_items_viewed_before': 'int',
    'position_in_candidate_pool': 'int',
    'retrieval_source': 'categorical_encoded', # bm25/als/popularity
}

4.5 XGBoost Model Versioning, Canary Deployment, and Rollback Architecture

flowchart TD
    subgraph TRIGGER["⏰ Retraining Triggers"]
        T1["Scheduled Weekly\nEvery Monday 02:00 WIB"]
        T2["PSI Alert\nDrift > 0.2 on top-10 features"]
        T3["NDCG Drop Alert\n> 3% regression vs baseline"]
        T4["Manual Dispatch\nData Science team"]
    end

    subgraph PIPELINE["🔄 Training Pipeline\nGitHub Actions"]
        P1["Data Extraction\nDuckDB: 90-day interaction window"]
        P2["Feature Build\n94 features · Compatibility graph refresh"]
        P3["Label Construction\n0-4 graded relevance"]
        P4["Optuna HPO\n40 trials · XGBoost params"]
        P5["XGBoost LambdaMART\nBest params · n_boost_round=1000"]
        P6["SHAP Analysis\nFeature importance report"]
        P7["Eval Gate\nNDCG@5 > prod_baseline + 0.01"]
        T1 & T2 & T3 & T4 --> P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7
    end

    subgraph REGISTRY["📦 MLflow Model Registry"]
        R1["Staging Stage\nNew candidate model"]
        R2["Champion vs Challenger\nSide-by-side NDCG comparison"]
        R3["Production Stage\nApproved model promoted"]
        R4["Archived Stage\nPrevious versions retained 90 days"]
        P7 -->|"Pass"| R1 --> R2 -->|"Challenger wins"| R3
        R3 -->|"New version"| R4
    end

    subgraph DEPLOYMENT["🚀 Deployment Strategy"]
        D1["Shadow Deploy 24h\nNew model logs · no user impact"]
        D2{"Shadow CTR ≥\nProduction CTR?"}
        D3["Canary 10%\nWorkshop users first (B2B)"]
        D4{"Compat Accuracy\n≥ 99.0% AND NDCG stable?"}
        D5["Scale to 50%\nMixed B2B + B2C"]
        D6["Full Rollout 100%"]
        D7["Automatic Rollback\nRevert to R4 (previous)"]
        D8["Incident Alert\nPagerDuty + Slack"]
        R3 --> D1 --> D2
        D2 -->|"Yes"| D3 --> D4
        D2 -->|"No"| D7
        D4 -->|"Yes"| D5 --> D6
        D4 -->|"No"| D7
        D7 --> D8
    end

    subgraph SERVING_INFRA["⚡ Serving Infrastructure"]
        SI1["Load Balancer\nRoute by user_id hash"]
        SI2["Model Server A\nCurrent Production"]
        SI3["Model Server B\nCanary / New model"]
        SI4["Prometheus Metrics\nLatency · NDCG · CTR · Compat%"]
        D6 --> SI1
        SI1 -->|"90%"| SI2
        SI1 -->|"10% canary"| SI3
        SI2 & SI3 --> SI4
        SI4 -->|"Alert"| TRIGGER
    end

    style TRIGGER fill:#1a1a0a,stroke:#d97706,color:#fef3c7
    style PIPELINE fill:#0a1a2a,stroke:#3b82f6,color:#dbeafe
    style REGISTRY fill:#0a2a0a,stroke:#16a34a,color:#dcfce7
    style DEPLOYMENT fill:#1a0a2a,stroke:#9333ea,color:#f3e8ff
    style SERVING_INFRA fill:#2a0a0a,stroke:#dc2626,color:#fee2e2

Diagram Walkthrough: Retraining is triggered by four sources: schedule, PSI drift alert, NDCG regression alert, or manual dispatch. The training pipeline runs on GitHub Actions with Optuna HPO before registering in MLflow. The deployment strategy runs in phases — shadow (24h, no impact) → B2B canary (compatibility-critical) → 50% → full rollout — with automatic rollback if compatibility accuracy drops below 99% or NDCG regresses. Prometheus metrics feed back into the trigger layer, closing the operational loop.


5. Mathematical Explanation

5.1 XGBoost LambdaMART: Exact Greedy with Monotone Constraints

XGBoost’s LambdaMART implementation differs from LightGBM in its tree construction algorithm:

XGBoost Level-wise tree growth:

For each tree m:
  For each level d = 0, 1, ..., max_depth:
    For each node at level d:
      Find best split: (feature j, threshold t) that maximizes:
      Gain = L(left_child) + L(right_child) - L(parent) - γ

Where L(node) = (Σᵢ∈node λᵢ)² / (Σᵢ∈node |dλᵢ/dsᵢ| + λ_reg)

Monotone Constraint Enforcement:

# XGBoost enforces monotone constraints at split time:
# If monotone_constraint[j] = +1:
#   For feature j, child_right_score >= child_left_score
# This guarantees: higher compatibility_score → higher ranking score

xgb_ranker = xgb.XGBRanker(
    monotone_constraints={
        'item_compatibility_score': 1,       # Must rank higher
        'item_oem_certified': 1,             # OEM always preferred
        'item_stock_availability': 1,        # In-stock preferred
        'item_defect_complaint_rate': -1,    # Higher defects → lower rank
        'item_return_rate_30d': -1,          # Higher returns → lower rank
    }
)

5.2 Lambda Gradients with NDCG Swap Deltas

For query group q with items ordered by predicted scores s:

For each pair (i, j) where s_i > s_j and rel_j > rel_i (incorrect pair):

  ΔNDCG(i,j) = (G(rel_i) - G(rel_j)) × (D(rank_i) - D(rank_j)) / IDCG

  λᵢⱼ = -|ΔNDCG(i,j)| × σ(-(sᵢ - sⱼ))
       = -|ΔNDCG(i,j)| × 1/(1 + e^(sᵢ - sⱼ))

  w_ij = |ΔNDCG(i,j)| × σ(sᵢ - sⱼ) × (1 - σ(sᵢ - sⱼ))

Total gradient for item i:
  λ_i = Σ_j>i λᵢⱼ - Σ_j<i λᵢⱼ

5.3 Segment-Aware Score Adjustment

After LambdaMART scoring, B2B and B2C segments receive different post-processing:

Score_B2B(u,i) = F(X(u,i)) + α_B2B × log(item_b2b_purchase_rate_7d + ε)
                           + β_B2B × item_b2b_discount_available

Score_B2C(u,i) = F(X(u,i)) + α_B2C × item_avg_review_score
                           + β_B2C × item_visual_quality_score

Where α, β are learned from segment-specific A/B tests.

6. Implementation: xgboost-ltr-ranking-system Repository

Repository: https://github.com/masterofray/xgboost-ltr-ranking-system

Repository Structure

xgboost-ltr-ranking-system/
├── data/
│   ├── raw/
│   │   ├── interactions.parquet
│   │   ├── search_logs.parquet
│   │   └── compatibility_graph.json
│   └── processed/
│       ├── train_ltr.parquet
│       ├── val_ltr.parquet
│       └── test_ltr.parquet
├── preprocessing/
│   ├── query_processor.py          # NLP pipeline: entity extraction
│   ├── compatibility_filter.py     # Vehicle-part compatibility graph
│   ├── feature_builder.py          # 94-feature construction
│   └── label_builder.py            # Graded relevance labels (0-4)
├── models/
│   ├── ltr/
│   │   ├── train_xgb_ranker.py     # Primary XGBoost LTR trainer
│   │   ├── evaluate_xgb.py         # NDCG, MAP, MRR evaluation
│   │   └── monotone_constraints.py # Business rule constraints
│   └── retrieval/
│       ├── bm25_retriever.py        # Elasticsearch BM25 wrapper
│       └── als_retriever.py         # FAISS ALS retrieval
├── ranking/
│   ├── online_ranker.py            # Real-time ranking service
│   └── segment_adjuster.py         # B2B/B2C score adjustment
├── evaluation/
│   ├── offline_metrics.py
│   └── compatibility_accuracy.py   # Compatibility constraint evaluation
├── monitoring/
│   ├── drift_detector.py
│   └── business_metrics_tracker.py # CTR, CVR, return rate
├── api/
│   ├── serve.py                    # FastAPI serving endpoint
│   ├── schemas.py                  # Request/response Pydantic models
│   └── middleware.py               # Auth, logging, circuit breaker
└── README.md

Core Training Script: models/ltr/train_xgb_ranker.py

import xgboost as xgb
import pandas as pd
import numpy as np
import mlflow
import mlflow.xgboost
import shap
from tqdm import tqdm
import optuna
from sklearn.metrics import ndcg_score

# ─────────────────────────────────────────────────────────────────────
# XGBoost LambdaMART Trainer — AutoParts.id Search + Recommendation
# Input:  94-feature LTR dataset with query groups and graded labels
# Output: XGBoost ranker model with monotone constraints + SHAP
# ─────────────────────────────────────────────────────────────────────

FEATURE_COLS = [
    # Query features (18)
    'bm25_score', 'title_exact_match', 'title_partial_match_score',
    'oem_number_match', 'brand_query_match', 'category_query_match_score',
    'embedding_cosine_similarity', 'description_match_score',
    'query_intent_search', 'query_intent_discover',
    'query_intent_oem_preferred', 'query_intent_aftermarket',
    'query_has_year_entity', 'query_has_brand_entity', 'query_has_part_type',
    'query_length_tokens', 'query_specificity_score', 'query_vehicle_match_score',

    # User features (32 — abbreviated for readability)
    'user_segment_b2b_workshop', 'user_segment_b2c_owner',
    'user_city_tier', 'user_primary_vehicle_brand',
    'user_primary_vehicle_year', 'user_n_registered_vehicles',
    'user_vehicle_age_years', 'user_avg_order_value_idr',
    'user_purchase_frequency_30d', 'user_preferred_brand_tier',
    'user_return_rate_90d', 'user_search_to_purchase_rate',
    *[f'user_als_emb_{i}' for i in range(16)],
    'hour_of_day', 'day_of_week', 'is_peak_workshop_hour',

    # Item features (30 — abbreviated)
    'item_category_l1', 'item_category_l2', 'item_brand_tier',
    'item_price_idr', 'item_price_vs_category_median', 'item_discount_rate',
    'item_b2b_discount_available', 'item_compatibility_score',
    'item_n_compatible_vehicles', 'item_oem_certified',
    'item_avg_review_score', 'item_review_count', 'item_return_rate_30d',
    'item_defect_complaint_rate', 'item_seller_rating',
    'item_fulfillment_speed_days', 'item_stock_availability',
    'item_search_click_rate_7d', 'item_purchase_rate_7d',
    'item_b2b_purchase_rate_7d', 'item_b2c_purchase_rate_7d',
    'item_days_since_listing', 'item_als_emb_norm',
    'item_text_emb_cluster', 'item_part_hierarchy_level',

    # Interaction features (14)
    'bm25_rank_in_pool', 'als_rank_in_pool', 'als_score_normalized',
    'user_item_historical_clicks', 'user_item_historical_purchases',
    'user_category_historical_cvr', 'user_brand_affinity_score',
    'user_price_range_affinity', 'item_co_purchase_score_with_user_history',
    'workshop_peer_purchase_score', 'session_search_refinement_count',
    'session_items_viewed_before', 'position_in_candidate_pool', 'retrieval_source',
]

MONOTONE_CONSTRAINTS = {
    'item_compatibility_score': 1,
    'item_oem_certified': 1,
    'item_stock_availability': 1,
    'item_seller_rating': 1,
    'item_defect_complaint_rate': -1,
    'item_return_rate_30d': -1,
}


class XGBoostLTRTrainer:
    """
    Production XGBoost LambdaMART trainer for unified
    search + recommendation ranking at AutoParts.id.
    """

    def __init__(self):
        self.model = None

    def build_dmatrix(
        self,
        df: pd.DataFrame,
        label_col: str = 'relevance_label',
        group_col: str = 'query_id'
    ) -> xgb.DMatrix:
        """Build XGBoost DMatrix with group information for LTR."""
        X = df[FEATURE_COLS].values
        y = df[label_col].values

        dmatrix = xgb.DMatrix(X, label=y, feature_names=FEATURE_COLS)

        # Compute group sizes
        groups = df.groupby(group_col).size().values
        dmatrix.set_group(groups)

        return dmatrix

    def train(
        self,
        train_df: pd.DataFrame,
        val_df: pd.DataFrame,
        params: dict = None,
        num_boost_round: int = 1000,
        experiment_name: str = "xgboost_ltr_autoparts"
    ) -> dict:
        """
        Full XGBoost LambdaMART training pipeline.
        """
        if params is None:
            params = self._default_params()

        mlflow.set_experiment(experiment_name)

        dtrain = self.build_dmatrix(train_df)
        dval = self.build_dmatrix(val_df)

        # Build monotone constraint tuple in feature order
        mono_tuple = tuple(
            MONOTONE_CONSTRAINTS.get(f, 0) for f in FEATURE_COLS
        )
        params['monotone_constraints'] = mono_tuple

        with mlflow.start_run(
            run_name=f"XGB_LambdaMART_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}"
        ):
            mlflow.log_params(params)

            print("Training XGBoost LambdaMART...")
            callbacks = [
                xgb.callback.EarlyStopping(
                    rounds=50,
                    metric_name="ndcg@5",
                    maximize=True,
                    save_best=True
                )
            ]

            self.model = xgb.train(
                params,
                dtrain,
                num_boost_round=num_boost_round,
                evals=[(dtrain, "train"), (dval, "val")],
                verbose_eval=100,
                callbacks=callbacks
            )

            # SHAP analysis
            print("Computing SHAP values...")
            explainer = shap.TreeExplainer(self.model)
            X_val_sample = val_df[FEATURE_COLS].values[:3000]
            shap_values = explainer.shap_values(X_val_sample)

            shap_df = pd.DataFrame({
                'feature': FEATURE_COLS,
                'shap_mean_abs': np.abs(shap_values).mean(axis=0)
            }).sort_values('shap_mean_abs', ascending=False)

            # Evaluate
            val_metrics = self._evaluate(val_df)
            mlflow.log_metrics(val_metrics)
            mlflow.log_dict(
                shap_df.head(15).set_index('feature')['shap_mean_abs'].to_dict(),
                "shap_top15.json"
            )

            mlflow.xgboost.log_model(
                self.model,
                "xgboost_ltr_model",
                registered_model_name="XGBoost_LTR_AutoParts"
            )

        return val_metrics

    def _default_params(self) -> dict:
        return {
            "objective": "rank:ndcg",
            "eval_metric": ["ndcg@5", "ndcg@10"],
            "eta": 0.05,
            "max_depth": 6,
            "min_child_weight": 10,
            "subsample": 0.8,
            "colsample_bytree": 0.8,
            "alpha": 0.1,
            "lambda": 1.0,
            "tree_method": "hist",
            "lambdarank_num_pair_per_sample": 8,
            "lambdarank_pair_method": "topk",
            "ndcg_exp_gain": True,
            "seed": 42,
            "verbosity": 1,
        }

    def _evaluate(
        self,
        df: pd.DataFrame,
        k_list: list = [5, 10, 20]
    ) -> dict:
        dmatrix = self.build_dmatrix(df)
        scores = self.model.predict(dmatrix)
        df = df.copy()
        df['pred_score'] = scores

        results = {}
        for k in k_list:
            ndcg_vals = []
            for qid, group in tqdm(
                df.groupby('query_id'),
                desc=f"NDCG@{k}",
                colour="#05ad46",
                leave=False
            ):
                if len(group) < 2:
                    continue
                true_rel = group['relevance_label'].values.reshape(1, -1)
                pred_s = group['pred_score'].values.reshape(1, -1)
                ndcg_vals.append(ndcg_score(true_rel, pred_s, k=k))
            results[f"ndcg@{k}"] = np.mean(ndcg_vals)

        return results


def optimize_xgb_hyperparameters(
    train_df: pd.DataFrame,
    val_df: pd.DataFrame,
    n_trials: int = 40
) -> dict:
    """Optuna-based hyperparameter optimization for XGBoost LTR."""
    trainer = XGBoostLTRTrainer()

    def objective(trial):
        params = {
            "objective": "rank:ndcg",
            "eval_metric": "ndcg@5",
            "eta": trial.suggest_float("eta", 0.01, 0.3, log=True),
            "max_depth": trial.suggest_int("max_depth", 3, 9),
            "min_child_weight": trial.suggest_int("min_child_weight", 1, 50),
            "subsample": trial.suggest_float("subsample", 0.5, 1.0),
            "colsample_bytree": trial.suggest_float("colsample_bytree", 0.5, 1.0),
            "alpha": trial.suggest_float("alpha", 1e-8, 10.0, log=True),
            "lambda": trial.suggest_float("lambda", 1e-8, 10.0, log=True),
            "lambdarank_num_pair_per_sample": trial.suggest_int(
                "lambdarank_num_pair_per_sample", 4, 16
            ),
            "tree_method": "hist",
            "verbosity": 0,
            "seed": 42,
        }
        result = trainer.train(
            train_df, val_df, params,
            num_boost_round=500,
            experiment_name="xgb_hpo_autoparts"
        )
        return result["ndcg@5"]

    study = optuna.create_study(direction="maximize")
    study.optimize(objective, n_trials=n_trials, show_progress_bar=True)
    return study.best_params

Online Serving: api/serve.py

from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
import xgboost as xgb
import pandas as pd
import numpy as np
import mlflow.xgboost
import redis
import json
from typing import List
from .schemas import RankingRequest, RankingResponse, RankedItem

app = FastAPI(title="AutoParts.id XGBoost LTR Ranking API", version="2.0")

# Load model at startup
model = mlflow.xgboost.load_model("models:/XGBoost_LTR_AutoParts/Production")
redis_client = redis.Redis(host='redis', port=6379, decode_responses=True)

@app.post("/rank", response_model=RankingResponse)
async def rank_items(request: RankingRequest) -> RankingResponse:
    """
    Re-rank a candidate pool for a user query.

    Input:
      - user_id: str
      - query: str (optional, empty for pure recommendation)
      - candidate_items: List[str] (item_ids from retrieval stage)
      - user_context: dict (session context)

    Output:
      - ranked_items: List[RankedItem] (top-K ranked items with scores)
    """
    try:
        # 1. Retrieve user features from Redis cache
        user_features = _get_user_features(request.user_id)
        if user_features is None:
            raise HTTPException(status_code=404, detail="User not found")

        # 2. Retrieve item features for candidates
        item_features_list = _get_item_features_batch(request.candidate_items)

        # 3. Compute query features if search context
        query_features = _compute_query_features(
            request.query,
            request.candidate_items,
            user_features
        ) if request.query else _default_query_features(len(request.candidate_items))

        # 4. Build feature matrix
        feature_df = _assemble_feature_matrix(
            user_features,
            item_features_list,
            query_features,
            request.user_context
        )

        # 5. XGBoost inference
        dmatrix = xgb.DMatrix(feature_df[FEATURE_COLS].values, feature_names=FEATURE_COLS)
        scores = model.predict(dmatrix)

        # 6. Segment-aware score adjustment
        segment = user_features.get('user_segment_b2b_workshop', 0)
        if segment == 1:
            # B2B: boost items with B2B pricing
            scores += 0.1 * feature_df['item_b2b_discount_available'].values

        # 7. Sort and return top-K
        sorted_indices = np.argsort(scores)[::-1]
        ranked_items = [
            RankedItem(
                item_id=request.candidate_items[i],
                score=float(scores[i]),
                rank=rank + 1
            )
            for rank, i in enumerate(sorted_indices[:request.top_k])
        ]

        return RankingResponse(
            user_id=request.user_id,
            query=request.query,
            ranked_items=ranked_items,
            model_version="XGBoost_LTR_AutoParts_v2"
        )

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

7. Experiment Results

Dataset Configuration

ParameterValue
PlatformAutoParts.id automotive marketplace
Total Users1,200,000 registered (workshops + owners)
B2B Workshop Users380,000 (31.7%)
B2C Car Owner Users820,000 (68.3%)
Total SKUs890,000
Total Interactions24,000,000 (12-month period)
Search Queries Logged8,400,000 unique queries
Compatibility Constraints2.1M vehicle-part compatibility rules
LTR Training Pairs1,200,000 query groups × avg 85 items
Feature Dimensions94 features
Label Scale0-4 graded relevance (0=no click, 4=purchase + re-purchase)

Offline Evaluation Results

ModelNDCG@5NDCG@10MAP@10MRRPrecision@5Precision@10
BM25 Baseline0.4200.4610.3890.5020.3810.294
ALS CF Only0.3120.3580.2840.4180.2670.218
BM25 + ALS Simple Blend0.4810.5160.4480.5610.4320.341
XGBoost Pointwise LTR0.5510.5820.5240.6140.4980.402
XGBoost Pairwise LTR0.5790.6070.5510.6380.5210.421
XGBoost LambdaMART0.6140.6410.5820.6710.5560.449
XGBoost LambdaMART + Monotone0.6180.6450.5870.6760.5610.453

Segment-Specific Results

ModelNDCG@5 (B2B Workshop)NDCG@5 (B2C Owner)Segment Gap
BM25 Baseline0.5120.3480.164
ALS CF Only0.2980.3220.024 (reversed)
XGBoost LambdaMART0.6710.5780.093
XGBoost + Segment Adjust0.6890.6010.088

B2B workshops show higher NDCG across all models because their search intent is more specific and their queries contain more structured information (OEM numbers, exact model years).

SHAP Top-10 Feature Importance

| Rank | Feature | SHAP Mean |Abs| | Business Insight | |---|---|---|---| | 1 | item_compatibility_score | 0.428 | Compatibility is the dominant signal | | 2 | bm25_score | 0.318 | Search relevance critical for query-driven sessions | | 3 | user_category_historical_cvr | 0.201 | User’s purchase history in category | | 4 | item_oem_certified | 0.187 | OEM certification is a strong quality proxy | | 5 | als_score_normalized | 0.162 | Collaborative signal contributes meaningfully | | 6 | item_avg_review_score | 0.138 | Social proof for B2C segment | | 7 | user_segment_b2b_workshop | 0.124 | Segment switch dramatically changes feature weights | | 8 | item_seller_rating | 0.112 | Distributor trust signal | | 9 | item_return_rate_30d | 0.098 | Quality penalty | | 10 | query_vehicle_match_score | 0.091 | Vehicle-query alignment |

Online A/B Test Results (35-Day Deployment)

MetricControl (BM25 + ALS Blend)Treatment (XGBoost LambdaMART)Lift
Search NDCG@5 (Online Eval)0.4810.614+27.7%
Search CTR (B2B Workshops)18.2%27.8%+52.7%
Search CTR (B2C Owners)3.2%5.1%+59.4%
Conversion Rate (Overall)4.1%6.3%+53.7%
Compatible Part Accuracy95.8%99.1%+3.4pp
Return Rate (Wrong Part)4.2%0.9%-78.6%
Workshop Reorder Rate41%57%+39.0%
Revenue Per Search SessionRp 38,000Rp 63,000+65.8%

Compatible part accuracy: percentage of recommended items verified compatible with user’s registered vehicle profile. Return rate reduction from 4.2% to 0.9% represents an operational cost saving of approximately Rp 2.1B per quarter.


8. Model Comparison

ModelNDCG@5CVR ImpactQuery IntegrationCompat. EnforcementTraining TimeInterpretability
BM25 Baseline0.4200% (ref)✅ Native❌ Post-filterInstant✅ Full
ALS CF0.312-24% (worse)❌ None❌ None90min❌ Latent
XGBoost Pairwise0.579+38%✅ Feature✅ Soft52min✅ SHAP
XGBoost LambdaMART0.614+54%✅ Feature✅ Monotone41min✅ SHAP
LightGBM LambdaMART0.608+51%✅ Feature❌ Less stable28min✅ SHAP

Why XGBoost over LightGBM here:

  • LightGBM leaf-wise growth with small group sizes (avg 85 items) caused training instability in 12% of experimental runs
  • XGBoost’s monotone constraint implementation is more stable for the automotive domain’s hard compatibility business rules
  • Level-wise tree growth produces more conservative models that generalize better with the high feature dimensionality introduced by query features
  • XGBoost pairwise (“rank:pairwise”) and listwise (“rank:ndcg”) objective switching is cleaner for ablation studies

9. Production Challenges

9.1 Compatibility Graph Scale and Latency

The 2.1M vehicle-part compatibility rules must be applied as a pre-filter before any ranking logic. A naive database query per request would add 200–400ms of latency. Solution:

class CompatibilityFilterCache:
    """
    Pre-load vehicle-part compatibility rules into Redis sets.
    Lookup: O(1) per item, O(n) for candidate set.
    """

    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client

    def get_compatible_items(
        self,
        vehicle_id: str,
        candidate_items: List[str]
    ) -> List[str]:
        # Redis SMEMBERS lookup: ~0.3ms for 300 candidate items
        compatible_set_key = f"compat:{vehicle_id}"
        pipeline = self.redis.pipeline()
        for item_id in candidate_items:
            pipeline.sismember(compatible_set_key, item_id)
        results = pipeline.execute()
        return [item for item, is_compat in zip(candidate_items, results) if is_compat]

Compatibility graph pre-loading: Redis sets keyed by vehicle_id, average set size 8,400 items, total memory: 21GB Redis cluster. Lookup latency: 1.2ms P99 for 300-item batch.

9.2 Query Intent Ambiguity

“Kampas rem depan belakang” (front and rear brake pad) is a multi-intent query that should return a brake pad kit, not individual pads. The entity extraction pipeline handles this:

class AutomotiveQueryProcessor:
    """
    Extracts structured automotive entities from Indonesian natural language queries.
    """
    PART_TYPE_PATTERNS = {
        'brake_pad': ['kampas rem', 'brake pad', 'kampas'],
        'timing_belt': ['timing belt', 'v-belt', 'tali kipas'],
        'oil_filter': ['filter oli', 'saringan oli', 'oil filter'],
        # ... 340 automotive part patterns
    }

    def process(self, query: str) -> dict:
        normalized = self._normalize_indonesian(query)
        return {
            'part_type': self._extract_part_type(normalized),
            'vehicle_brand': self._extract_brand(normalized),
            'vehicle_year': self._extract_year(normalized),
            'quality_preference': self._classify_quality_intent(normalized),  # OEM vs aftermarket
            'quantity_intent': self._detect_bulk_intent(normalized),  # single vs kit/set
        }

9.3 B2B Price Sensitivity Modeling

Workshop buyers are acutely price-sensitive and respond to bulk pricing. The standard ranking model underweights this:

def b2b_score_adjustment(
    base_score: float,
    item_features: dict,
    user_features: dict
) -> float:
    if user_features['user_segment_b2b_workshop'] != 1:
        return base_score

    # B2B-specific adjustments
    bulk_discount_bonus = 0.15 if item_features['item_b2b_discount_available'] else 0
    brand_loyalty_bonus = 0.10 * user_features['user_brand_loyalty_score']
    fulfillment_speed_bonus = 0.08 * (1 - item_features['item_fulfillment_speed_days'] / 7)

    return base_score + bulk_discount_bonus + brand_loyalty_bonus + fulfillment_speed_bonus

9.4 Long-Tail SKU Cold-Start

67% of 890,000 SKUs have fewer than 10 historical purchases. XGBoost handles these through feature engineering:

def build_cold_start_item_features(item_id: str) -> dict:
    """
    For items with < 10 purchases, use seller-level and category-level proxies.
    """
    item_meta = get_item_metadata(item_id)
    seller_stats = get_seller_stats(item_meta['seller_id'])
    category_stats = get_category_stats(item_meta['category_l2'])

    return {
        # Use seller history as proxy for item quality
        'item_avg_review_score': seller_stats['avg_review_score'],
        'item_return_rate_30d': seller_stats['return_rate'],
        'item_seller_rating': seller_stats['rating'],

        # Use category popularity as proxy for item demand
        'item_search_click_rate_7d': category_stats['avg_new_item_ctr'],
        'item_purchase_rate_7d': category_stats['avg_new_item_cvr'],

        # New item signals
        'item_days_since_listing': item_meta['days_since_listing'],
        'item_is_new_listing': 1,
        'item_n_compatible_vehicles': item_meta['n_compatible_vehicles'],
    }

9.5 Serving Latency Budget

The 85ms SLA for search results requires tight latency budget management:

StepP50P95P99
BM25 retrieval (Elasticsearch)12ms24ms38ms
ALS retrieval (FAISS)8ms14ms22ms
Compatibility filter (Redis)1ms2ms4ms
Feature assembly (Redis batch)4ms8ms14ms
XGBoost inference (300 items × 94 features)9ms15ms22ms
Response serialization2ms4ms6ms
Total36ms67ms106ms

P99 exceeds the 85ms SLA target. Mitigation:

  • Reduce candidate pool to 150 items for anonymous users
  • Pre-compute compatibility filter results for top-50K vehicle-SKU pairs
  • Parallelize BM25 + ALS retrieval (async calls)
  • XGBoost compiled with AVX2 SIMD: -30% inference time

After optimization: P99 = 78ms — within SLA.


10. Conclusion

The XGBoost LambdaMART deployment on AutoParts.id demonstrates the power of learning-to-rank for domains where multiple, heterogeneous signals must be unified into a single ranking decision. The key architectural insights:

  1. Compatibility as a hard constraint (pre-filter) + soft signal (feature) simultaneously — enforced compatibility as a pre-filter eliminates incompatible items; the item_compatibility_score feature within XGBoost’s monotone constraint framework further differentiates among compatible items.

  2. Query features are first-class citizens in unified search-recommendation ranking — bm25_score is the second most important SHAP feature, confirming that search and recommendation cannot be siloed.

  3. Monotone constraints transform business rules into model constraints — rather than post-processing hacks, XGBoost’s monotone constraint API enforces that OEM-certified items always rank above non-certified items, all else equal.

  4. Segment-aware scoring (B2B vs. B2C) is better implemented as a post-processing adjustment than as separate models — it preserves model simplicity while allowing segment-specific business rule application.

Business outcomes: +54% CVR, +66% revenue per session, +78.6% reduction in wrong-part return rate (from 4.2% to 0.9%), +39% workshop reorder rate.

Future Directions:

  • Two-Tower Deep Retrieval: Replace FAISS+ALS with a fine-tuned two-tower model for semantic retrieval, incorporating automotive domain pre-training.
  • Graph Neural Networks for Compatibility: Model the vehicle-part compatibility graph using GNNs to generate compatibility embeddings that capture higher-order vehicle subsystem relationships.
  • Transformer-based Query Encoding: Replace BM25 with IndoBERT-fine-tuned query encoders for richer semantic matching of Indonesian automotive terminology.
  • Multi-Objective Optimization: Pareto-optimal ranking that simultaneously optimizes CTR, CVR, compatibility accuracy, and return rate.

11. Key Takeaways

  • XGBoost LambdaMART with monotone constraints is the model of choice when business rules must be embedded directly into the ranking model — the constraint API turns “compatibility must dominate” from a post-processing hack into a mathematically enforced gradient property.
  • Unified search-recommendation is architecturally distinct from pure recommendation — query features (BM25 score, intent, entity extraction) must be first-class features in the LTR model, not afterthoughts applied as post-filtering.
  • Compatibility enforcement is a safety requirement in technical domains (automotive, pharmaceutical, industrial) — the 78.6% return rate reduction from 4.2% to 0.9% is not a recommendation quality metric; it’s a product safety metric.
  • XGBoost level-wise tree growth outperforms LightGBM leaf-wise when query group sizes are small (< 200 items) — smaller groups provide less stable gradient estimates for aggressive leaf-wise splits.
  • B2B and B2C segments require separate evaluation frameworks — B2B NDCG@5 of 0.689 vs. B2C 0.601 is not a model failure; it reflects the fact that professional buyers have more structured and informative queries.
  • The SHAP value that item_compatibility_score contributes 0.428 mean absolute value — more than BM25 (0.318) and collaborative filtering (0.162) combined — is the definitive empirical proof that domain-specific constraints should dominate general-purpose ranking signals in technical marketplaces.
  • Cold-start items (< 10 purchases) are not hopeless — seller-level and category-level proxy features allow XGBoost to produce meaningful rankings for new SKUs by borrowing distributional signal from related entities.
  • 94 features is operationally manageable with Redis batch lookup and XGBoost’s HIST tree method — the incremental NDCG gain from 50 to 94 features was +3.1pp NDCG@5, justifying the feature engineering investment.
  • Latency budget management is a first-class engineering concern — every millisecond in the serving path must be allocated intentionally; P99 latency breaches require architectural changes, not just code optimization.
  • A/B test duration for technical marketplaces should be ≥ 35 days — workshop reorder cycles (the primary B2B conversion metric) operate on weekly rhythms, requiring at least 5 full cycles for statistical significance.

This concludes the four-part enterprise recommendation system series. The complete journey: collaborative filtering (Part 1) → LightFM hybrid (Part 2) → LightGBM LambdaMART (Part 3) → XGBoost LambdaMART for unified search-recommendation (Part 4). Repository: https://github.com/masterofray/xgboost-ltr-ranking-system