फ़ारसी RAG के लिए हाइब्रिड सर्च: व्यावहारिक गाइड

फ़ारसी RAG में हाइब्रिड सर्च: अंग्रेज़ी तरीक़े क्यों विफल होते हैं, PostgreSQL में क्या कमी है, RRF से डेंस व लेक्सिकल रैंक कैसे मिलाएँ, और टेस्ट कैसे करें।

फ़ारसी (Persian/Farsi) RAG के लिए हाइब्रिड सर्च का मतलब है दो रिट्रीवर साथ-साथ चलाना, एक डेंस (dense) एम्बेडिंग सर्च और एक लेक्सिकल (lexical) सर्च, और उनकी रैंकिंग को reciprocal rank fusion (RRF) से मिलाना। दोनों की ज़रूरत इसलिए है क्योंकि फ़ारसी उन अनकही धारणाओं को तोड़ देती है जिन पर अंग्रेज़ी के तरीक़े टिके हैं: एक ही शब्द अरबी या फ़ारसी अक्षरों में स्टोर हो सकता है, शब्दों के भीतर zero-width non-joiner बैठा होता है, और फ़ारसी स्टेमर न Snowball के साथ आता है, न PostgreSQL के साथ। संक्षेप में: स्टोर करते समय टेक्स्ट नॉर्मलाइज़ करें, लेक्सिकल हिस्से को स्टेमर के बिना रखें, स्कोर नहीं बल्कि रैंक मिलाएँ, भाषा (locale) के आधार पर कभी फ़िल्टर न करें, दस्तावेज़ को उसकी अपनी संरचना के हिसाब से चंक करें, और इनमें से किसी पर भी भरोसा करने से पहले द्विभाषी प्रश्न-सेट पर टेस्ट करें।

यह गाइड दो सिस्टम से निकली है। पहला इस साइट का सहायक है, जिसे मैंने बनाया है और जिसका रखरखाव मैं करता हूँ। दूसरा Kartez Contract है, जहाँ मैंने AI इंजीनियर के रूप में काम किया; उसके कोपायलट को फ़ारसी अनुबंधों के भीतर वे सही खंड ढूँढने होते थे, जिनकी अपनी क्रमांकन-व्यवस्था अनुच्छेद, खंड और टिप्पणी की होती है। यहाँ जो रिट्रीवल डिज़ाइन बताया गया है (डेंस और लेक्सिकल, रैंक फ़्यूज़न, pgvector) वह इस साइट का है; Kartez के बारे में मैं सिर्फ़ वही कहता हूँ जो उसकी केस स्टडी कहती है। इनमें से किसी भी सिस्टम का बेंचमार्क यहाँ नहीं है।

फ़ारसी के लिए हाइब्रिड रिट्रीवल: स्टोर करते समय चंक नॉर्मलाइज़ और एम्बेड किए जाते हैं; डेंस और लेक्सिकल सूचियाँ RRF से मिलाई जाती हैं और भाषा सिर्फ़ सटीक बराबरी तोड़ती है; भाषा मॉडल को कॉल करने से पहले समानता की न्यूनतम सीमा जाँची जाती है: उससे नीचे, मेरे बारे में प्रश्न को साइट के रिकॉर्ड मिलते हैं और बाक़ी हर प्रश्न अस्वीकार कर दिया जाता है।

अंग्रेज़ी RAG के तरीक़े फ़ारसी टेक्स्ट पर विफल क्यों होते हैं

आम विफलता ग़लत उत्तर नहीं, बल्कि कोई उत्तर न मिलना है: सर्च वह शब्द नहीं ढूँढ पाता जो उपयोगकर्ता को दस्तावेज़ में साफ़ दिख रहा है, क्योंकि एक जैसी दिखने वाली दो स्ट्रिंग भीतर से अलग होती हैं।

एक शब्द, दो स्ट्रिंग: अरबी और फ़ारसी के Yeh और Kaf

फ़ारसी और अरबी की लिपि एक है, पर हर code point एक नहीं। फ़ारसी ی (U+06CC ARABIC LETTER FARSI YEH) और अरबी Yeh (U+064A ARABIC LETTER YEH) दो अलग-अलग कैरेक्टर हैं, और यही बात फ़ारसी ک (U+06A9 ARABIC LETTER KEHEH) और अरबी Kaf (U+0643 ARABIC LETTER KAF) पर भी लागू होती है। W3C के Arabic and Persian Layout Requirements Yeh वाली जोड़ी का सीधे नाम लेते हैं: यह अक्षर “दो अलग-अलग कैरेक्टर से दर्शाया जाता है”। एक रूप में स्टोर किया गया पैसेज दूसरे रूप में टाइप किए गए प्रश्न से मेल नहीं खाता; ऊपर के डायग्राम में नॉर्मलाइज़ वाला चरण दोनों जोड़ियाँ दिखाता है।

यह साइट किसी पैसेज के रिट्रीवल इंडेक्स में जाने से पहले, दूसरे कैरेक्टरों के साथ-साथ, अरबी Yeh, Alef Maksura और Kaf को उनके फ़ारसी रूपों में फ़ोल्ड करती है, यानी एक ही रूप में बदल देती है; उस कॉपी से अदृश्य bidi चिह्न हटाती है; और हर प्रश्न पर भी यही फ़ोल्डिंग चलाती है।

zero-width non-joiner (ZWNJ) शब्द का हिस्सा है

می‌شود, میشود और می شود तीन अलग-अलग स्ट्रिंग हैं। पहली स्ट्रिंग में उपसर्ग को क्रिया से zero-width non-joiner (ZWNJ, U+200C) अलग करता है, जो स्पेस जोड़े बिना अक्षरों को आपस में जुड़ने से रोकता है। Unicode Standard इस ग़ैर-डिफ़ॉल्ट जुड़ाव का वर्णन ऐसे करता है, “जैसा कि अरबी लिपि में फ़ारसी लिखते समय कभी-कभी ज़रूरी होता है”, और W3C की आवश्यकताएँ कहती हैं कि कंट्रोल कैरेक्टर “टेक्स्ट को स्टोर और प्रदर्शित करते समय सुरक्षित रखे जाने चाहिए”।

ZWNJ हटा दें, जैसा कोई अंग्रेज़ी पाइपलाइन कर सकती है, तो می‌شود बन जाता है میشود; उसे स्पेस में बदल दें, तो एक शब्द दो टोकन बन जाता है। फ़ारसी NLP टूलकिट hazm के normaliser में “ZWNJ correction” शामिल है, जो کتاب های को کتاب‌های में बदल देता है। इस साइट पर स्टोर किया गया टेक्स्ट ZWNJ को बनाए रखता है, और सिर्फ़ तुलना के भीतर इस्तेमाल होने वाली फ़ोल्डिंग उसे स्पेस मानती है।

अंकों के तीन सेट

फ़ारसी ۱۲۳ को Eastern Arabic-Indic अंकों (U+06F0 से U+06F9) में लिखती है, अरबी उसी संख्या को Arabic-Indic अंकों (U+0660 से U+0669) में लिखती है, और तकनीकी टेक्स्ट में अक्सर 123 लिखा जाता है।

