Busca híbrida para RAG em persa: um guia prático

Busca híbrida para RAG em persa no PostgreSQL e pgvector: normalização segura, RRF sobre rankings denso e lexical, sem filtro por idioma e testes bilíngues.

Busca híbrida para RAG em persa (farsi) significa rodar dois retrievers lado a lado, uma busca densa por embeddings e uma busca lexical, e fundir os rankings dos dois com reciprocal rank fusion (RRF). Você precisa dos dois porque o persa quebra os pressupostos tácitos das receitas feitas para o inglês: a mesma palavra pode estar armazenada com letras árabes ou persas, um zero-width non-joiner fica dentro das palavras, e nem o Snowball nem o PostgreSQL trazem um stemmer para persa. Em resumo: normalize na escrita, mantenha o lado lexical sem stemming, funda posições em vez de pontuações, nunca filtre por idioma, divida o texto em chunks seguindo a estrutura do próprio documento e teste com um conjunto de perguntas bilíngue antes de confiar em qualquer parte disso.

Este guia se apoia em dois sistemas. Um é o assistente deste site, que eu construí e mantenho. O outro é o Kartez Contract, onde trabalhei como engenheiro de IA; o copiloto dele precisava encontrar as cláusulas certas em contratos em persa, numerados por artigo, cláusula e nota. O design de recuperação descrito aqui (denso mais lexical, fusão de posições, pgvector) é o deste site; do Kartez, uso apenas o que o estudo de caso afirma. Nenhum dos dois sistemas é avaliado em benchmark aqui.

Recuperação híbrida para persa: chunks normalizados e convertidos em embeddings no momento da escrita; listas densa e lexical fundidas com RRF, com o idioma apenas desempatando pontuações exatamente iguais; um piso de similaridade verificado antes de o modelo de linguagem ser chamado: abaixo dele, uma pergunta sobre mim recebe os registros do site e qualquer outra pergunta é recusada.

Por que receitas de RAG feitas para o inglês falham com texto em persa

A falha típica não é uma resposta errada, e sim nenhuma resposta: a busca não encontra uma palavra que o usuário está vendo no documento, porque duas strings que parecem idênticas são diferentes por baixo.

Uma palavra, duas strings: o Yeh e o Kaf árabes e persas

Persa e árabe compartilham a escrita, não todos os code points. O ی persa (U+06CC ARABIC LETTER FARSI YEH) e o Yeh árabe (U+064A ARABIC LETTER YEH) são dois caracteres diferentes, assim como o ک persa (U+06A9 ARABIC LETTER KEHEH) e o Kaf árabe (U+0643 ARABIC LETTER KAF). Os Arabic and Persian Layout Requirements do W3C citam o par do Yeh diretamente: a letra é “representada por dois caracteres diferentes”. Um trecho armazenado com uma das formas não corresponde a uma pergunta digitada com a outra; a etapa de normalização no diagrama acima mostra os dois pares.

Este site converte o Yeh árabe, o Alef Maksura e o Kaf, entre outros caracteres, para as formas persas antes de um trecho entrar no índice de recuperação, remove dessa cópia as marcas bidi invisíveis e aplica a mesma conversão a toda pergunta.

O zero-width non-joiner faz parte da palavra

می‌شود, میشود e می شود são três strings diferentes. A primeira separa o prefixo do verbo com um zero-width non-joiner (ZWNJ, U+200C), que impede a ligação entre as letras sem acrescentar um espaço. O Unicode Standard descreve essa junção fora do padrão como algo que “às vezes é necessário ao escrever persa com a escrita árabe”, e os requisitos do W3C dizem que caracteres de controle “devem ser preservados ao armazenar e exibir textos”.

Remova o ZWNJ, como um pipeline feito para o inglês poderia fazer, e می‌شود vira میشود; troque-o por um espaço e uma palavra vira dois tokens. O normalizador do hazm, um toolkit de PLN para persa, inclui a “correção de ZWNJ”, que transforma کتاب های em کتاب‌های. Neste site, o texto armazenado mantém o ZWNJ, e só a conversão usada dentro das comparações o trata como espaço.

Três conjuntos de dígitos

