Skip to content

Scalability Ceilings: Optimization Strategies Above 100M Ratings

MovieLens-1M is too small — in real world you work with 100M+ ratings, 10M+ items. This lesson: offline batch precomputation pattern, LSH (Locality-Sensitive Hashing), MinHash for approximate Jaccard, distributed computation with MapReduce/Spark, Redis-based serving.

Şükrü Yusuf KAYA
22 min read
Advanced
Scalability Tavanları: 100M Rating Üstünde Optimizasyon Stratejileri
📈 Bu dersin amacı
ML-1M'i 30 saniyede eğittik. Amazon-scale (100M user, 10M item) bu kodla yaklaşık 15 yıl sürer. Bu derste: aynı algoritma fikrini büyük ölçeğe taşıma stratejileri. LSH, MinHash, offline batch, Spark — endüstri pattern'leri.

Sorunun Boyutu#

DatasetUserItemRatingItem-Item Matrix (dense)Naive runtime
MovieLens-1M6K3.7K1M55 MB30 sn
MovieLens-25M162K62K25M16 GB30 dk
Amazon Beauty800K250K2M250 GBimkansız (dense)
Amazon Books8M2.3M22M21 TBimkansız
YouTube ölçeği2B800M100Bimkansızimkansız
Mevcut algoritmamız MovieLens-25M'e kadar zorla çalışır. Ötesi için yapısal değişiklik gerek.

Strateji 1: Offline Batch Precomputation#

Fikir#

Item-item similarity matrix statik — kullanıcı her geldiğinde yeniden hesaplamaya gerek yok. Günde 1-2 kez batch olarak hesapla, Redis'e koy.
Cron job (her gece): 1. Tüm rating data'sını oku 2. Item-item similarity matrix hesapla (Spark) 3. Her item için top-100 similar item al 4. Redis'e key-value yaz: item:i:similar → [(j, sim), ...] Online (real-time): 1. User u → Redis'ten history al 2. History'deki her item için Redis'ten similar'lar al 3. Aggregate + filter + top-N 4. 50ms total latency

Niçin Bu Çalışıyor?#

User behavior dataset boyutuyla lineer. Ama item-item similarity matrix subquadratik (top-K filtered). 24 saatte similarity matrix yeniden hesaplanabilir.

Amazon'un 2003 Paper'ının Anahtarı#

Linden et al. 2003 paper'ının ana kontribüsyonu bu fikirdi: "Yapma her online query'de — offline pre-compute, online lookup."
python
# Offline batch script (her gece çalışır)
import redis
 
def offline_precompute_similarity():
"""Tüm rating data → item-item similarity → Redis."""
# 1. Data load (Spark veya Polars, large data)
R = load_full_rating_matrix() # CSR
 
# 2. Similarity compute
S = compute_item_item_similarity(R, top_k=100)
 
# 3. Redis write
r = redis.Redis(host="redis-prod", port=6379)
pipe = r.pipeline()
for item_idx in range(S.shape[0]):
row = S.getrow(item_idx)
neighbors = [(int(row.indices[i]), float(row.data[i])) for i in range(row.nnz)]
# Sort by similarity desc
neighbors.sort(key=lambda x: -x[1])
# JSON or msgpack serialize
pipe.set(f"item_sim:{item_idx}", json.dumps(neighbors), ex=86400 * 2) # 2 day expiry
pipe.execute()
 
 
# Online serving (her API request'te çalışır)
def serve_recommendation(user_id: int, k: int = 10) -> list[int]:
"""Real-time öneri — sadece Redis lookup, 30ms total."""
r = redis.Redis(host="redis-prod", port=6379)
 
# 1. User history (recent N items)
history = r.zrange(f"user_hist:{user_id}", 0, 50, desc=True) # son 50 etkileşim
 
# 2. Her item için neighbors al, score aggregate
score_dict = defaultdict(float)
for item_id in history:
neighbors_json = r.get(f"item_sim:{item_id}")
if not neighbors_json:
continue
for neighbor_id, sim in json.loads(neighbors_json)[:50]:
score_dict[neighbor_id] += sim
 
# 3. History filter + top-K
for h in history:
score_dict.pop(h, None)
 
top_k_items = sorted(score_dict.items(), key=lambda x: -x[1])[:k]
return [item_id for item_id, _ in top_k_items]
 
Offline batch + online serving pattern.

Strateji 2: LSH (Locality-Sensitive Hashing) — Approximate Similarity#

Sorun#

Item-item similarity matrix'i offline'da hesaplamak hala O(N² × d). 10M item için yıllar sürer.

LSH Fikri#

Yakın item'lar, aynı hash bucket'a düşsün. Sadece aynı bucket içindeki item çiftlerini similarity hesapla.
Geleneksel: O(N²) comparison LSH: O(N · log N) comparison (yaklaşık)

MinHash — Jaccard için LSH#