अंकों को सिर्फ़ तुलना के लिए फ़ोल्ड करें, स्टोर किए गए टेक्स्ट में कभी नहीं; इस साइट पर स्टोर किए गए फ़ारसी अंकों को बदलकर लिखने से फ़ारसी CV के दिखने का ढंग बदल जाएगा। लेकिन फ़ोल्डिंग दोनों तरफ़ चलनी चाहिए: सिर्फ़ प्रश्न के अंक फ़ोल्ड करें, तो फ़ारसी अंकों में लिखा प्रश्न उसी पैसेज से मेल नहीं खाएगा जिससे उसे कॉपी किया गया था। स्ट्रीमिंग LLM गार्डरेल पर लेख मॉडल के आउटपुट में आने वाली संख्याओं पर यही फ़ोल्डिंग लागू करता है।

डायक्रिटिक्स कम इस्तेमाल होते हैं, इसलिए एक भटका हुआ चिह्न सटीक मिलान तोड़ देता है

W3C की आवश्यकताएँ साफ़ कहती हैं: “अरबी लिपि के टेक्स्ट में स्वर की जानकारी और व्यंजन को लंबा करने के लिए डायक्रिटिक्स का इस्तेमाल असामान्य है।” इसलिए किसी पैसेज में लगा एक अकेला कसरा (ह्रस्व स्वर का चिह्न), या तश्दीद (व्यंजन को दोहराने का चिह्न) के साथ पेस्ट किया गया प्रश्न, बिना चिह्न वाली वर्तनी के साथ हर सटीक मिलान में विफल हो जाता है। MCI के लिए फ़ारसी टेक्स्ट-टू-स्पीच शोध में, जिस पर मैंने काम किया, स्पीच की तरफ़ से यही विशेषता सबसे कठिन हिस्सा थी: ह्रस्व स्वर और इज़ाफ़त (ezāfe), यानी संज्ञा को उसके विशेषण से जोड़ने वाला स्वर, लिखे ही नहीं जाते। रिट्रीवल के लिए डायक्रिटिक्स को तुलना के भीतर हटाएँ और स्टोर किए गए टेक्स्ट में रहने दें।

उपयोगकर्ता बोलचाल में पूछते हैं, दस्तावेज़ औपचारिक भाषा में लिखे होते हैं

“फ़ारसी उन भाषाओं में से है जिनकी लेखन की औपचारिक और अनौपचारिक शैलियों में बहुत अंतर है,” 50,000 वाक्य-युग्मों वाले एक समानांतर कॉर्पस का परिचय देते हुए Tajalli, Shamsfard और Kalantari लिखते हैं। उपयोगकर्ता می‌تونم टाइप करता है, जबकि औपचारिक टेक्स्ट में می‌توانم लिखा होता है। इसका परिणाम मैं ऐसे समझता हूँ: लेक्सिकल मैचर को बोलचाल वाले प्रश्न और औपचारिक खंड में बहुत कम साझा शब्द दिखते हैं, इसलिए मौक़ा डेंस रिट्रीवर के पास ही है, और अच्छी तरह गढ़े गए प्रश्नों वाला टेस्ट सेट इस अंतर को छिपा देगा।

फ़ारसी के लिए सुरक्षित नॉर्मलाइज़ेशन

  • स्टोर करते समय अरबी Yeh और Kaf को फ़ारसी ی और ک में फ़ोल्ड करें, और सिर्फ़ फ़ारसी सामग्री में।

  • स्टोर किए गए टेक्स्ट में ZWNJ बनाए रखें।

  • bidi चिह्न उस कॉपी से हटाएँ जिसे आप इंडेक्स करते हैं और जिससे तुलना करते हैं, दिखाए जाने वाले टेक्स्ट से नहीं।

  • अंकों की फ़ोल्डिंग और डायक्रिटिक्स हटाने का काम सिर्फ़ तुलना के भीतर करें, और दोनों तरफ़।

  • प्रश्नों पर ठीक वही फ़ंक्शन चलाएँ जो पैसेज पर चलाते हैं।

  • अरबी टेक्स्ट पर फ़ारसी फ़ोल्डिंग कभी लागू न करें; वहाँ अरबी Yeh (U+064A) और Kaf (U+0643) ही सही अक्षर हैं।

इस साइट का अपना रिट्रीवर अभी इस सूची पर पूरा नहीं उतरता: वह फ़ारसी अक्षर-रूपों की फ़ोल्डिंग हर भाषा पर लागू करता है, अरबी पर भी; उसकी लेक्सिकल सूची फ़ारसी अंकों में लिखे पैसेज को चूक सकती है; और अरबी लिपि के डायक्रिटिक्स प्रश्न के शब्दों से और जवाब जाँचने वाले गेट की हर तुलना से हटा दिए जाते हैं, पर उस संग्रहीत टेक्स्ट से नहीं जिसमें कीवर्ड सर्च खोजता है। इस सूची को लक्ष्य की तरह पढ़ें।

PostgreSQL में फ़ारसी फ़ुल-टेक्स्ट सर्च: कोई बिल्ट-इन कॉन्फ़िगरेशन नहीं

हाइब्रिड के लेक्सिकल आधे हिस्से के लिए फ़ुल-टेक्स्ट सर्च स्वाभाविक विकल्प है, और PostgreSQL का डॉक्यूमेंटेशन कहता है कि यह “कई भाषाओं के लिए पहले से तय कॉन्फ़िगरेशन के साथ आता है”। फ़ारसी उनमें शामिल नहीं है।

Snowball में अरबी स्टेमर है, फ़ारसी नहीं

Snowball की स्टेमिंग एल्गोरिदम सूची में अरबी है, पर Persian या Farsi की कोई प्रविष्टि नहीं। PostgreSQL अपनी स्टेमिंग डिक्शनरी Snowball से बनाता है: PostgreSQL 18 के सोर्स की भाषा-सूची में arabic है, persian नहीं, और डॉक्यूमेंटेशन की डिक्शनरी सूची में arabic_stem और simple तो दिखती हैं, पर कोई फ़ारसी डिक्शनरी नहीं। अरबी स्टेमर फ़ारसी के साथ लिपि साझा करता है, व्याकरण नहीं। डेटाबेस के बाहर hazm एक फ़ारसी स्टेमर और लेमैटाइज़र देता है; डेटाबेस के भीतर कोई नहीं है।

simple कॉन्फ़िगरेशन क्या देता है, और क्या नहीं

PostgreSQL के डिक्शनरी डॉक्यूमेंटेशन के शब्दों में, simple डिक्शनरी टेम्पलेट “इनपुट टोकन को लोअर केस में बदलकर और उसे स्टॉप वर्ड्स की एक फ़ाइल से जाँचकर” काम करता है। बिल्ट-इन simple डिक्शनरी के पास कोई स्टॉप-वर्ड फ़ाइल नहीं है, इसलिए हर टोकन बना रहता है, और स्टेमिंग भी नहीं होती: قرارداد और قراردادها दो अलग लेक्सीम रहते हैं — recall का नुक़सान, पर अनुमान-योग्यता (predictability) का फ़ायदा।