O persa escreve ۱۲۳ com dígitos Eastern Arabic-Indic (U+06F0 a U+06F9), o árabe escreve o mesmo número com dígitos Arabic-Indic (U+0660 a U+0669), e textos técnicos costumam escrever 123.

Converta os dígitos só para comparação, nunca no texto armazenado; neste site, reescrever os dígitos persas armazenados mudaria a forma como o currículo em persa é exibido. A conversão precisa rodar dos dois lados, porém: converta só os dígitos da pergunta e uma pergunta com dígitos persas deixa de corresponder ao trecho de onde foi copiada. O artigo sobre guardrails para LLM em streaming aplica a mesma conversão aos números que um modelo gera.

Diacríticos são raros, então uma marca solta quebra a correspondência exata

Os requisitos do W3C dizem sem rodeios: “Em textos na escrita árabe, é incomum usar diacríticos para informação de vogais e para alongamento de consoantes.” Assim, um trecho com uma kasra (uma marca de vogal curta), ou uma pergunta colada com um tashdid (a marca de duplicação), falha em toda correspondência exata com a grafia sem marcas. Na pesquisa de texto para fala em persa em que trabalhei para a MCI, a mesma propriedade era a parte difícil do lado da fala: as vogais curtas e o ezāfe, que liga um substantivo ao seu modificador, não são escritos. Para recuperação, remova os diacríticos dentro das comparações e mantenha-os no texto armazenado.

Usuários perguntam em linguagem coloquial; documentos são escritos na norma formal

“O persa é uma das línguas com muitas diferenças entre os estilos formal e informal de escrita”, escrevem Tajalli, Shamsfard e Kalantari, ao apresentar um corpus paralelo de 50.000 pares de frases. Um usuário digita می‌تونم onde o texto formal escreve می‌توانم. A minha leitura da consequência: uma busca lexical vê pouca sobreposição entre uma pergunta coloquial e uma cláusula formal, então é o retriever denso que tem alguma chance, e um conjunto de teste com perguntas bem formuladas vai esconder essa lacuna.

Normalização segura para persa

  • Converta o Yeh e o Kaf árabes para ی e ک persas na escrita, apenas no conteúdo em persa.

  • Mantenha o ZWNJ no texto armazenado.

  • Remova as marcas bidi da cópia que você indexa e compara, não do texto exibido.

  • Converta dígitos e remova diacríticos apenas dentro das comparações, dos dois lados.

  • Aplique às perguntas exatamente a mesma função que aplica aos trechos.

  • Nunca aplique a conversão persa a texto em árabe, em que o Yeh árabe (U+064A) e o Kaf árabe (U+0643) são as letras corretas.

O retriever deste site ainda não cumpre essa lista: ele converte as formas das letras persas em todos os idiomas, inclusive o árabe; a lista lexical dele pode não encontrar um trecho escrito com dígitos persas; e os diacríticos da escrita árabe são removidos dos termos da pergunta e de todas as comparações que a trava por frase faz, mas não do texto armazenado que a busca por palavras-chave percorre. Leia a lista como a meta.

Busca full-text em persa no PostgreSQL: não há configuração nativa

A busca full-text é a metade lexical óbvia de um sistema híbrido, e a documentação do PostgreSQL diz que ele “vem com configurações predefinidas para muitos idiomas”. O persa não é um deles.

O Snowball tem stemmer para árabe, não para persa

A lista de algoritmos de stemming do Snowball inclui o árabe e não tem entrada para persa nem para farsi. O PostgreSQL monta seus dicionários de stemming a partir do Snowball: a lista de idiomas no código-fonte do PostgreSQL 18 inclui arabic e nenhum persian, e a listagem de dicionários da documentação mostra arabic_stem e simple, sem nenhum dicionário para persa. Um stemmer de árabe compartilha a escrita do persa, não a gramática. Fora do banco de dados, o hazm traz um stemmer e um lematizador para persa; dentro dele, não há nenhum.

O que a configuração simple oferece, e o que não oferece

O template de dicionário simple, nas palavras da documentação de dicionários do PostgreSQL, funciona “convertendo o token de entrada para minúsculas e verificando-o em um arquivo de stop words”. O dicionário embutido simple não tem arquivo de stop words, então todo token é mantido, e não há stemming: قرارداد e قراردادها continuam sendo dois lexemas, uma perda de recall e um ganho de previsibilidade.

