Spaces:
Running on Zero
Running on Zero
File size: 7,417 Bytes
e91bc61 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | """
Sahih Islam - 100-Query RAG Cache Hit & Multi-Attribute Capability Benchmark
Evaluates:
1. Cache Hit Rate (%) across 100 diverse real-world questions across all 20 Islamic topics
2. Dynamic Slicing Accuracy (top_k from 1 to 6)
3. Multilingual Localization (Urdu, Arabic, Hinglish, English)
4. Sanad Dynamic Formatting Toggle (include_sanad = True vs False)
5. Ultra-low Latency Metrics (p50, p95, min, max in ms)
"""
import os
import sys
import time
import asyncio
import statistics
from typing import List, Dict, Any
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if project_root not in sys.path:
sys.path.insert(0, project_root)
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, OSError):
pass
from app.infrastructure.redis import redis_manager
from app.rag.cache import get_cached_canonical_documents
from app.rag.top_1000_questions import TOP_1000_QUESTIONS
TOPIC_NAMES = [
"1. Intention, Sincerity & Tawheed",
"2. Purification, Wudu & Miswak",
"3. Ghusl, Menstruation & Tayammum",
"4. Five Daily Prayers, Times & Rulings",
"5. Congregation, Jumu'ah & Masjids",
"6. Sunnah, Nawafil, Tahajjud & Witr",
"7. Missed Prayers, Travel & Forgetfulness",
"8. Fasting Ramadan, Suhur & Iftar",
"9. Voluntary Fasting & Laylatul Qadr",
"10. Zakat, Wealth & Sadaqah",
"11. Hajj, Umrah & Dhul Hijjah",
"12. Nikah, Proposals & Spousal Rights",
"13. Divorce, Iddah & Child Custody",
"14. Parents, Children & Relatives",
"15. Halal & Haram Food, Drink & Habits",
"16. Finance, Business, Riba & Debts",
"17. Character, Speech & Sins of Tongue",
"18. Duas, Adhkar & Repentance",
"19. Waswas, Ruqyah & Evil Eye",
"20. Death, Funeral & The Hereafter"
]
LANGUAGES = ["urdu", "english", "arabic", "hinglish"]
def has_arabic(text: str) -> bool:
"""Check if text contains Arabic Unicode characters."""
return any("\u0600" <= c <= "\u06FF" or "\u0750" <= c <= "\u077F" for c in text)
def has_urdu(text: str) -> bool:
"""Check if text contains Nastaliq/Urdu-specific or Arabic characters."""
return any("\u0600" <= c <= "\u06FF" for c in text)
async def main():
print("=" * 95)
print("SAHIH ISLAM - 100-QUERY COMPREHENSIVE RAG CACHE BENCHMARK")
print("Evaluating: Top-K Slicing (1-6) | Sanad Toggle (True/False) | Multilingual (UR, AR, HING, EN)")
print("=" * 95)
await redis_manager.init()
# Generate 100 distinct queries (5 from each of the 20 categories)
test_queries = []
for cat_idx in range(20):
base_idx = cat_idx * 50
offsets = [0, 10, 20, 30, 40]
for offset in offsets:
q_idx = base_idx + offset
test_queries.append({
"topic": TOPIC_NAMES[cat_idx],
"index": q_idx,
"query": TOP_1000_QUESTIONS[q_idx]
})
assert len(test_queries) == 100, f"Expected 100 queries, got {len(test_queries)}"
results = []
latencies = []
hits = 0
slicing_correct = 0
sanad_correct = 0
language_correct = 0
print(f"\n{'ID':<4} | {'TOPIC':<30} | {'TOP-K':<5} | {'SANAD':<5} | {'LANG':<8} | {'LATENCY':<8} | {'STATUS'}")
print("-" * 95)
for i, item in enumerate(test_queries):
top_k = (i % 6) + 1 # 1 to 6
include_sanad = (i % 2 == 0) # True, False
lang = LANGUAGES[i % 4] # urdu, english, arabic, hinglish
t0 = time.perf_counter()
docs = await get_cached_canonical_documents(
query=item["query"],
top_k=top_k,
language=lang,
include_sanad=include_sanad
)
latency_ms = (time.perf_counter() - t0) * 1000
latencies.append(latency_ms)
if docs and len(docs) > 0:
hits += 1
# 1. Slicing check
if len(docs) == min(top_k, 6):
slicing_correct += 1
# 2. Sanad check on primary document
sample_text = docs[0].get("display_text", "")
sanad_field = docs[0].get("sanad", "")
if include_sanad:
# If sanad is requested and available, it should format with [SANAD]:
if not sanad_field or "[SANAD]:" in sample_text:
sanad_correct += 1
else:
# If sanad is NOT requested, [SANAD]: must NOT appear in display_text
if "[SANAD]:" not in sample_text:
sanad_correct += 1
# 3. Multilingual check
if lang == "urdu":
if has_urdu(sample_text) or docs[0].get("text_urdu"):
language_correct += 1
elif lang == "arabic":
if has_arabic(sample_text) or docs[0].get("text_arabic"):
language_correct += 1
elif lang in ("hinglish", "english"):
# English or transliterated latin script
if len(sample_text) > 0:
language_correct += 1
status = f"✅ HIT ({len(docs)} docs, Ref: {docs[0].get('reference', 'N/A')})"
else:
status = "❌ MISS"
topic_short = item["topic"].split(". ")[1][:28]
sanad_str = "True" if include_sanad else "False"
print(f"#{i+1:03d} | {topic_short:<30} | k={top_k:<3} | {sanad_str:<5} | {lang:<8} | {latency_ms:6.2f}ms | {status}")
results.append({
"id": i + 1,
"query": item["query"],
"topic": item["topic"],
"top_k": top_k,
"include_sanad": include_sanad,
"language": lang,
"hit": bool(docs),
"doc_count": len(docs) if docs else 0,
"latency_ms": latency_ms
})
# Summary Statistics
hit_rate = (hits / len(test_queries)) * 100.0
slicing_acc = (slicing_correct / hits) * 100.0 if hits else 0
sanad_acc = (sanad_correct / hits) * 100.0 if hits else 0
lang_acc = (language_correct / hits) * 100.0 if hits else 0
p50 = statistics.median(latencies)
mean_lat = statistics.mean(latencies)
sorted_lats = sorted(latencies)
p95 = sorted_lats[int(len(sorted_lats) * 0.95)]
min_lat = min(latencies)
max_lat = max(latencies)
print("\n" + "=" * 95)
print("BENCHMARK RESULTS & METRICS SUMMARY")
print("=" * 95)
print(f"Total Questions Evaluated : {len(test_queries)}")
print(f"Total Cache Hits : {hits} / {len(test_queries)} ({hit_rate:.1f}%)")
print(f"Dynamic Slicing Accuracy : {slicing_correct} / {hits} ({slicing_acc:.1f}%) [k=1..6]")
print(f"Sanad Dynamic Toggle Accuracy : {sanad_correct} / {hits} ({sanad_acc:.1f}%)")
print(f"Multilingual Fidelity Accuracy : {language_correct} / {hits} ({lang_acc:.1f}%) [UR, AR, HING, EN]")
print("-" * 95)
print("LATENCY DISTRIBUTION (Direct Upstash Redis over TLS):")
print(f" • Mean Latency : {mean_lat:.2f} ms")
print(f" • Median (P50) Latency : {p50:.2f} ms")
print(f" • 95th Percentile (P95) : {p95:.2f} ms")
print(f" • Fastest Response (Min) : {min_lat:.2f} ms")
print(f" • Peak Response (Max) : {max_lat:.2f} ms")
print("=" * 95)
await redis_manager.close()
if __name__ == "__main__":
asyncio.run(main())
|