कोई भी डिक्शनरी चलने से पहले parser तय करता है कि शब्द क्या है, और अक्षर की उसकी समझ “डेटाबेस की locale सेटिंग, ख़ास तौर पर lc_ctype, से तय होती है”। इसलिए यह मानकर न चलें कि आपका डेटाबेस फ़ारसी को कैसे बाँटता है, ख़ासकर ZWNJ से जुड़े शब्दों को। डेटाबेस से ही पूछें, टेक्स्ट सर्च की डीबगिंग वाले डॉक्यूमेंटेशन के ts_debug फ़ंक्शन से:

-- How does this database tokenise Persian? Check; do not assume.
SELECT alias, token, lexemes
FROM ts_debug('simple', 'قراردادها را می‌توان فسخ کرد');

इस साइट का कीवर्ड सर्च फ़ारसी पंक्तियों के लिए simple और अंग्रेज़ी पंक्तियों के लिए english इस्तेमाल करता है। सिमैंटिक बनाम कीवर्ड खोज डेमो इसे इस साइट की सामग्री पर सहायक के हाइब्रिड रिट्रीवर के साथ-साथ चलाता है।

टाइपो के लिए ट्राइग्राम समानता

pg_trgm एक्सटेंशन तीन-तीन कैरेक्टर के अनुक्रमों (trigrams) की तुलना करता है, इसलिए ग़लत वर्तनी वाला शब्द भी सही शब्द के साथ ट्राइग्राम साझा करता है। यह “ट्राइग्राम निकालते समय non-word कैरेक्टर (non-alphanumerics) को नज़रअंदाज़ करता है”, इसलिए उस पर भरोसा करने से पहले जाँच लें: SELECT show_trgm('قرارداد'); को फ़ारसी अक्षरों से बने ट्राइग्राम लौटाने चाहिए। साइट का कीवर्ड सर्च एक word_similarity शाखा जोड़ता है, जो छोटी क्वेरी का मिलान लंबे पैसेज के सबसे अच्छे लगातार हिस्से से करती है।

पूरे शब्दों की कवरेज: इस साइट के रिट्रीवर की लेक्सिकल सूची

सहायक का रिट्रीवर अपनी लेक्सिकल सूची के लिए फ़ुल-टेक्स्ट सर्च का इस्तेमाल नहीं करता, क्योंकि कोई फ़ारसी कॉन्फ़िगरेशन है ही नहीं। वह गिनता है कि प्रश्न के कितने सार्थक शब्द (content words) किसी पैसेज में पूरे शब्दों के रूप में आते हैं, regular expression की word boundaries के साथ। वह साइट की हर भाषा के आम शब्द (stop words) हटा देता है, बहुत छोटे शब्दों को छोड़ देता है, हिंदी की मात्राओं को उसी शब्द के भीतर रखता है जिसका वे हिस्सा हैं, और कोई स्टेमिंग नहीं करता। नतीजा 0 और 1 के बीच का एक कवरेज अनुपात है।

Word boundaries मायने रखती हैं: “is” पर substring मिलान “his”, “this” और “Isfahan” को भी पकड़ लेता है, और हर पैसेज साक्ष्य जैसा दिखने लगता है। अनुपात का एक तय दायरे में बँधा होना भी मायने रखता है, क्योंकि समानता की न्यूनतम सीमा से उसकी तुलना की जा सकती है। इनकार वाली न्यूनतम सीमा में यह अनुपात तभी गिना जाता है जब कम से कम दो अलग-अलग सार्थक शब्द मेल खाएँ।

सीखे हुए स्पार्स वेक्टर: डॉक्यूमेंटेड विकल्प

Learned sparse retrieval, यानी सीखे हुए स्पार्स वेक्टर से रिट्रीवल, लेक्सिकल सर्च की तरह शब्दों का मिलान करता है, पर उनके वज़न एक मॉडल सीखता है। BGE-M3 में ये शब्द टोकनाइज़र के टुकड़े होते हैं — उसका मॉडल कार्ड 'De', 'fin' और 'ation' को वज़न देता है — और वेक्टर, इसे पेश करने वाले पेपर के शब्दों में, “वोकैबुलरी के आकार के बराबर” होता है: मॉडल के कॉन्फ़िगरेशन में 2,50,002। इस साइट का प्रोडक्शन रिट्रीवर इनका इस्तेमाल नहीं करता: वह डेंस और लेक्सिकल चलाता है, और जिस एम्बेडिंग API को वह कॉल करता है, वह कोई स्पार्स वज़न नहीं लौटाता। अगर आप इन्हें जोड़ते हैं, तो पहले नीचे off-by-one वाला सेक्शन पढ़ें।

reciprocal rank fusion से हाइब्रिड सर्च: स्कोर नहीं, रैंक मिलाएँ

डेंस रिट्रीवर और लेक्सिकल रिट्रीवर रैंक की हुई सूचियाँ लौटाते हैं, जिनके स्कोर का अर्थ अलग-अलग होता है। Reciprocal rank fusion स्कोर को नज़रअंदाज़ करके स्थानों (positions) को जोड़ता है।

फ़ॉर्मूला, और k = 60 कहाँ से आता है

Cormack, Clarke और Büttcher किसी दस्तावेज़ के फ़्यूज़्ड स्कोर को सभी रैंकिंग पर 1/(k + r(d)) के योग के रूप में परिभाषित करते हैं, जहाँ r(d) हर सूची में उसकी रैंक है। यह स्थिरांक जितना नाज़ुक दिखता है, उतना है नहीं: “k = 60 एक पायलट जाँच के दौरान तय किया गया और बाद के सत्यापन में बदला नहीं गया”, और पायलट ने “संकेत दिया कि k = 60 लगभग इष्टतम था, पर यह चुनाव निर्णायक नहीं था”।

इस साइट का रिट्रीवर k = 60 इस्तेमाल करता है, और हर सूची उत्तर में इस्तेमाल होने वाले उम्मीदवारों से ज़्यादा उम्मीदवार देती है, ताकि जो पैसेज एक सूची के बीच में हो और दूसरी सूची में सबसे ऊपर के पास, वह भी ऊपर आ सके। मोटे तौर पर:

K = 60  # Cormack, Clarke and Büttcher

def fuse(lists, k=K):
    """lists: (weight, passage ids best-first) for each retriever."""
    scores = {}
    for weight, ranked in lists:
        for rank, passage in enumerate(ranked, start=1):
            scores[passage] = scores.get(passage, 0.0) + weight / (k + rank)
    return scores

def order(scores, locales_of, query_locale):
    # The fused score decides. Locale only separates exactly equal
    # scores, and the id keeps the order identical on every run.
    return sorted(scores, key=lambda p: (-scores[p], query_locale not in locales_of[p], str(p)))