Antes de qualquer dicionário rodar, o parser decide o que é uma palavra, e a noção de letra dele “é determinada pela configuração de locale do banco de dados, especificamente lc_ctype”. Então não presuma como o seu banco divide o persa, muito menos palavras unidas por um ZWNJ. Pergunte ao próprio banco, com a função ts_debug da documentação de depuração da busca textual:

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

A busca por palavra-chave deste site usa simple para linhas em persa e english para linhas em inglês. A demo de busca semântica vs. por palavra-chave a executa ao lado do retriever híbrido do assistente, sobre o conteúdo deste site.

Similaridade por trigramas para erros de digitação

A extensão pg_trgm compara sequências de três caracteres, então uma palavra digitada errado ainda compartilha trigramas com a forma correta. Ela “ignora caracteres que não são de palavra (não alfanuméricos) ao extrair trigramas”, então verifique antes de depender disso: SELECT show_trgm('قرارداد'); deve retornar trigramas formados por letras persas. A busca por palavra-chave do site acrescenta um braço word_similarity, que compara uma consulta curta com a melhor extensão contínua de um trecho longo.

Cobertura de palavras inteiras: a lista lexical dentro do retriever deste site

O retriever do assistente não usa busca full-text na lista lexical, já que não existe configuração para persa. Ele conta quantas palavras de conteúdo da pergunta aparecem em um trecho como palavras inteiras, com limites de palavra de expressão regular. Ele remove as palavras vazias de cada um dos idiomas do site, descarta termos muito curtos, mantém os sinais vocálicos do híndi dentro da palavra a que pertencem e não aplica stemming. O resultado é uma razão de cobertura entre 0 e 1.

Os limites de palavra importam: uma correspondência de substring com “is” encontra “his”, “this” e “Isfahan”, e todo trecho começa a parecer evidência. A razão limitada também importa, porque um piso de similaridade pode ser comparado com ela. A razão só conta para o piso de recusa quando pelo menos duas palavras de conteúdo diferentes coincidem.

Vetores esparsos aprendidos: a opção documentada

A recuperação esparsa aprendida (learned sparse retrieval) compara termos como uma busca lexical, com pesos que um modelo aprende. No BGE-M3, os termos são pedaços do tokenizador (o model card dele atribui pesos a 'De', 'fin' e 'ation'), e o vetor tem “tamanho igual ao do vocabulário”, nas palavras do artigo que o apresentou: 250.002 na configuração do modelo. O retriever de produção deste site não os usa: ele roda denso mais lexical, e a API de embeddings que ele chama não retorna pesos esparsos. Se você for adicioná-los, leia antes a seção sobre o off-by-one, mais adiante.

Busca híbrida com reciprocal rank fusion: fundir posições, não pontuações

Um retriever denso e um retriever lexical retornam listas ranqueadas cujas pontuações significam coisas diferentes. A reciprocal rank fusion ignora as pontuações e combina as posições.

A fórmula, e de onde vem k = 60

Cormack, Clarke e Büttcher definem a pontuação fundida de um documento como a soma, sobre os rankings, de 1/(k + r(d)), em que r(d) é a posição dele em cada lista. A constante é menos delicada do que parece: “k = 60 foi fixado durante uma investigação piloto e não foi alterado durante a validação subsequente”, e o piloto “indicou que k = 60 era quase ótimo, mas que a escolha não era crítica”.

O retriever deste site usa k = 60, e cada lista contribui com mais candidatos do que a resposta vai usar, então um trecho no meio de uma lista e perto do topo da outra ainda pode aparecer. Em linhas gerais:

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]

Por que somar pontuações exige pesos que você precisa ajustar

A alternativa é somar pontuações, como faz o artigo por trás do BGE-M3: s_rank ← w1·s_dense + w2·s_lex + w3·s_mul, “em que os valores de w1, w2 e w3 dependem do cenário downstream”. Para denso mais esparso, os autores usam w1 = 1 e w2 = 0,3 no MIRACL, e w1 = 0,2 e w2 = 0,8 no MLDR, um benchmark de documentos longos.