MinHash, set similarity için (Broder 1997):
  1. Her set için K random hash function uygula.
  2. Her hash function'ın minimum değeri signature olur.
  3. İki set'in signature'larındaki eşleşme oranı ≈ Jaccard similarity.
# Pseudo-code def minhash_signature(item_set, n_hashes=128): return [min(hash_i(x) for x in item_set) for i in range(n_hashes)] # İki set'in tahmini Jaccard: estimated_jaccard = sum(s1 == s2 for s1, s2 in zip(sig_a, sig_b)) / len(sig_a)

LSH Banding#

128-bit signature'ı 32 band'a böl (her band 4 hash). Aynı band'da identical value varsa, "candidate similar pair". Sadece candidate pair'leri tam similarity hesapla.

Pratik#

Spark MLlib ve Datasketch kütüphaneleri kullanılır. 100M item için bile günde 1 kez batch fizibıl.

Strateji 3: Distributed Computation — Spark / Ray / Dask#

Niçin Distributed?#

Tek makinede memory tavan: 1-2 TB RAM. 10M item × 10M item similarity bu sınırı aşar.

Spark Pattern#

from pyspark.sql import SparkSession from pyspark.sql.functions import col, count, sum as _sum spark = SparkSession.builder.appName("ItemItemCF").getOrCreate() # Rating DataFrame ratings = spark.read.parquet("s3://my-bucket/ratings.parquet") # Self-join over users — her user için tüm item çiftleri # Bu kısım Spark'ın güçlü yanı — distributed shuffle pairs = ratings.alias("a").join( ratings.alias("b"), (col("a.user_id") == col("b.user_id")) & (col("a.item_id") < col("b.item_id")) ).select( col("a.item_id").alias("item_i"), col("b.item_id").alias("item_j"), col("a.rating").alias("rating_i"), col("b.rating").alias("rating_j"), ) # Aggregate per pair agg = pairs.groupBy("item_i", "item_j").agg( count("*").alias("n_common"), _sum(col("rating_i") * col("rating_j")).alias("dot_product"), # ... daha fazla aggregate ) # Similarity hesapla (UDF) similarity = agg.withColumn("sim", ...) # Top-K per item windowed = similarity.groupBy("item_i").apply(top_k_udf) windowed.write.parquet("s3://my-bucket/item_similarity.parquet")

Performance Beklentisi#

  • 100M rating, 10M item — 50-node Spark cluster — 6-12 saat batch.
  • Sonuç parquet'e yazılır, ayrı bir process Redis'e taşır.

Production Mimari — Tüm Parçalar Birlikte#

┌─────────────────────────────────────────────────┐ │ Offline (her gece, 3-6 saat) │ ├─────────────────────────────────────────────────┤ │ │ │ S3 raw data → Spark cluster (50 node) │ │ ↓ │ │ LSH-based item-item similarity batch │ │ ↓ │ │ Top-K per item → Parquet │ │ ↓ │ │ Pusher process → Redis cluster │ │ │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ Online (real-time, p99 < 50ms) │ ├─────────────────────────────────────────────────┤ │ │ │ User request │ │ ↓ │ │ FastAPI / gRPC service │ │ ↓ │ │ Redis lookup: │ │ - user_hist:{uid} (son 50 etkileşim) │ │ - item_sim:{iid} (her history item için) │ │ ↓ │ │ Aggregate + history filter + business rules │ │ ↓ │ │ Top-N response │ │ │ └─────────────────────────────────────────────────┘

Tipik Latency Budget#

  • Network (round-trip): 5ms
  • Redis 50 lookup (parallel pipeline): 10ms
  • Aggregation in-process: 5ms
  • Business rules + diversity: 10ms
  • Response serialize + send: 5ms
  • Total p99: 35-50ms

🎉 Modül 5 Tamamlandı#

4 ders bitti:
  • 5.1 User-user vs item-item felsefesi
  • 5.2 4 similarity metric + shrinkage trick
  • 5.3 MovieLens-1M üzerinde production-grade item-item k-NN
  • 5.4 100M+ scale için LSH, Spark, offline batch
Benchmark tablomuz şimdi 3 satır:
  • Popularity: 0.064 NDCG@10
  • Content-Based: 0.089
  • Item-Item k-NN CF: 0.117
Bir sonraki modülde Modül 6 — Matrix Factorization. Funk SVD'den BiasedMF'e, ALS'ten implicit ALS'e. NDCG@10'umuz 0.13-0.16'ya çıkacak. Recommender literatürünün en güzel bölümü bizi bekliyor. ☕

Frequently Asked Questions

Evet, ama 16-32GB RAM gerek. ML-25M'de 62K item × 62K = ~4B similarity cell. Sparse'da top-K=100 ile 6.2M entry — 1GB. Computation 30-60 dakika. Memory yetersiz ise chunked computation (item'ları 5K'lık batch'lerde işle).

Yorumlar & Soru-Cevap

(0)
Yorum yazmak için giriş yap.
Yorumlar yükleniyor...

Related Content