fused = fuse([(1.0, dense_ids), (0.5, lexical_ids)])
passages = order(fused, locales_of, query_locale="fa")[:top_k]

स्कोर जोड़ने के लिए ऐसे वज़न क्यों चाहिए जिन्हें ट्यून करना पड़े

दूसरा रास्ता है स्कोर को जोड़ना, जैसा BGE-M3 वाला पेपर करता है: s_rank ← w1·s_dense + w2·s_lex + w3·s_mul, “जहाँ w1, w2 और w3 के मान downstream परिदृश्य पर निर्भर करते हैं”। डेंस और स्पार्स के लिए लेखक MIRACL पर w1 = 1 और w2 = 0.3 रखते हैं, और लंबे दस्तावेज़ों के बेंचमार्क MLDR पर w1 = 0.2 और w2 = 0.8।

Cosine similarity, स्पार्स inner product और कवरेज अनुपात अलग-अलग पैमानों पर होते हैं, और उनके बीच की विनिमय दर कॉर्पस के साथ बदलती रहती है। रैंक फ़्यूज़न इस कैलिब्रेशन से बच निकलता है। यह एक समझौता है, मुफ़्त की जीत नहीं: पेपर वज़न वाले योग (weighted sums) के साथ मज़बूत नतीजे बताता है, और RRF भारी जीत और मामूली बढ़त में फ़र्क़ नहीं कर सकता।

लेक्सिकल सूची को आधा वज़न क्यों मिलता है

इस साइट पर लेक्सिकल सूची उन पैसेज को ऊपर लाने के लिए है जिनमें ऐसी सटीक स्ट्रिंग हैं जिन्हें डेंस मॉडल धुँधला कर सकता है: booqai.app जैसा कोई डोमेन नाम, कोई संक्षिप्त नाम (acronym), SAMPAD जैसा किसी कार्यक्रम का नाम। लेक्सिकल सूची रखने की आम वजह यही होती है। Contextual Retrieval पर Anthropic का लेख यही बात कहता है: एम्बेडिंग मॉडल “सामान्य रूप से error codes के बारे में सामग्री तो ढूँढ सकता है, पर ठीक 'TS-999' वाला मिलान चूक सकता है।”

लेक्सिकल सूची आधे वज़न पर योगदान देती है, जिससे डेंस सूची में भी आया कोई पैसेज और ऊपर उठ जाता है, पर कीवर्ड का मेल डेंस रैंकिंग को पछाड़ नहीं पाता। इसकी क़ीमत असली है: k = 60 और इस साइट की उम्मीदवार-संख्याओं के साथ, जो पैसेज सिर्फ़ लेक्सिकल सूची को मिलता है वह हर डेंस उम्मीदवार से नीचे रहता है, इसलिए फ़्यूज़न उस पैसेज को वापस नहीं ला सकता जिसे डेंस मॉडल पूरी तरह चूक गया हो। इसका एक ही अपवाद है: सबसे ज़्यादा समानता वाला पैसेज, जो हमेशा लौटाया जाता है, जैसा अगला सेक्शन बताता है। अगर एम्बेडिंग कॉल विफल हो जाए, तो रिट्रीवल सिर्फ़ लेक्सिकल सूची पर लौट आता है, और समानता की न्यूनतम सीमा तब भी लागू रहती है।

RRF स्कोर भरोसे (confidence) का माप नहीं है

RRF स्कोर सिर्फ़ स्थानों से बनता है, इसलिए पूरी तरह शोर (noise) से भरा कॉर्पस भी पहली रैंक का एक पैसेज देगा, जिसके पास उपलब्ध सबसे अच्छा फ़्यूज़्ड स्कोर होगा। इसलिए इस साइट की इनकार वाली न्यूनतम सीमा की तुलना हर पैसेज की एक असली समानता से की जाती है, फ़्यूज़्ड स्कोर से कभी नहीं: पूछे गए प्रश्न के साथ उसकी cosine similarity (और follow-up प्रश्न में, पिछले प्रश्न से जोड़े गए प्रश्न के साथ भी; अगर वह किसी सूची में स्थान के रूप में पूछा गया हो, तो इस जोड़ में वह आइटम भी शामिल होता है जिसकी ओर वह इशारा करता है, और अगर वह आइटम पढ़ा न जा सके, तो पिछले जवाब की शुरुआत) और उसकी पूरे शब्दों की कवरेज में से जो बेहतर हो, जिसमें कवरेज तभी गिनी जाती है जब कम से कम दो सार्थक शब्द मेल खाएँ। यह जाँच भाषा मॉडल को कॉल करने से पहले होती है।

सभी उम्मीदवारों में सबसे ज़्यादा समानता वाला पैसेज हमेशा लौटाए गए पैसेजों में होता है, भले ही फ़्यूज़न ने उसे इतना नीचे रखा हो कि वह चुना न जाता। सीमा से नीचे, जो प्रश्न मेरे बारे में नहीं है उसे मॉडल को कॉल किए बिना अस्वीकार कर दिया जाता है, और मेरे बारे में प्रश्न को इसकी जगह साइट के रिकॉर्ड दिए जाते हैं; RAG हैलुसिनेशन गार्डरेल पर लेख दोनों को विस्तार से बताता है।

रिट्रीवल को भाषा (locale) के आधार पर कभी फ़िल्टर न करें

भाषा का फ़िल्टर सुव्यवस्थित लगता है: फ़ारसी प्रश्न फ़ारसी पैसेज में खोजें। द्विभाषी कॉर्पस पर यह साक्ष्य मिटा देता है।

आपका सबसे अच्छा साक्ष्य दूसरी भाषा में हो सकता है

इस साइट पर हर भाषा के पैसेज एक ही टेबल में साथ-साथ रहते हैं, और एक प्रश्न उन सब में खोजता है। फ़ारसी CV मूल दस्तावेज़ है और अंग्रेज़ी CV उसका रूपांतर, इसलिए किसी भूमिका के बारे में अंग्रेज़ी प्रश्न का सबसे अच्छा साक्ष्य फ़ारसी में हो सकता है। Kartez Contract में यही स्थिति उलटी दिशा में थी: उसके मसौदा विज़ार्ड को फ़ारसी में बताई गई ज़रूरतों को अनुबंध-लेखन के अंग्रेज़ी ग्रंथों की एक लाइब्रेरी तक ले जाना होता था।

टाईब्रेक के तौर पर भाषा: सिर्फ़ सटीक बराबरी के लिए