Uma similaridade de cosseno, um produto interno esparso e uma razão de cobertura vivem em escalas diferentes, e a taxa de câmbio entre elas muda com o corpus. A fusão de posições evita essa calibração. É um trade-off, não uma vitória de graça: o artigo relata resultados fortes com somas ponderadas, e a RRF não consegue distinguir uma vitória esmagadora de uma vitória apertada.

Por que a lista lexical recebe metade do peso

Neste site, a lista lexical existe para promover trechos com strings exatas que um modelo denso pode borrar: um nome de domínio como booqai.app, uma sigla, o nome de um programa como SAMPAD. Esse é o motivo habitual para manter uma. No texto da Anthropic sobre Contextual Retrieval, um modelo de embedding “pode encontrar conteúdo sobre códigos de erro em geral, mas pode deixar passar a correspondência exata com ‘TS-999’”.

A lista lexical contribui com metade do peso, o que levanta um trecho que a lista densa também retornou, sem deixar que a sobreposição de palavras-chave se imponha ao ranking denso. O preço é real: com k = 60 e as quantidades de candidatos deste site, um trecho que só a lista lexical encontra fica abaixo de todos os candidatos densos, então a fusão não consegue trazer de volta um trecho que o modelo denso perdeu por completo. A única exceção é o trecho com a maior similaridade, que sempre é retornado, como explica a próxima seção. Se a chamada de embedding falhar, a recuperação recorre só à lista lexical, e o piso de similaridade continua valendo.

Uma pontuação RRF não é um grau de confiança

Uma pontuação RRF é construída só a partir de posições, então um corpus de puro ruído ainda produz um trecho em primeiro lugar, com a melhor pontuação fundida disponível. Por isso, o piso de recusa deste site é comparado com uma similaridade real de cada trecho, nunca com a pontuação fundida: a maior entre a similaridade de cosseno do trecho com a pergunta como foi feita (ou, em uma pergunta de continuação, com a pergunta unida à anterior, à qual também se une o item a que ela se refere quando é formulada como uma posição numa lista, ou o começo da resposta anterior quando esse item não pode ser lido) e a cobertura de palavras inteiras dele, que só conta quando pelo menos duas palavras de conteúdo coincidem. O piso é verificado antes de o modelo de linguagem ser chamado.

O trecho com a maior similaridade entre todos os candidatos sempre está entre os retornados, mesmo que a fusão o tenha classificado baixo demais para entrar na seleção. Abaixo do piso, uma pergunta que não é sobre mim é recusada sem chamar o modelo, e uma pergunta sobre mim recebe, em vez disso, os registros do site; o artigo sobre guardrails contra alucinação em RAG trata das duas coisas.

Nunca filtre a recuperação por idioma

Um filtro por idioma parece organização: perguntas em persa buscam trechos em persa. Em um corpus bilíngue, ele apaga evidências.

A sua melhor evidência pode estar no outro idioma

Neste site, trechos em todos os idiomas ficam lado a lado em uma única tabela, e uma pergunta busca em todos eles. O currículo em persa é o documento de origem e o currículo em inglês é uma adaptação, então uma pergunta em inglês sobre um cargo pode ter a sua melhor evidência em persa. O Kartez Contract tinha o mesmo formato na direção oposta: o assistente de elaboração precisava levar requisitos em persa para uma biblioteca de textos em inglês sobre elaboração de contratos.

Idioma como desempate, só para empates exatos

O idioma ainda tem uma utilidade no ranking: entre dois trechos com exatamente a mesma pontuação fundida, o que existe no idioma da pergunta vem primeiro, e depois um id estável decide, como na chave de ordenação acima. Com uma única lista densa e a lista lexical com metade do peso, empates exatos praticamente nunca acontecem. Eles podem acontecer quando uma pergunta é buscada em mais de uma versão, como descrito mais abaixo, e dois trechos obtêm as mesmas posições nas listas densas dessas versões, que têm o mesmo peso; mesmo assim, o idioma só decide quando apenas um dos dois existe no idioma da pergunta. Isso é proposital: o idioma nunca pode passar à frente de uma pontuação. O idioma tem mais uma função: escolher qual tradução de um trecho o leitor vê.

