Гибридный поиск для персидского RAG: практическое руководство
Гибридный поиск для RAG на персидском (фарси) в PostgreSQL и pgvector: безопасная нормализация, RRF поверх плотных и лексических рангов, без фильтра по языку.
Гибридный поиск для RAG на персидском языке (фарси) — это два ретривера, работающих бок о бок: плотный (dense) поиск по эмбеддингам и лексический поиск, чьи ранжирования объединяются взаимным слиянием рангов (reciprocal rank fusion, RRF). Нужны оба, потому что персидский ломает негласные допущения английских рецептов: одно и то же слово может храниться с арабскими или персидскими буквами, внутри слов прячется невидимый знак нулевой ширины, а стеммера для персидского нет ни в Snowball, ни в PostgreSQL. Коротко: нормализуйте текст при записи, не используйте стемминг на лексической стороне, объединяйте ранги, а не оценки, никогда не фильтруйте по языку, нарезайте документ на чанки по его собственной структуре и, прежде чем доверять чему-либо из этого, проверьте всё на двуязычном наборе вопросов.
Это руководство выросло из двух систем. Первая — ассистент этого сайта, который я создал и поддерживаю. Вторая — Kartez Contract, где я работал инженером по ИИ: его копилот должен был находить нужные пункты в персидских договорах с нумерацией по статьям, пунктам и примечаниям. Описанная здесь архитектура поиска (плотный плюс лексический, слияние рангов, pgvector) — архитектура этого сайта; от Kartez я беру только то, что сказано в его кейсе. Бенчмарков ни для одной из этих систем здесь нет.
Почему английские рецепты RAG не работают с персидским текстом
Типичный сбой — не неверный ответ, а отсутствие ответа: поиск не находит слово, которое пользователь видит в документе, потому что две строки, выглядящие одинаково, внутри различаются.
Одно слово, две разные строки: арабские и персидские йе и каф
Персидский и арабский пользуются одним письмом, но не всеми кодовыми точками. Персидская ی (U+06CC ARABIC LETTER FARSI YEH) и арабская йе (U+064A ARABIC LETTER YEH) — два разных символа, и то же верно для персидской ک (U+06A9 ARABIC LETTER KEHEH) и арабской каф (U+0643 ARABIC LETTER KAF). В документе W3C Arabic and Persian Layout Requirements пара йе названа прямо: буква «представлена двумя разными символами». Фрагмент, сохранённый в одной форме, не совпадёт с вопросом, набранным в другой; шаг нормализации на схеме выше показывает обе пары.
Этот сайт приводит арабские йе, алиф максуру и каф — среди прочих символов — к персидским формам до того, как фрагмент попадёт в поисковый индекс, удаляет из этой копии невидимые знаки направления текста (bidi) и применяет то же приведение к каждому вопросу.
Знак нулевой ширины ZWNJ — часть слова
میشود, میشود и می شود — три разные строки. В первой приставка отделена от глагола знаком нулевой ширины (zero-width non-joiner, ZWNJ, U+200C): он разрывает соединение букв, не добавляя пробела. Стандарт Unicode описывает такое нестандартное соединение, «как это иногда требуется при записи персидского языка арабским письмом», а требования W3C гласят, что управляющие символы «следует сохранять при хранении и отображении текстов».
Удалите ZWNJ, как может сделать пайплайн, написанный под английский, — и میشود превратится в میشود; замените его пробелом — и одно слово станет двумя токенами. Нормализатор в hazm, наборе инструментов для обработки персидского языка, включает «исправление ZWNJ» и превращает کتاب های в کتابهای. На этом сайте хранимый текст сохраняет ZWNJ, и только приведение, используемое внутри сравнений, считает его пробелом.
Три набора цифр
Персидский записывает ۱۲۳ восточными арабско-индийскими цифрами (от U+06F0 до U+06F9), арабский записывает то же число арабско-индийскими цифрами (от U+0660 до U+0669), а в техническом тексте часто пишут 123.
Приводите цифры к одному виду только для сравнения и никогда — в хранимом тексте: на этом сайте переписывание хранимых персидских цифр изменило бы то, как отображается персидское резюме. Но приводить их нужно с обеих сторон: если привести только цифры вопроса, вопрос с персидскими цифрами перестанет совпадать с фрагментом, из которого он скопирован. В статье о защитных механизмах для потоковых ответов LLM то же приведение применяется к числам, которые выдаёт модель.
Диакритика встречается редко, поэтому один случайный знак ломает точное совпадение
Требования W3C говорят об этом прямо: «В тексте арабским письмом использовать диакритические знаки для обозначения гласных и удлинения согласных необычно». Поэтому единственный фрагмент с касрой (знаком краткого гласного) или единственный вопрос, вставленный с ташдидом (знаком удвоения), не проходит ни одного точного сравнения с написанием без знаков. В исследовании по синтезу персидской речи, над которым я работал для MCI, то же свойство было трудной частью задачи, только со стороны речи: краткие гласные и изафет, связывающий существительное с его определением, на письме не обозначаются. Для поиска удаляйте диакритику внутри сравнений и сохраняйте её в хранимом тексте.
Пользователи спрашивают разговорным языком, а документы написаны формальным
«Персидский — один из языков с множеством различий между формальным и неформальным стилями письма», — пишут Tajalli, Shamsfard и Kalantari, представляя параллельный корпус из 50 000 пар предложений. Пользователь набирает میتونم там, где формальный текст пишет میتوانم. Следствие, как я его понимаю: лексическое сопоставление видит мало пересечений между разговорным вопросом и формальным пунктом, поэтому шанс остаётся у плотного ретривера, а тестовый набор из грамотно сформулированных вопросов этот разрыв скроет.
Безопасная нормализация персидского текста
Приводите арабские йе и каф к персидским ی и ک при записи — только в персидском контенте.
Сохраняйте ZWNJ в хранимом тексте.
Удаляйте знаки bidi из копии, которую индексируете и сравниваете, а не из отображаемого текста.
Приводите цифры к одному виду и удаляйте диакритику только внутри сравнений, с обеих сторон.
Применяйте к вопросам ровно ту же функцию, что и к фрагментам.
Никогда не применяйте персидское приведение к арабскому тексту, где арабские йе (U+064A) и каф (U+0643) — правильные буквы.
Собственный ретривер сайта этому списку пока не соответствует: персидское приведение букв он применяет ко всем языкам, включая арабский; его лексический список может не найти фрагмент, записанный персидскими цифрами; а диакритика арабского письма удаляется из терминов вопроса и из всех сравнений, которые делает шлюз по предложениям, но не из хранимого текста, который просматривает поиск по ключевым словам. Читайте список как цель.
Полнотекстовый поиск на персидском в PostgreSQL: встроенной конфигурации нет
Полнотекстовый поиск — очевидная лексическая половина гибрида, и документация PostgreSQL сообщает, что он «поставляется с предопределёнными конфигурациями для многих языков». Персидского среди них нет.
В Snowball есть стеммер для арабского, но не для персидского
Список алгоритмов стемминга Snowball включает арабский, и в нём нет записи ни для Persian, ни для Farsi. PostgreSQL строит свои словари стемминга на основе Snowball: список языков в исходном коде PostgreSQL 18 включает arabic и не включает persian, а перечень словарей в документации показывает arabic_stem и simple без персидского словаря. Арабский стеммер разделяет с персидским письменность, но не грамматику. За пределами базы данных стеммер и лемматизатор для персидского есть — их поставляет hazm; внутри базы данных нет ни того ни другого.
Что даёт конфигурация simple, а чего не даёт
Шаблон словаря simple, по словам документации PostgreSQL о словарях, работает так: «приводит входной токен к нижнему регистру и проверяет его по файлу стоп-слов». У встроенного словаря simple файла стоп-слов нет, поэтому сохраняется каждый токен, и стемминга тоже нет: قرارداد и قراردادها остаются двумя лексемами — потеря для полноты, выигрыш в предсказуемости.
Ещё до работы словарей парсер решает, что считать словом, а его представление о букве «определяется настройкой локали базы данных, а именно 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 сравнивает последовательности из трёх символов, поэтому слово с опечаткой всё равно разделяет триграммы с правильным. Оно «игнорирует символы, не входящие в слова (не буквенно-цифровые), при извлечении триграмм», так что проверьте, прежде чем на него полагаться: SELECT show_trgm('قرارداد'); должен вернуть триграммы из персидских букв. Поиск по ключевым словам на сайте добавляет ветку word_similarity, которая сопоставляет короткий запрос с наиболее подходящим непрерывным участком длинного фрагмента.
Покрытие целыми словами: лексический список в ретривере этого сайта
Ретривер ассистента не использует полнотекстовый поиск для своего лексического списка, потому что персидской конфигурации нет. Он считает, сколько содержательных слов вопроса встречается во фрагменте как целые слова, с границами слов в регулярных выражениях. Он убирает стоп-слова каждого из языков сайта, отбрасывает очень короткие термины, оставляет знаки гласных хинди внутри слова, к которому они относятся, и не применяет стемминг. Результат — доля покрытия от 0 до 1.
Границы слов важны: поиск подстроки «is» находит «his», «this» и «Isfahan», и любой фрагмент начинает выглядеть как подтверждение. Важна и ограниченность доли: с ней можно сравнивать порог сходства. В порог отказа эта доля идёт, только если совпали хотя бы два разных содержательных слова.
Обученные разрежённые векторы: задокументированный вариант
Обученный разрежённый поиск (learned sparse retrieval) сопоставляет термины, как лексический поиск, но с весами, которые выучивает модель. В BGE-M3 термины — это кусочки слов из токенизатора (в её карточке модели вес получают «De», «fin» и «ation»), а вектор имеет «размер, равный словарю», по словам статьи, которая её представила: 250 002 в конфигурации модели. Продакшен-ретривер этого сайта их не использует: он работает на связке плотного и лексического поиска, а API эмбеддингов, который он вызывает, разрежённых весов не возвращает. Если вы их добавляете, сначала прочитайте раздел об ошибке на единицу ниже.
Гибридный поиск и RRF: объединяйте ранги, а не оценки
Плотный и лексический ретриверы возвращают ранжированные списки, оценки в которых означают разные вещи. Взаимное слияние рангов не смотрит на оценки и объединяет позиции.
Формула и откуда взялось 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 зависят от прикладного сценария». Для связки плотного и разрежённого поиска авторы задают w1 = 1 и w2 = 0,3 на MIRACL и w1 = 0,2 и w2 = 0,8 на MLDR, бенчмарке поиска по длинным документам.
Косинусное сходство, скалярное произведение разрежённых векторов и доля покрытия живут в разных шкалах, а «курс обмена» между ними меняется от корпуса к корпусу. Слияние рангов обходит эту калибровку. Это компромисс, а не бесплатный выигрыш: статья сообщает о сильных результатах со взвешенными суммами, а RRF не отличает разгромную победу от победы с минимальным отрывом.
Почему лексический список получает половинный вес
На этом сайте лексический список нужен, чтобы поднимать фрагменты с точными строками, которые плотная модель может размыть: доменное имя вроде booqai.app, аббревиатуру, название программы вроде SAMPAD. Это обычная причина держать такой список. В заметке Anthropic о Contextual Retrieval модель эмбеддингов «может найти материалы о кодах ошибок в целом, но пропустить точное совпадение „TS-999“».
Лексический список входит с половинным весом: этого хватает, чтобы поднять фрагмент, который вернул и плотный список, но недостаточно, чтобы пересечение по ключевым словам перевесило плотное ранжирование. Цена у этого настоящая: при k = 60 и тех количествах кандидатов, которые берёт этот сайт, фрагмент, найденный только лексическим списком, оказывается ниже любого плотного кандидата, так что слияние не может вернуть фрагмент, который плотная модель пропустила совсем. Единственное исключение — фрагмент с наибольшим сходством: он возвращается всегда, как объясняет следующий раздел. Если вызов эмбеддингов завершится ошибкой, поиск откатится к одному лексическому списку, и порог сходства всё равно будет применён.
Оценка RRF — не мера уверенности
Оценка RRF строится только из позиций, поэтому даже корпус из чистого шума даст фрагмент на первом месте с лучшей из имеющихся итоговых оценок. Поэтому порог отказа на этом сайте сравнивается не с итоговой оценкой слияния, а с настоящим сходством каждого фрагмента — большим из его косинусного сходства с вопросом в том виде, в каком его задали (а для уточняющего вопроса — ещё и с вопросом, соединённым с предыдущим, а если он сформулирован как позиция в списке, то соединённым и с тем пунктом, на который он указывает, или с началом предыдущего ответа, если этот пункт прочитать не удалось), и покрытия целыми словами, которое учитывается, только если совпали хотя бы два содержательных слова. Порог проверяется до того, как будет вызвана языковая модель.
Фрагмент с наибольшим сходством среди всех кандидатов всегда оказывается среди возвращённых, даже если слияние поставило его слишком низко, чтобы он прошёл отбор. Ниже порога вопрос, который не касается меня, отклоняется без вызова модели, а вопрос обо мне вместо этого получает записи сайта; и то и другое описано в статье о защитных механизмах против галлюцинаций в RAG.
Никогда не фильтруйте поиск по языку
Фильтр по языку выглядит как порядок: персидские вопросы ищут по персидским фрагментам. На двуязычном корпусе он удаляет подтверждения.
Лучшее подтверждение может быть на другом языке
На этом сайте фрагменты на всех языках лежат рядом в одной таблице, и вопрос ищет по всем. Персидское резюме — исходный документ, а английское — адаптация, поэтому у английского вопроса о какой-либо должности лучшее подтверждение может оказаться на персидском. У Kartez Contract была та же картина в обратном направлении: его мастер составления договоров должен был переносить требования на персидском в библиотеку англоязычных текстов по составлению договоров.
Язык решает только при точном равенстве оценок
У языка всё же остаётся одно применение в ранжировании: из двух фрагментов с абсолютно одинаковой итоговой оценкой первым идёт тот, что есть на языке вопроса, а дальше решает стабильный id — как в ключе сортировки выше. При одном плотном списке и лексическом списке с половинным весом точные равенства практически не случаются. Они возможны, когда вопрос ищется в нескольких вариантах, о которых речь ниже, и два фрагмента получают одинаковые места в плотных списках этих вариантов, у которых равный вес; но и тогда язык решает, только если на языке вопроса есть лишь один из двух. Так и задумано: язык никогда не должен перевешивать оценку. Есть у языка и ещё одна задача: выбрать, какой перевод фрагмента увидит читатель.
Небольшая надбавка фрагментам на языке вопроса — не способ разрешать точные равенства. Соседние вклады одного списка с полным весом отличаются на 1/((k + r)(k + r + 1)), и этот разрыв сокращается к концу списка, а суммы из двух списков могут лежать ещё ближе. Фиксированная надбавка безопасна, только пока она остаётся меньше любого разрыва, который способны дать ваши глубины списков и веса, — а этот предел сдвигается, как только меняется хоть одно из них. Стоит надбавке превысить предел, и в середине списка местами меняются результаты, между которыми равенства не было, — среди тех самых кандидатов, ради подъёма которых слияние и существует.
Один фрагмент, десять переводов
Сайт публикует страницы на десяти языках, и копии одного фрагмента на разных языках могли занимать несколько из восьми мест, которые передаются модели. Теперь поиск считает переводы одного фрагмента одним кандидатом: он оценивается по лучшему из своих переводов, ранжируется один раз и показывается на языке читателя, если такой перевод есть. Статьи группируются по разделам, а не по чанкам, потому что длинный раздел на каждом языке делится на разное число чанков; перевод, разделы которого не совпадают с разделами английской версии, остаётся отдельно, чтобы не попасть не в ту группу. Язык читателя решает, какой перевод показать; кроме случая точного равенства, он никогда не решает, на каком месте окажется фрагмент.
Персидские вопросы по английским документам
Пересекать границу языков может только плотная сторона: у персидского вопроса и английского фрагмента почти нет общих слов, если не считать латинских терминов вроде RAG. Собственная оценка BGE-M3 не закрывает этот вопрос для персидского: её кросс-языковой бенчмарк MKQA ищет фрагменты английской Википедии по запросам на 25 языках, кроме английского, и арабский среди них есть, а персидского нет. Проверяйте собственные пары «персидский — английский».
Вопросы без ключевых слов
Вопрос посетителя «Чем он занимался в последнее время?» не содержит ни одного слова, которое корпус мог бы найти, а «А до этого?» не содержит вообще ничего. Если вопрос касается меня, его ищут ещё и с моим именем в начале, а уточняющий вопрос — ещё и вместе с предыдущим вопросом; если он сформулирован как позиция в списке, например «второй», между ними ставится и тот пункт, на который он указывает, или начало предыдущего ответа, если этот пункт прочитать не удалось. Все варианты превращаются в эмбеддинги за один вызов и объединяются по рангам. Вариант с моим именем упорядочивает фрагменты, но никогда не идёт в порог отказа: одно лишь имя может сделать вопрос обо мне, не связанный с моей работой, близким к моему резюме. Вопросы о моей работе в целом не зависят от поиска: ответ на них строится по датированным записям, которые сайт собирает из моего резюме, кейсов, статей и услуг, как описано в статье о защитных механизмах против галлюцинаций в RAG.
Разбиение персидских договоров на чанки по статьям, пунктам и примечаниям
У персидских договоров своя структура: статьи (ماده), пункты (بند) и примечания (تبصره). В копилоте Kartez Contract договор самого пользователя разбивался по этой структуре, и для ответа на вопрос использовались только относящиеся к нему разделы; ответы «опираются только на отобранные пункты». Остальную часть проекта описывает кейс Kartez Contract, а саму предметную область — статья о том, что строить против галлюцинаций юридического ИИ.
Тот же принцип определяет работу чанкера этого сайта: один чанк — одна смысловая запись. Должность в резюме никогда не отделяется от своих дат, кейс делится по разделам, которые написал автор, пост — по заголовкам, а длинный раздел разрывается только на границе абзаца. Каждый чанк раздела несёт название документа, поэтому даже извлечённый в одиночку чанк сообщает, откуда он взят.
Для персидских договоров отсюда следует несколько правил — это общие рекомендации, а не описание того, как был построен Kartez:
Держите номер вместе с текстом. «ماده ۵» и её текст должны быть в одном чанке.
Переносите заголовок родительской статьи в каждый чанк пункта и примечания.
Никогда не режьте пункт по числу токенов. Примечание, отделённое от своего пункта, может изменить то, что этот пункт, как кажется, говорит.
Храните структурный путь (статья › пункт › примечание), чтобы ответ мог указать специалисту точное место.
pgvector: точный поиск или HNSW?
pgvector сам отвечает на половину этого вопроса: «По умолчанию pgvector выполняет точный поиск ближайших соседей, что обеспечивает идеальную полноту». Приближённый индекс «жертвует частью полноты ради скорости».
Менее очевидная цена — фильтры. «С приближёнными индексами фильтрация применяется после сканирования индекса. Если условию соответствуют 10% строк, то с HNSW и значением hnsw.ef_search по умолчанию, равным 40, в среднем совпадут только 4 строки». pgvector 0.8.0 и новее умеет сканировать индекс итеративно, пока не наберётся достаточно строк, — если включить эту возможность.
У таблицы поиска этого сайта нет приближённого индекса: векторный поиск — это точное сканирование. Статус публикации входит в условие SQL WHERE, а не применяется как фильтр после LIMIT, где он незаметно урезал бы набор результатов. Начинайте с точного сканирования, добавляйте индекс, когда этого потребует задержка, и, прежде чем ему доверять, сравните его top-k с точным сканированием на собственных вопросах.
Ошибка на единицу в sparsevec у pgvector
Это важно, только если вы добавляете обученные разрежённые векторы в PostgreSQL, — и ошибка проявляется молча.
Текстовый формат разрежённых векторов в pgvector — {index1:value1,index2:value2}/dimensions, «причём индексы начинаются с 1, как в массивах SQL». Идентификаторы токенизатора начинаются с 0. Python-клиент принимает индексы с нуля («индексы начинаются с 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"Сдвигайте ровно один раз — там, где SQL-литерал собирается вручную. Текстовый ввод pgvector переводит каждый индекс в нумерацию с нуля и выдаёт «sparsevec index out of bounds», только если результат меньше 0 либо больше размерности или равен ей. Поэтому двойной сдвиг ломается только на верхнем конце словаря, пропущенный сдвиг — только на токене с id 0, а все остальные веса тихо попадают на соседний токен.
Как выбрать модель эмбеддингов для персидского (фарси)
Что MIRACL говорит о персидском и арабском
MIRACL — бенчмарк поиска на 18 языках, среди которых персидский и арабский. Статья, стоящая за BGE-M3, приводит на его dev-наборе значения nDCG@10 — метрики качества ранжирования в первой десятке результатов (версия ACL; в июле 2024 года авторы обновили эти результаты):
Ретривер | Персидский (fa) | Арабский (ar) |
|---|---|---|
BM25 на токенах XLM-RoBERTa | 28,7 | 39,5 |
mE5-large (многоязычный 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 работает на подсловных токенах. Чтобы токены совпадали с моделью, авторы запускали его на токенизаторе XLM-RoBERTa; в приложении они показывают, что анализатор Lucene поднимает среднее по языкам MIRACL с 31,9 до 38,5. Собственный базовый BM25 из MIRACL, с языковыми анализаторами там, где они есть, достигает 0,333 nDCG@10 на персидском и 0,481 на арабском — по шкале от 0 до 1.
MIRACL — одноязычный бенчмарк. Он «предназначен для задач одноязычного поиска, в которых запросы и корпуса написаны на одном языке», поэтому ничего не говорит о персидских вопросах по английским документам.
Персидский и арабский — не контролируемое сравнение. У них разные вопросы и разные корпуса Википедии; страница проекта указывает в dev-наборе 632 запроса для персидского и 2 896 — для арабского, так что разрыв между ними — сигнал, а не мера сложности.
BGE-M3 дообучали на обучающих данных MIRACL, поэтому её строки — оценки внутри домена.
FaMTEB и Hakim
FaMTEB, опубликованный в Findings of EMNLP 2025, — персидский бенчмарк на основе MTEB: «63 набора данных, охватывающих семь различных задач», среди которых и поиск.
Hakim — персидская модель эмбеддингов, описанная в препринте на arXiv: первая версия появилась в мае 2025 года, переработанная — в октябре 2025 года. Её авторы сообщают об улучшении на 8,5% по сравнению с существующими подходами на FaMTEB. Рядом с этим результатом стоят два факта: авторы Hakim — те же, что и авторы FaMTEB, а в данных второго этапа обучения Hakim значится MIRACL_fa. Ни то ни другое не делает результат неверным; и то и другое — причины проверять на собственных данных.
Что проверить перед выбором
Собственные вопросы на персидском и английском, включая разговорные формулировки.
Кросс-языковые пары: персидские вопросы, ответ на которые содержится в английском тексте, и наоборот.
Варианты написания одного вопроса: формы йе и каф, ZWNJ или пробел, каждый набор цифр.
Лицензию. В карточке модели BGE-M3 указана MIT.
Максимальную длину входа в сравнении с размером чанка, в токенах. Авторы BGE-M3 описывают длинные документы до 8 192 токенов, а персидскому тексту для того же содержания может понадобиться больше токенов, чем английскому; персидский налог на токены нашёл эту надбавку небольшой на собственном токенизаторе BGE-M3 и куда большей на токенизаторах генеративных моделей, которые там измерялись.
Размерность вектора. На этом сайте она зафиксирована в схеме, поэтому её изменение означает миграцию и полную переиндексацию.
Обозреватель эмбеддингов на этом сайте намеренно не нейросетевой: его векторы на символьных n-граммах группируют слова по написанию, а не по смыслу, — и именно этот разрыв плотной модели приходится закрывать для разговорного персидского.
Оценка персидского RAG на двуязычном эталонном наборе вопросов
Эталонному набору (golden set) для персидского RAG нужны вопросы на обоих языках: те, на которые есть ответ, — вместе с фрагментами, которые должны быть найдены; те, на которые система обязана ответить отказом, потому что подтверждений в корпусе нет; и те, лучшее подтверждение для которых находится на другом языке. Измеряйте recall@k на вопросах с ответом, проверяйте, что каждый вопрос без подтверждений оказывается ниже порога сходства, и добавьте тест, который падает, если ни один ответ не опирается на фрагмент на другом языке. Он поймает фильтр по языку в тот же день, когда его кто-нибудь вернёт.
На этом сайте эталонный набор — на всех десяти языках сайта, причём большинство вопросов на английском и персидском — прогоняется в CI через настоящий код слияния, группировки, упорядочивания и отбора top-k, но на корпусе в памяти и с суррогатным эмбеддером на основе мешка токенов (bag-of-tokens), а не с продакшен-моделью. Суррогат заодно питает разрежённый список, которого в продакшене нет, а набор вопросов без подтверждений мал. CI ловит регрессии в механике ранжирования, а не полноту живого ассистента. Команда, которая выполняет только поиск и никогда не вызывает языковую модель, проверяет порог отказа на развёрнутом эмбеддере; полнота на этом эмбеддере не измерялась, поэтому никаких цифр полноты для этого ретривера я не публикую.
Что переносится на арабский
Я не проектировал и не оценивал поиск по арабскому тексту. Ретривер этого сайта индексирует его арабскоязычный контент, но ничто в нём не создавалось и не тестировалось для арабского. Этот раздел — то, что персидская архитектура подразумевает на бумаге.
Большая часть переносится: слияние рангов, отсутствие фильтра по языку, разбиение на чанки с учётом структуры, удаление диакритики внутри сравнений (утверждение W3C касается арабского письма в целом), точное сканирование для начала и двуязычный эталонный набор. Отличаются три вещи:
Арабский в PostgreSQL есть. В Snowball есть арабский стеммер, а в PostgreSQL — словарь
arabic_stem; помогает ли стемминг, решит ваш эталонный набор.Персидское приведение букв запускать нельзя. В арабском йе (U+064A) и каф (U+0643) — правильные буквы, поэтому смешанный корпус нужно нормализовать с учётом языка.
Цифры другие. Арабско-индийские цифры — это диапазон от U+0660 до U+0669, а не диапазон U+06F0, который использует персидский.
У арабского и покрытие бенчмарками шире: его включают и MIRACL, и MKQA. В таблице MKQA из статьи о BGE-M3 арабские вопросы по английской Википедии достигают Recall@100, равного 71,1, с плотным BGE-M3 против 18,9 у подсловного BM25 из той же статьи; Recall@100 засчитывает вопрос как найденный, если фрагмент, содержащий ответ, входит в первую сотню. У персидского такого кросс-языкового числа нет.
Чек-лист: гибридный поиск для персидского RAG
Приводите формы персидских букв при записи — только в персидском контенте; удаляйте знаки bidi из поисковой копии.
Применяйте тот же нормализатор к каждому вопросу и сохраняйте ZWNJ в хранимом тексте.
Не используйте стемминг на лексической стороне и добавьте триграммное сходство для опечаток.
Объединяйте плотные и лексические ранги с помощью RRF, k = 60, лексический список с половинным весом — помня, что половинный вес не спасёт фрагмент, который плотный список пропустил.
Ставьте порог отказа на сходство, а не на оценку RRF.
Никогда не фильтруйте по языку; пусть язык решает только при точном равенстве оценок и при выборе того, какой перевод фрагмента показать.
Разбивайте на чанки по структуре документа, с названием документа в каждом чанке.
Начинайте с точного сканирования и проверяйте по нему любой приближённый индекс.
Оценивайте на эталонном наборе на каждом языке, который вы поддерживаете, с кросс-языковыми вопросами и вопросами без подтверждений.
Тестируйте модели эмбеддингов на собственных персидских вопросах, включая разговорные.
Если вы планируете поиск по персидскому или многоязычному контенту, «Диагностика проекта» превращает пять вопросов в первый набросок архитектуры.
Частые вопросы
Достаточно ли BM25 для поиска на персидском?
Сам по себе — нет. BM25 находит точные имена, числа и коды, но пропускает перефразирования, а разговорный вопрос на персидском может почти не совпадать по словам с формальным документом. На персидском наборе MIRACL опубликованные результаты плотного поиска намного выше BM25 — и когда BM25 работает на подсловных токенах, и когда использует языковые анализаторы Lucene. Используйте BM25 или покрытие целыми словами как лексическую половину гибрида.
Стоит ли переводить персидские запросы на английский перед поиском?
По умолчанию — нет. Многоязычная модель эмбеддингов уже отображает оба языка в одно векторное пространство, а перевод добавляет вызов модели, который может исказить имена и точные термины. Кросс-языковой бенчмарк, который использовали авторы BGE-M3, включает арабский, но не персидский, поэтому проверьте оба подхода на собственных парах «персидский — английский».
Какая модель эмбеддингов лучше всего подходит для персидского RAG?
Бенчмарки дают шорт-лист, а не ответ: результаты MIRACL охватывают BGE-M3 и многоязычный E5 на персидском, FaMTEB включает 63 набора данных, а авторы Hakim сообщают о выигрыше на FaMTEB. Проверьте шорт-лист на собственных разговорных и кросс-языковых вопросах — после того как посмотрите лицензию и размерность вектора.
Нужна ли для персидского RAG отдельная векторная база данных?
Для начала — нет. pgvector в вашем существующем PostgreSQL по умолчанию выполняет точный поиск ближайших соседей с идеальной полнотой и оставляет фильтры, например по статусу публикации, внутри SQL-запроса. Добавляйте приближённый индекс, только когда этого требует задержка.
Почему поиск на персидском не находит слово, которое явно есть в тексте?
Обычно потому, что строки различаются так, что глазом этого не увидеть: арабские йе или каф (U+064A, U+0643) вместо персидских ی или ک, ZWNJ, записанный пробелом или пропущенный, другой набор цифр или случайный диакритический знак. Нормализуйте хранимые фрагменты и вопросы одной и той же функцией, а цифры и диакритику приводите с обеих сторон сравнения.
Источники
G. V. Cormack, C. L. A. Clarke и S. Büttcher, «Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods», SIGIR 2009.
Chen, Xiao, Zhang, Luo, Lian и Liu, «M3-Embedding: Multi-Linguality, Multi-Functionality, Multi-Granularity Text Embeddings Through Self-Knowledge Distillation», Findings of ACL 2024.
BAAI, карточка модели BGE-M3.
Zhang и др., «MIRACL: A Multilingual Retrieval Dataset Covering 18 Diverse Languages», TACL 2023 (PDF с таблицей базовых результатов), и страница проекта MIRACL.
Zinvandi и др., «FaMTEB: Massive Text Embedding Benchmark in Persian Language», Findings of EMNLP 2025.
Sarmadi и др., «Hakim: Farsi Text Embedding Model», препринт arXiv, 2025.
README pgvector, исходный код разрежённых векторов pgvector и README pgvector-python.
Документация PostgreSQL: введение в текстовый поиск, словари, поддержка в psql, парсеры, тестирование и отладка текстового поиска и pg_trgm.
Исходный код PostgreSQL (REL_18_STABLE): snowball_create.pl и pg_ts_dict.dat.
W3C, Arabic & Persian Layout Requirements, Group Draft Note, 2 октября 2025 года.
The Unicode Standard 18.0, Chapter 23: Special Areas and Format Characters, и Unicode Character Database.
Tajalli, Shamsfard и Kalantari, «Developing an Informal-Formal Persian Corpus: Highlighting the Differences between Two Writing Styles», AbjadNLP 2025.
Anthropic, «Introducing Contextual Retrieval», 19 сентября 2024 года.