रैंकिंग में भाषा का फिर भी एक उपयोग है: ठीक एक जैसे फ़्यूज़्ड स्कोर वाले दो पैसेज में से पहले वह आता है जो प्रश्न की भाषा में उपलब्ध है, और उसके बाद एक स्थिर id तय करता है, जैसा ऊपर की sort key में है। एक डेंस सूची हो और लेक्सिकल सूची आधे वज़न पर, तो सटीक बराबरी व्यवहार में होती ही नहीं। यह तब हो सकती है जब प्रश्न को एक से ज़्यादा रूपों में खोजा जाए, जैसा आगे बताया गया है, और दो पैसेज उन रूपों की बराबर वज़न वाली डेंस सूचियों से एक जैसी रैंक पा लें; तब भी भाषा तभी तय करती है जब दोनों में से सिर्फ़ एक प्रश्न की भाषा में उपलब्ध हो। यह जान-बूझकर है: भाषा को कभी स्कोर से ऊपर नहीं होना चाहिए। भाषा का एक और काम है: यह चुनना कि पाठक किसी पैसेज का कौन-सा अनुवाद देखे।

एक ही भाषा वाले पैसेज को छोटा-सा बोनस दे देना टाईब्रेक नहीं है। पूरे वज़न वाली एक सूची में आस-पास की रैंकों के योगदान का अंतर 1/((k + r)(k + r + 1)) होता है, और यह अंतर सूची में नीचे जाते-जाते और घटता है; दो सूचियों से बने योग इससे भी पास बैठ सकते हैं। कोई तय बोनस तभी तक सुरक्षित है जब तक वह उन सारे अंतरों से छोटा रहे जो आपकी सूची-गहराइयाँ और वज़न पैदा कर सकते हैं — और यह सीमा इनमें से कुछ भी बदलते ही खिसक जाती है। जैसे ही बोनस उस सीमा से ऊपर जाता है, जो नतीजे बराबर नहीं थे वे सूची के बीच में जगह बदल लेते हैं, ठीक उन्हीं उम्मीदवारों के बीच जिन्हें ऊपर लाने के लिए फ़्यूज़न बना है।

एक पैसेज, दस अनुवाद

इस साइट के पेज दस भाषाओं तक में प्रकाशित होते हैं, और अलग-अलग भाषाओं में एक ही पैसेज की प्रतियाँ मॉडल को दी जाने वाली आठ जगहों में से कई घेर सकती थीं। अब रिट्रीवल किसी पैसेज के अनुवादों को एक ही उम्मीदवार मानता है: उसे उसके सबसे अच्छे अनुवाद से आँका जाता है, एक बार रैंक किया जाता है, और अगर पाठक की भाषा में अनुवाद मौजूद हो तो उसी भाषा में दिखाया जाता है। लेखों को चंक के बजाय सेक्शन के हिसाब से समूहित किया जाता है, क्योंकि एक लंबा सेक्शन हर भाषा में अलग संख्या के चंक में बँटता है; जिस अनुवाद के सेक्शन अंग्रेज़ी संस्करण के सेक्शन से मेल नहीं खाते, उसे ग़लत समूह में डालने के बजाय अलग रखा जाता है। पाठक की भाषा तय करती है कि कौन-सा अनुवाद दिखे; सटीक बराबरी को छोड़कर, वह कभी तय नहीं करती कि पैसेज रैंकिंग में कहाँ आए।

अंग्रेज़ी दस्तावेज़ों पर फ़ारसी प्रश्न

भाषा की सीमा सिर्फ़ डेंस हिस्सा पार कर सकता है: फ़ारसी प्रश्न और अंग्रेज़ी पैसेज में साझा शब्द लगभग नहीं होते, RAG जैसे लैटिन शब्दों को छोड़कर। BGE-M3 का अपना मूल्यांकन फ़ारसी के लिए यह सवाल हल नहीं करता: उसका cross-lingual बेंचमार्क, MKQA, 25 गैर-अंग्रेज़ी भाषाओं की क्वेरी के लिए अंग्रेज़ी Wikipedia के पैसेज रिट्रीव करता है, और उनमें अरबी है, फ़ारसी नहीं। अपने ख़ुद के फ़ारसी–अंग्रेज़ी जोड़ों पर टेस्ट करें।

बिना कीवर्ड वाले प्रश्न

किसी विज़िटर के प्रश्न “उन्होंने हाल ही में क्या किया है?” में ऐसा कोई शब्द नहीं जिससे कॉर्पस मेल खा सके, और “और उससे पहले?” में तो कुछ भी नहीं है। जब प्रश्न मेरे बारे में हो, तो उसे मेरे नाम के साथ भी खोजा जाता है, और follow-up प्रश्न को पिछले प्रश्न से जोड़कर भी खोजा जाता है; अगर वह किसी सूची में स्थान के रूप में पूछा गया हो, जैसे “दूसरा”, तो दोनों के बीच वह आइटम भी रखा जाता है जिसकी ओर वह इशारा करता है, और अगर वह आइटम पढ़ा न जा सके, तो पिछले जवाब की शुरुआत। सभी रूपों को एक ही कॉल में एम्बेड किया जाता है और रैंक के आधार पर मिलाया जाता है। मेरे नाम वाला रूप पैसेजों का क्रम तय करता है, पर इनकार वाली न्यूनतम सीमा में कभी नहीं गिना जाता, क्योंकि सिर्फ़ मेरा नाम मेरे बारे में ऐसे प्रश्न को भी, जिसका मेरे काम से कोई संबंध नहीं, मेरे CV के क़रीब दिखा सकता है। मेरे पूरे काम के बारे में प्रश्न रिट्रीवल पर निर्भर नहीं करते: उनके जवाब उन तारीख़ वाले रिकॉर्ड से बनते हैं जिन्हें साइट मेरे CV, केस स्टडी, लेखों और सेवाओं से तैयार करती है, जैसा RAG हैलुसिनेशन गार्डरेल पर लेख बताता है।

फ़ारसी अनुबंधों की चंकिंग: अनुच्छेद, खंड और टिप्पणी के हिसाब से

फ़ारसी अनुबंधों की अपनी संरचना होती है: अनुच्छेद (ماده), खंड (بند) और टिप्पणियाँ (تبصره)। Kartez Contract के कोपायलट में उपयोगकर्ता का अपना अनुबंध इसी संरचना के हिसाब से बाँटा जाता था, और किसी प्रश्न का उत्तर देने के लिए सिर्फ़ उससे जुड़े हिस्से इस्तेमाल होते थे; उत्तर “केवल चुने हुए खंडों पर आधारित” होते हैं। Kartez Contract केस स्टडी बाक़ी प्रोजेक्ट को कवर करती है, और लीगल AI हैलुसिनेशन के ख़िलाफ़ क्या बनाना चाहिए इस क्षेत्र को कवर करता है।

यही सिद्धांत इस साइट के chunker को चलाता है: एक चंक एक अर्थपूर्ण रिकॉर्ड है। CV की कोई भूमिका उसकी तारीख़ों से कभी अलग नहीं होती, केस स्टडी अपने लिखे हुए सेक्शन पर बँटती है, पोस्ट अपने हेडिंग पर, और लंबा सेक्शन सिर्फ़ पैराग्राफ़ की सीमा पर टूटता है। हर सेक्शन-चंक अपने साथ दस्तावेज़ का शीर्षक ले जाता है, ताकि अकेले रिट्रीव हुआ चंक भी बता सके कि वह कहाँ से आया है।