Um pequeno bônus para trechos do mesmo idioma não é um desempate. Contribuições adjacentes de uma lista com peso total diferem por 1/((k + r)(k + r + 1)), uma diferença que encolhe quanto mais se desce na lista, e somas de duas listas podem ficar ainda mais próximas. Um bônus fixo só é seguro enquanto permanecer abaixo de toda diferença que as profundidades e os pesos das suas listas possam produzir, um limite que se move sempre que qualquer um dos dois muda. Assim que o bônus ultrapassa esse limite, resultados que não estavam empatados trocam de lugar no meio da lista, justamente entre os candidatos que a fusão existe para levantar.

Um trecho, dez traduções

As páginas deste site são publicadas em até dez idiomas, e as cópias de um mesmo trecho em idiomas diferentes podiam ocupar vários dos oito lugares entregues ao modelo. Agora a recuperação trata as traduções de um trecho como um único candidato: ele é pontuado pela sua melhor tradução, ranqueado uma vez só e mostrado no idioma do leitor quando essa tradução existe. Os artigos são agrupados por seção, e não por chunk, porque uma seção longa se divide em um número diferente de chunks em cada idioma; uma tradução cujas seções não batem com as da versão em inglês fica separada, em vez de ser agrupada de forma errada. O idioma do leitor decide qual tradução aparece; fora de um empate exato, ele nunca decide a posição do trecho no ranking.

Perguntas em persa sobre documentos em inglês

Só o lado denso atravessa idiomas: uma pergunta em persa e um trecho em inglês quase não compartilham palavras, fora termos em alfabeto latino como RAG. A própria avaliação do BGE-M3 não resolve a questão para o persa: o benchmark cross-lingual dela, o MKQA, recupera trechos da Wikipedia em inglês para consultas em 25 idiomas além do inglês, com o árabe incluído e o persa não. Teste os seus próprios pares persa–inglês.

Perguntas sem palavras-chave

A pergunta de um visitante “O que ele fez recentemente?” não traz nenhuma palavra que o corpus consiga encontrar, e “E antes?” não traz nada. Quando uma pergunta é sobre mim, ela também é buscada com o meu nome na frente, e uma pergunta de continuação também é buscada unida à pergunta anterior, com o item a que se refere entre as duas quando é formulada como uma posição numa lista, como “o segundo”, ou com o começo da resposta anterior quando esse item não pode ser lido. Todas as versões viram embeddings em uma única chamada e são fundidas pela posição no ranking. A versão com o meu nome ordena os trechos, mas nunca conta para o piso de recusa, porque o meu nome sozinho pode fazer uma pergunta sobre mim, sem relação com o meu trabalho, parecer próxima do meu currículo. As perguntas sobre o meu trabalho como um todo não dependem da recuperação: são respondidas a partir de registros datados que o site monta com o meu currículo, os meus estudos de caso, os meus artigos e os meus serviços, como explica o artigo sobre guardrails contra alucinação em RAG.

Chunking de contratos em persa por artigo, cláusula e nota

Contratos em persa têm uma estrutura própria de artigos (ماده), cláusulas (بند) e notas (تبصره). No copiloto do Kartez Contract, o contrato do próprio usuário era dividido seguindo essa estrutura, e só as seções relevantes para uma pergunta eram usadas para respondê-la; as respostas “se baseiam apenas nas cláusulas selecionadas”. O estudo de caso do Kartez Contract cobre o resto do projeto, e o que construir contra alucinações de IA jurídica cobre o domínio.

O mesmo princípio orienta o chunker deste site: um chunk é um registro semântico. Um cargo do currículo nunca é separado das suas datas, um estudo de caso é dividido nas seções definidas pelo autor, um post nos seus cabeçalhos, e uma seção longa só é quebrada no limite de um parágrafo. Todo chunk de seção carrega o título do documento, então um chunk recuperado sozinho ainda diz de onde veio.

Para contratos em persa, isso sugere algumas regras, oferecidas como conselho geral e não como descrição de como o Kartez foi construído:

  • Mantenha o número junto com o texto. “ماده ۵” e o seu corpo pertencem a um só chunk.

  • Leve o título do artigo pai para todo chunk de cláusula e de nota.

  • Nunca corte uma cláusula por contagem de tokens. Uma nota separada da sua cláusula pode mudar o que a cláusula parece dizer.

  • Armazene o caminho estrutural, artigo › cláusula › nota, para que uma resposta possa indicar a um profissional o lugar exato.