फ़ारसी अनुबंधों के लिए इससे कुछ नियम निकलते हैं, जो सामान्य सलाह के रूप में दिए गए हैं, इस बात के वर्णन के रूप में नहीं कि Kartez कैसे बना था:

  • संख्या को टेक्स्ट के साथ रखें। “ماده ۵” और उसका मुख्य भाग एक ही चंक में होने चाहिए।

  • मूल अनुच्छेद का हेडिंग हर खंड और टिप्पणी वाले चंक में साथ ले जाएँ।

  • खंड को कभी टोकन की गिनती पर न काटें। अपने खंड से अलग हुई टिप्पणी यह बदल सकती है कि वह खंड क्या कहता प्रतीत होता है।

  • संरचनात्मक पथ स्टोर करें, अनुच्छेद › खंड › टिप्पणी, ताकि उत्तर किसी पेशेवर को ठीक उसी जगह तक ले जा सके।

pgvector: सटीक स्कैन या HNSW?

आधा जवाब pgvector ख़ुद देता है: “डिफ़ॉल्ट रूप से pgvector exact nearest neighbor search करता है, जो perfect recall देता है।” अनुमानित (approximate) इंडेक्स “गति के बदले कुछ recall छोड़ देता है”।

कम स्पष्ट क़ीमत फ़िल्टर की है। “Approximate इंडेक्स में फ़िल्टरिंग इंडेक्स स्कैन होने के बाद लागू होती है। अगर कोई शर्त 10% पंक्तियों से मेल खाती है, तो HNSW और hnsw.ef_search के डिफ़ॉल्ट मान 40 के साथ औसतन सिर्फ़ 4 पंक्तियाँ मेल खाएँगी।” pgvector 0.8.0 और उसके बाद के संस्करण यह सुविधा चालू करने पर इंडेक्स को बार-बार (iteratively) तब तक स्कैन कर सकते हैं जब तक पर्याप्त पंक्तियाँ न मिल जाएँ।

इस साइट की रिट्रीवल टेबल पर कोई अनुमानित इंडेक्स नहीं है: वेक्टर सर्च एक सटीक स्कैन है। प्रकाशन की स्थिति SQL के WHERE क्लॉज़ का हिस्सा है, LIMIT के बाद लगाया गया फ़िल्टर कभी नहीं; वहाँ वह नतीजों की संख्या चुपचाप घटा देता है। सटीक स्कैन से शुरुआत करें, latency की माँग हो तब इंडेक्स जोड़ें, और उस पर भरोसा करने से पहले अपने ख़ुद के प्रश्नों पर उसके top-k की तुलना सटीक स्कैन से करें।

pgvector के sparsevec में off-by-one की चूक

यह सिर्फ़ तभी मायने रखता है जब आप PostgreSQL में सीखे हुए स्पार्स वेक्टर जोड़ें, और इसकी विफलता चुपचाप होती है।

pgvector में स्पार्स वेक्टर का टेक्स्ट फ़ॉर्मैट {index1:value1,index2:value2}/dimensions है, “और indices SQL arrays की तरह 1 से शुरू होते हैं”। टोकनाइज़र की ids 0 से शुरू होती हैं। Python क्लाइंट 0 से शुरू होने वाले indices लेता है (“Indices start at 0”) और टेक्स्ट रूप लिखते समय ख़ुद 1 जोड़ देता है।

from pgvector import SparseVector

weights = {0: 0.05, 2: 0.14, 4: 0.04}  # tokeniser ids, 0-based

# Client library: pass 0-based ids; it adds 1 when it writes SQL text.
vec = SparseVector(weights, 6)

# Hand-built SQL literal: shift exactly once, here.
literal = "{" + ",".join(f"{i + 1}:{w}" for i, w in weights.items()) + "}/6"
# "{1:0.05,3:0.14,5:0.04}/6"

Index को ठीक एक बार खिसकाएँ, वहीं जहाँ SQL literal हाथ से बनाया जाता है। pgvector का text input हर index को 0 से शुरू होने वाली गिनती में बदलता है और “sparsevec index out of bounds” त्रुटि सिर्फ़ तब देता है जब नतीजा 0 से कम हो, या dimension के बराबर या उससे ज़्यादा हो। इसलिए दो बार खिसकाने पर विफलता सिर्फ़ वोकैबुलरी के आख़िरी सिरे पर दिखती है, खिसकाना भूल जाने पर सिर्फ़ टोकन id 0 पर, और बाक़ी हर वज़न चुपचाप पड़ोसी टोकन पर जा बैठता है।

फ़ारसी (Farsi) के लिए एम्बेडिंग मॉडल कैसे चुनें

MIRACL फ़ारसी और अरबी के बारे में क्या कहता है

MIRACL 18 भाषाओं में फैला एक रिट्रीवल बेंचमार्क है, जिनमें फ़ारसी और अरबी भी हैं। BGE-M3 वाला पेपर इसके development सेट पर nDCG@10 की रिपोर्ट देता है, जो शीर्ष दस नतीजों में रैंकिंग की गुणवत्ता का स्कोर है (ACL संस्करण; लेखकों ने ये नतीजे जुलाई 2024 में अपडेट किए थे):

रिट्रीवर

फ़ारसी (fa)

अरबी (ar)

XLM-RoBERTa टोकन पर BM25

28.7

39.5

mE5-large (multilingual E5)

59.0

76.0

BGE-M3 स्पार्स

45.1

67.1

BGE-M3 डेंस

60.9

78.4

BGE-M3 डेंस + स्पार्स + मल्टी-वेक्टर

63.4

80.2

फ़ारसी पर डेंस रिट्रीवल का स्कोर इस BM25 से कहीं ऊपर है, सीखा हुआ स्पार्स इन दोनों के बीच है, और तीनों मोड मिलाने से डेंस के ऊपर थोड़ा-सा और जुड़ता है। इन संख्याओं के साथ चार सावधानियाँ जुड़ी हैं:

  • यह BM25 subword टोकन इस्तेमाल करता है। मॉडल से मेल बिठाने के लिए लेखकों ने इसे XLM-RoBERTa के टोकनाइज़र पर चलाया; उनका परिशिष्ट दिखाता है कि Lucene के analyser से सभी भाषाओं पर MIRACL का औसत 31.9 से बढ़कर 38.5 हो जाता है। MIRACL की अपनी BM25 baseline, जो जहाँ उपलब्ध हो वहाँ भाषा-विशिष्ट analyser इस्तेमाल करती है, फ़ारसी पर 0.333 nDCG@10 और अरबी पर 0.481 तक पहुँचती है, 0 से 1 के पैमाने पर।

  • MIRACL एकभाषी है। यह “एकभाषी रिट्रीवल कार्यों को सपोर्ट करने के लिए बनाया गया है, जहाँ क्वेरी और कॉर्पस एक ही भाषा में होते हैं”, इसलिए यह अंग्रेज़ी दस्तावेज़ों पर फ़ारसी प्रश्नों के बारे में कुछ नहीं बताता।

  • फ़ारसी और अरबी की तुलना नियंत्रित तुलना नहीं है। दोनों में अलग-अलग प्रश्न और अलग Wikipedia कॉर्पस हैं; प्रोजेक्ट पेज फ़ारसी के लिए 632 और अरबी के लिए 2,896 development क्वेरी गिनाता है, इसलिए यह अंतर एक संकेत है, कठिनाई का माप नहीं।

  • BGE-M3 को MIRACL के ट्रेनिंग डेटा पर fine-tune किया गया था, इसलिए इसकी पंक्तियाँ in-domain स्कोर हैं।