pgvector: varredura exata ou HNSW?

O próprio pgvector responde metade dessa pergunta: “Por padrão, o pgvector realiza busca exata de vizinhos mais próximos, o que oferece recall perfeito.” Um índice aproximado “troca um pouco de recall por velocidade”.

Os filtros são o custo menos óbvio. “Com índices aproximados, a filtragem é aplicada depois que o índice é varrido. Se uma condição corresponder a 10% das linhas, com HNSW e o hnsw.ef_search padrão de 40, apenas 4 linhas vão corresponder, em média.” O pgvector 0.8.0 e versões posteriores conseguem varrer o índice de forma iterativa até que linhas suficientes correspondam, desde que você habilite isso.

A tabela de recuperação deste site não tem índice aproximado: a busca vetorial é uma varredura exata. O estado de publicação faz parte da cláusula WHERE do SQL, nunca é um filtro aplicado depois do LIMIT, onde encolheria o conjunto de resultados em silêncio. Comece com uma varredura exata, adicione um índice quando a latência exigir e, antes de confiar nele, compare o top-k do índice com o de uma varredura exata nas suas próprias perguntas.

O off-by-one do sparsevec no pgvector

Isso só importa se você adicionar vetores esparsos aprendidos ao PostgreSQL, e a falha é silenciosa.

O formato de texto para vetores esparsos no pgvector é {index1:value1,index2:value2}/dimensions, “e os índices começam em 1, como nos arrays SQL”. IDs de tokenizador começam em 0. O cliente Python recebe índices baseados em 0 (“os índices começam em 0”) e soma 1 por conta própria quando escreve a forma de texto.

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"

Desloque exatamente uma vez, no ponto em que um literal SQL é montado à mão. A entrada de texto do pgvector converte cada índice para base 0 e dispara “sparsevec index out of bounds” só quando o resultado fica abaixo de 0 ou é igual ou maior que a dimensão. Assim, um deslocamento duplo só falha no topo do vocabulário, um deslocamento ausente só falha no token de id 0, e todos os outros pesos caem em silêncio no token vizinho.

Como escolher um modelo de embedding para persa (farsi)

O que o MIRACL diz sobre persa e árabe

O MIRACL é um benchmark de recuperação em 18 idiomas, entre eles o persa e o árabe. O artigo por trás do BGE-M3 relata o nDCG@10, uma pontuação da qualidade do ranking nos dez primeiros resultados, no conjunto de desenvolvimento do benchmark (versão da ACL; os autores atualizaram esses resultados em julho de 2024):

Retriever

Persa (fa)

Árabe (ar)

BM25 sobre tokens do XLM-RoBERTa

28,7

39,5

mE5-large (E5 multilíngue)

59,0

76,0

BGE-M3 esparso

45,1

67,1

BGE-M3 denso

60,9

78,4

BGE-M3 denso + esparso + multivetor

63,4

80,2

Em persa, a recuperação densa pontua muito acima desse BM25, a esparsa aprendida fica entre as duas, e combinar os três modos acrescenta um pouco sobre o denso. Quatro ressalvas acompanham esses números:

  • Este BM25 usa tokens de subpalavra. Os autores o rodaram com o tokenizador do XLM-RoBERTa para alinhá-lo ao modelo; o apêndice deles mostra o analisador do Lucene elevando a média do MIRACL entre os idiomas de 31,9 para 38,5. A baseline BM25 do próprio MIRACL, com analisadores específicos de idioma quando eles existem, chega a 0,333 de nDCG@10 em persa e a 0,481 em árabe, numa escala de 0 a 1.

  • O MIRACL é monolíngue. Ele foi “projetado para dar suporte a tarefas de recuperação monolíngue, em que as consultas e os corpora estão no mesmo idioma”, então não diz nada sobre perguntas em persa sobre documentos em inglês.

  • Persa e árabe não formam uma comparação controlada. Os dois usam perguntas e corpora da Wikipedia diferentes; a página do projeto lista 632 consultas de desenvolvimento para o persa e 2.896 para o árabe, então a diferença é um sinal, não uma medida de dificuldade.

  • O BGE-M3 passou por fine-tuning com os dados de treino do MIRACL, então as linhas dele são pontuações in-domain.

FaMTEB e Hakim

O FaMTEB, publicado no Findings of EMNLP 2025, é um benchmark para persa construído sobre o MTEB, com “63 conjuntos de dados abrangendo sete tarefas diferentes”, entre elas recuperação.

O Hakim é um modelo de embedding para persa descrito em um preprint do arXiv, publicado pela primeira vez em maio de 2025 e revisado em outubro de 2025. Os autores relatam uma melhoria de 8,5% em relação às abordagens existentes no FaMTEB. Dois fatos devem ficar ao lado desse resultado: os autores do Hakim são também os autores do FaMTEB, e os dados de treino da segunda etapa do Hakim incluem o MIRACL_fa. Nenhum dos dois torna o resultado errado; os dois são motivos para testar com os seus próprios dados.

O que testar antes de escolher

  • As suas próprias perguntas em persa e em inglês, incluindo formulações coloquiais.

  • Pares entre idiomas: perguntas em persa respondidas por texto em inglês, e o inverso.

  • Variantes de grafia de uma mesma pergunta: as formas do Yeh e do Kaf, ZWNJ ou espaço, cada conjunto de dígitos.

  • A licença. O model card do BGE-M3 indica MIT.

  • O tamanho máximo de entrada comparado ao tamanho dos seus chunks, contado em tokens. Os autores do BGE-M3 descrevem documentos longos de até 8.192 tokens, e um texto em persa pode precisar de mais tokens que o inglês para o mesmo conteúdo; o imposto de tokens do persa encontrou esse acréscimo pequeno no próprio tokenizador do BGE-M3 e muito maior nos tokenizadores dos modelos de geração que mediu.

  • A dimensão do vetor. Neste site ela é fixada no schema, então mudá-la significa uma migração e uma reindexação completa.

O explorador de embeddings do site é propositalmente não neural: os vetores de n-gramas de caracteres dele agrupam palavras pela grafia, não pelo significado, e essa é a lacuna que um modelo denso precisa fechar para o persa coloquial.

Avaliação de RAG em persa com um golden set bilíngue

Um golden set para RAG em persa precisa de perguntas nos dois idiomas: perguntas respondíveis, com os trechos que deveriam ser recuperados; perguntas que exigem recusa, sem evidência no corpus; e perguntas cuja melhor evidência está no outro idioma. Meça o recall@k nas perguntas respondíveis, verifique se toda pergunta sem evidência fica abaixo do piso de similaridade e acrescente um teste que falhe se nenhuma resposta se apoiar em um trecho do outro idioma. Ele pega um filtro por idioma no mesmo dia em que alguém o colocar de volta.

Neste site, o golden set, nos dez idiomas do site e com a maioria das perguntas em inglês e em persa, roda no CI pelo código real de fusão, de agrupamento, de ordenação e de top-k, mas contra um corpus em memória e um embedder substituto do tipo bag-of-tokens, não o de produção. O substituto também alimenta uma lista esparsa que a produção não roda, e o conjunto sem evidência é pequeno. O CI pega regressões na engrenagem de ranqueamento, não o recall do assistente em produção. Um comando que só executa a recuperação, sem nunca chamar o modelo de linguagem, verifica o piso de recusa contra o embedder implantado; nenhuma medição de recall contra esse embedder foi feita, por isso não publico nenhum número de recall para este retriever.

O que vale também para o árabe

Não projetei nem avaliei recuperação para texto em árabe. O retriever deste site indexa o conteúdo em árabe do site, mas nada nele foi construído ou testado para o árabe. Esta seção é o que o design para persa implica no papel.

Quase tudo se transfere: fusão de posições, nenhum filtro por idioma, chunking que respeita a estrutura, remoção de diacríticos dentro das comparações (a afirmação do W3C cobre a escrita árabe em geral), varredura exata primeiro e um golden set bilíngue. Três coisas mudam:

  • O PostgreSQL já traz o árabe. O Snowball tem um stemmer para árabe e o PostgreSQL, um dicionário arabic_stem; se o stemming ajuda ou não, quem decide é o seu golden set.

  • A conversão de letras persas não pode rodar. Em árabe, o Yeh (U+064A) e o Kaf (U+0643) são as letras corretas, então um corpus misto precisa normalizar por idioma.

  • Os dígitos são outros. Os dígitos Arabic-Indic vão de U+0660 a U+0669, e não da faixa U+06F0 que o persa usa.