FaMTEB और Hakim

Findings of EMNLP 2025 में प्रकाशित FaMTEB MTEB पर बना एक फ़ारसी बेंचमार्क है, जिसमें “सात अलग-अलग टास्क में फैले 63 डेटासेट” हैं, और रिट्रीवल उनमें से एक है।

Hakim एक फ़ारसी एम्बेडिंग मॉडल है, जिसका वर्णन arXiv के एक preprint में है, जो पहली बार मई 2025 में पोस्ट हुआ और अक्टूबर 2025 में संशोधित हुआ। इसके लेखक FaMTEB पर मौजूदा तरीक़ों की तुलना में 8.5% सुधार की रिपोर्ट देते हैं। इस नतीजे के साथ दो तथ्य रखे जाने चाहिए: Hakim के लेखक ही FaMTEB के भी लेखक हैं, और Hakim के दूसरे चरण के ट्रेनिंग डेटा में MIRACL_fa शामिल है। इनमें से कोई भी तथ्य नतीजे को ग़लत नहीं बनाता; दोनों अपने ख़ुद के डेटा पर टेस्ट करने के कारण हैं।

चुनने से पहले क्या टेस्ट करें

  • फ़ारसी और अंग्रेज़ी में आपके अपने प्रश्न, बोलचाल वाले रूपों सहित।

  • Cross-lingual जोड़े: ऐसे फ़ारसी प्रश्न जिनका उत्तर अंग्रेज़ी टेक्स्ट में है, और इसका उलटा।

  • एक ही प्रश्न के वर्तनी-रूप: Yeh और Kaf के दोनों रूप, ZWNJ या स्पेस, और अंकों का हर सेट।

  • लाइसेंस। BGE-M3 का मॉडल कार्ड MIT बताता है।

  • आपके चंक के आकार की तुलना में अधिकतम इनपुट लंबाई, टोकन में गिनी हुई। BGE-M3 के लेखक 8,192 टोकन तक के लंबे दस्तावेज़ों का वर्णन करते हैं, और एक ही सामग्री के लिए फ़ारसी टेक्स्ट को अंग्रेज़ी से ज़्यादा टोकन लग सकते हैं; फ़ारसी टोकन टैक्स ने यह अतिरिक्त ख़र्च ख़ुद BGE-M3 के टोकनाइज़र पर छोटा पाया, और जिन जनरेशन मॉडलों के टोकनाइज़र उसने मापे, उन पर कहीं बड़ा।

  • वेक्टर का dimension। इस साइट पर यह schema में तय है, इसलिए उसे बदलने का मतलब है एक migration और पूरी री-इंडेक्सिंग।

साइट का एम्बेडिंग एक्सप्लोरर जान-बूझकर neural नहीं है: उसके character n-gram वेक्टर शब्दों को अर्थ के बजाय वर्तनी के आधार पर समूहित करते हैं, और यही वह अंतर है जिसे बोलचाल की फ़ारसी के लिए डेंस मॉडल को पाटना होता है।

फ़ारसी RAG का मूल्यांकन: द्विभाषी गोल्डन सेट पर

फ़ारसी RAG के गोल्डन सेट में ये होने चाहिए: फ़ारसी और अंग्रेज़ी प्रश्न; उत्तर-योग्य प्रश्न, उन पैसेज के साथ जिन्हें रिट्रीव होना चाहिए; ऐसे प्रश्न जिन्हें ठुकराना ज़रूरी है, क्योंकि कॉर्पस में उनका कोई साक्ष्य नहीं; और ऐसे प्रश्न जिनका सबसे अच्छा साक्ष्य दूसरी भाषा में है। उत्तर-योग्य प्रश्नों पर recall@k मापें, जाँचें कि बिना साक्ष्य वाला हर प्रश्न समानता की न्यूनतम सीमा से नीचे रहे, और एक ऐसा टेस्ट जोड़ें जो तब विफल हो जब कोई भी उत्तर दूसरी भाषा के पैसेज पर आधारित न हो। जिस दिन कोई भाषा का फ़िल्टर वापस जोड़ेगा, यह टेस्ट उसी दिन उसे पकड़ लेगा।

इस साइट पर गोल्डन सेट, जो साइट की सभी दस भाषाओं में है और जिसके ज़्यादातर प्रश्न अंग्रेज़ी और फ़ारसी में हैं, CI में असली फ़्यूज़न, समूहन, क्रम तय करने और top-k वाले कोड से होकर चलता है, पर एक in-memory कॉर्पस और bag-of-tokens वाले सरोगेट embedder के साथ, प्रोडक्शन वाले के साथ नहीं। यही सरोगेट एक स्पार्स सूची भी देता है, जो प्रोडक्शन में चलती ही नहीं, और बिना साक्ष्य वाले प्रश्नों का सेट छोटा है। CI रैंकिंग की मशीनरी में regressions पकड़ता है, लाइव सहायक का recall नहीं। सिर्फ़ रिट्रीवल चलाने वाला एक कमांड, जो भाषा मॉडल को कभी कॉल नहीं करता, deploy किए गए embedder पर इनकार वाली न्यूनतम सीमा जाँचता है; उस embedder पर recall का कोई रन अब तक नहीं हुआ है, इसलिए मैं इस रिट्रीवर के लिए recall का कोई आँकड़ा प्रकाशित नहीं करता।

अरबी पर क्या लागू होता है

मैंने अरबी टेक्स्ट के लिए रिट्रीवल न डिज़ाइन किया है, न उसका मूल्यांकन किया है। इस साइट का रिट्रीवर साइट की अरबी सामग्री को इंडेक्स करता है, पर उसका कोई भी हिस्सा अरबी के लिए न बनाया गया, न टेस्ट किया गया। यह सेक्शन बस वह है जो फ़ारसी डिज़ाइन काग़ज़ पर सुझाता है।