O árabe também tem mais cobertura em benchmarks, no MIRACL e no MKQA. Na tabela do MKQA do artigo do BGE-M3, perguntas em árabe sobre a Wikipedia em inglês chegam a um Recall@100 de 71,1 com o BGE-M3 denso, contra 18,9 do BM25 de subpalavras do artigo; o Recall@100 conta uma pergunta como encontrada quando um trecho que contém a resposta está entre os 100 primeiros. O persa não tem um número cross-lingual assim.

Checklist: busca híbrida para RAG em persa

  • Converta as formas das letras persas na escrita, apenas no conteúdo em persa; remova as marcas bidi da cópia de recuperação.

  • Aplique o mesmo normalizador a toda pergunta; mantenha o ZWNJ no texto armazenado.

  • Mantenha o lado lexical sem stemming; acrescente similaridade por trigramas para erros de digitação.

  • Funda os rankings denso e lexical com RRF, k = 60, e a lista lexical com metade do peso, sabendo que metade do peso não resgata um trecho que a lista densa perdeu.

  • Coloque o piso de recusa sobre uma similaridade, nunca sobre a pontuação RRF.

  • Nunca filtre por idioma; deixe o idioma decidir só empates exatos e qual tradução de um trecho é mostrada.

  • Divida em chunks seguindo a estrutura do documento, com o título em todo chunk.

  • Comece com uma varredura exata e confira qualquer índice aproximado contra ela.

  • Avalie com um golden set em cada idioma que você atende, com perguntas entre idiomas e perguntas sem evidência.

  • Teste modelos de embedding com as suas próprias perguntas em persa, incluindo as coloquiais.

Se você está planejando recuperação sobre conteúdo em persa ou em vários idiomas, o Diagnóstico de projeto transforma cinco perguntas em uma primeira arquitetura.

Perguntas frequentes

O BM25 basta para busca em persa?

Sozinho, não. O BM25 encontra nomes, números e códigos exatos, mas não pega paráfrases, e uma pergunta coloquial em persa pode ter pouco vocabulário em comum com um documento formal. No conjunto persa do MIRACL, os resultados densos publicados ficam bem acima do BM25, seja com tokens de subpalavra, seja com os analisadores de idioma do Lucene. Use o BM25, ou a cobertura de palavras inteiras, como a metade lexical de um sistema híbrido.

Devo traduzir consultas em persa para o inglês antes da recuperação?

Não por padrão. Um modelo de embedding multilíngue já mapeia os dois idiomas em um mesmo espaço vetorial, e a tradução acrescenta uma chamada de modelo que pode estragar nomes e termos exatos. O benchmark cross-lingual que os autores do BGE-M3 usaram inclui o árabe, mas não o persa; por isso, teste as duas abordagens nos seus próprios pares persa–inglês.

Qual é o melhor modelo de embedding para RAG em persa?

Benchmarks dão uma lista curta, não uma resposta: os resultados do MIRACL cobrem o BGE-M3 e o E5 multilíngue em persa, o FaMTEB abrange 63 conjuntos de dados, e os autores do Hakim relatam ganhos no FaMTEB. Teste a lista curta com as suas próprias perguntas coloquiais e entre idiomas, depois de verificar a licença e a dimensão do vetor.

Preciso de um banco de dados vetorial separado para RAG em persa?

Não para começar. O pgvector no PostgreSQL que você já tem faz busca exata de vizinhos mais próximos por padrão, com recall perfeito, e mantém filtros como o estado de publicação dentro da consulta SQL. Adicione um índice aproximado só quando a latência exigir.

Por que a minha busca em persa não encontra uma palavra que claramente está no texto?

Geralmente porque as strings diferem de um jeito que o olho não vê: Yeh ou Kaf árabes (U+064A, U+0643) no lugar de ی ou ک persas, um ZWNJ escrito como espaço ou omitido, outro conjunto de dígitos ou um diacrítico solto. Normalize trechos armazenados e perguntas com a mesma função, e converta dígitos e diacríticos dos dois lados da comparação.

Fontes