इसका ज़्यादातर हिस्सा अरबी पर भी लागू होता है: रैंक फ़्यूज़न, भाषा का कोई फ़िल्टर नहीं, संरचना के हिसाब से चंकिंग, तुलना के भीतर डायक्रिटिक्स हटाना (W3C का कथन अरबी लिपि पर आम तौर पर लागू होता है), पहले सटीक स्कैन, और द्विभाषी गोल्डन सेट। तीन बातें अलग हैं:

  • PostgreSQL अरबी के साथ आता है। Snowball में अरबी स्टेमर है और PostgreSQL में arabic_stem डिक्शनरी; स्टेमिंग से फ़ायदा होता है या नहीं, यह आपका गोल्डन सेट तय करेगा।

  • फ़ारसी अक्षरों की फ़ोल्डिंग नहीं चलनी चाहिए। अरबी में Yeh (U+064A) और Kaf (U+0643) ही सही अक्षर हैं, इसलिए मिली-जुली भाषाओं वाले कॉर्पस को भाषा के हिसाब से नॉर्मलाइज़ करना होगा।

  • अंक अलग हैं। Arabic-Indic अंक U+0660 से U+0669 हैं, फ़ारसी में इस्तेमाल होने वाली U+06F0 रेंज नहीं।

अरबी की बेंचमार्क कवरेज भी ज़्यादा है, MIRACL और MKQA दोनों में। BGE-M3 पेपर की MKQA टेबल में, अंग्रेज़ी Wikipedia पर अरबी प्रश्न BGE-M3 डेंस के साथ 71.1 के Recall@100 तक पहुँचते हैं, जबकि पेपर के subword BM25 के साथ 18.9 तक; Recall@100 किसी प्रश्न को तब मिला हुआ गिनता है जब उसका उत्तर रखने वाला कोई पैसेज शीर्ष 100 में हो। फ़ारसी के पास ऐसी कोई cross-lingual संख्या नहीं है।

चेकलिस्ट: फ़ारसी RAG के लिए हाइब्रिड सर्च

  • स्टोर करते समय फ़ारसी अक्षर-रूपों को फ़ोल्ड करें, सिर्फ़ फ़ारसी सामग्री में; रिट्रीवल वाली कॉपी से bidi चिह्न हटाएँ।

  • हर प्रश्न पर वही normaliser चलाएँ, और स्टोर किए गए टेक्स्ट में ZWNJ बनाए रखें।

  • लेक्सिकल हिस्से को स्टेमिंग से मुक्त रखें, और टाइपो के लिए ट्राइग्राम समानता जोड़ें।

  • डेंस और लेक्सिकल रैंक को RRF से मिलाएँ, k = 60, लेक्सिकल सूची आधे वज़न पर — यह जानते हुए कि आधा वज़न उस पैसेज को नहीं बचा सकता जिसे डेंस सूची चूक गई हो।

  • इनकार की न्यूनतम सीमा किसी समानता पर रखें, RRF स्कोर पर कभी नहीं।

  • भाषा के आधार पर कभी फ़िल्टर न करें; भाषा को सिर्फ़ सटीक बराबरी का फ़ैसला करने दें, और यह चुनने दें कि किसी पैसेज का कौन-सा अनुवाद दिखे।

  • दस्तावेज़ की संरचना के हिसाब से चंक करें, और हर चंक में शीर्षक रखें।

  • सटीक स्कैन से शुरू करें, और हर अनुमानित इंडेक्स को उसी से जाँचें।

  • आप जिन भी भाषाओं में सेवा देते हैं, उन सबके गोल्डन सेट पर मूल्यांकन करें, जिसमें cross-language और बिना साक्ष्य वाले प्रश्न हों।

  • एम्बेडिंग मॉडल को अपने फ़ारसी प्रश्नों पर टेस्ट करें, बोलचाल वाले प्रश्नों सहित।

अगर आप फ़ारसी या मिली-जुली भाषाओं वाली सामग्री पर रिट्रीवल की योजना बना रहे हैं, तो परियोजना निदान पाँच सवालों को एक शुरुआती आर्किटेक्चर में बदल देता है।

अक्सर पूछे जाने वाले सवाल

क्या फ़ारसी सर्च के लिए BM25 काफ़ी है?

अकेले नहीं। BM25 सटीक नाम, संख्याएँ और कोड पकड़ लेता है, पर दूसरे शब्दों में कही गई बात (paraphrase) चूक जाता है, और बोलचाल वाले फ़ारसी प्रश्न और औपचारिक दस्तावेज़ में साझा शब्द बहुत कम हो सकते हैं। MIRACL के फ़ारसी सेट पर प्रकाशित डेंस नतीजे BM25 से काफ़ी ऊपर हैं, चाहे BM25 subword टोकन इस्तेमाल करे या Lucene के भाषा analysers। BM25, या पूरे शब्दों की कवरेज, को हाइब्रिड के लेक्सिकल आधे हिस्से के रूप में इस्तेमाल करें।

क्या रिट्रीवल से पहले फ़ारसी क्वेरी का अंग्रेज़ी में अनुवाद करना चाहिए?

डिफ़ॉल्ट रूप से नहीं। Multilingual एम्बेडिंग मॉडल दोनों भाषाओं को पहले से ही एक vector space में रखता है, और अनुवाद एक और मॉडल कॉल जोड़ता है जो नामों और सटीक शब्दों को बिगाड़ सकता है। BGE-M3 के लेखकों ने जो cross-lingual बेंचमार्क इस्तेमाल किया, उसमें अरबी है पर फ़ारसी नहीं, इसलिए अपने फ़ारसी–अंग्रेज़ी जोड़ों पर दोनों तरीक़े टेस्ट करें।

फ़ारसी RAG के लिए सबसे अच्छा एम्बेडिंग मॉडल कौन-सा है?

बेंचमार्क एक shortlist देते हैं, उत्तर नहीं: प्रकाशित MIRACL नतीजे फ़ारसी पर BGE-M3 और multilingual E5 को कवर करते हैं, FaMTEB में 63 डेटासेट हैं, और Hakim के लेखक FaMTEB पर सुधार की रिपोर्ट देते हैं। Shortlist को अपने बोलचाल वाले और cross-lingual प्रश्नों पर टेस्ट करें, और उससे पहले लाइसेंस और वेक्टर का dimension जाँच लें।

क्या फ़ारसी RAG के लिए अलग vector database चाहिए?

शुरुआत में नहीं। आपके मौजूदा PostgreSQL में pgvector डिफ़ॉल्ट रूप से exact nearest-neighbour search करता है, perfect recall के साथ, और प्रकाशन की स्थिति जैसे फ़िल्टर SQL क्वेरी के भीतर ही रखता है। अनुमानित इंडेक्स तभी जोड़ें जब latency के लिए ज़रूरी हो।

मेरा फ़ारसी सर्च वह शब्द क्यों नहीं ढूँढता जो टेक्स्ट में साफ़ मौजूद है?

आम तौर पर इसलिए कि स्ट्रिंग में ऐसा अंतर है जो आँख से नहीं दिखता: फ़ारसी ی या ک की जगह अरबी Yeh या Kaf (U+064A, U+0643), स्पेस के रूप में लिखा गया या छूट गया ZWNJ, अंकों का दूसरा सेट, या कोई भटका हुआ डायक्रिटिक। स्टोर किए गए पैसेज और प्रश्नों को एक ही फ़ंक्शन से नॉर्मलाइज़ करें, और तुलना के दोनों तरफ़ अंकों और डायक्रिटिक्स को फ़ोल्ड करें।

स्रोत