Búsqueda híbrida para RAG en persa: guía práctica
Búsqueda híbrida para RAG en persa (farsi) con PostgreSQL y pgvector: normalización segura, fusión RRF de rangos, sin filtro por idioma y pruebas bilingües.
La búsqueda híbrida para RAG en persa (farsi) consiste en ejecutar dos recuperadores en paralelo, una búsqueda densa por embeddings y una búsqueda léxica, y fusionar sus clasificaciones con la fusión recíproca de rangos (reciprocal rank fusion, RRF). Hacen falta las dos porque el persa rompe los supuestos tácitos de las recetas pensadas para el inglés: la misma palabra puede almacenarse con letras árabes o persas, dentro de las palabras se aloja un carácter de no unión de ancho cero, y ni Snowball ni PostgreSQL incluyen un stemmer para persa. En resumen: normalice al indexar, mantenga la parte léxica sin stemming, fusione rangos y no puntuaciones, no filtre nunca por idioma, fragmente siguiendo la estructura del propio documento y, antes de fiarse de nada de esto, pruébelo con un conjunto de preguntas bilingüe.
Esta guía se basa en dos sistemas. Uno es el asistente de este sitio, que he construido y mantengo. El otro es Kartez Contract, donde trabajé como ingeniero de IA: su copiloto tenía que encontrar las cláusulas correctas en contratos en persa, numerados por artículo, cláusula y nota. El diseño de recuperación que se describe aquí (búsqueda densa más léxica, fusión de rangos, pgvector) es el de este sitio; de Kartez tomo solo lo que dice su caso de estudio. Ninguno de los dos sistemas se somete aquí a un benchmark.
Por qué las recetas de RAG pensadas para el inglés fallan con texto en persa
El fallo típico no es una respuesta equivocada, sino la ausencia de respuesta: una búsqueda no encuentra una palabra que el usuario ve en el documento, porque dos cadenas que parecen idénticas son distintas por dentro.
Una palabra, dos cadenas: la ye y la kaf árabes y persas
El persa y el árabe comparten escritura, pero no todos los puntos de código. La ی persa (U+06CC ARABIC LETTER FARSI YEH) y la ye árabe (U+064A ARABIC LETTER YEH) son dos caracteres distintos, igual que la ک persa (U+06A9 ARABIC LETTER KEHEH) y la kaf árabe (U+0643 ARABIC LETTER KAF). Los requisitos de maquetación del W3C para árabe y persa mencionan expresamente el par de la ye: la letra «se representa con dos caracteres distintos». Un pasaje almacenado con una forma no coincide con una pregunta escrita con la otra; el paso de normalización del diagrama anterior muestra ambos pares.
Este sitio convierte a sus formas persas la ye árabe, la alif maqsura y la kaf árabe, entre otros caracteres, antes de que un pasaje entre en el índice de recuperación; además, quita de esa copia las marcas bidi invisibles y aplica la misma unificación a cada pregunta.
El ZWNJ (carácter de no unión de ancho cero) forma parte de la palabra
میشود, میشود y می شود son tres cadenas distintas. La primera separa el prefijo del verbo con un carácter de no unión de ancho cero (zero-width non-joiner, ZWNJ, U+200C), que impide que las letras se unan, y lo hace sin añadir un espacio. El estándar Unicode describe esta unión no predeterminada «como a veces se requiere al escribir persa en escritura árabe», y los requisitos del W3C indican que los caracteres de control «deberían conservarse al almacenar y mostrar textos».
Si elimina el ZWNJ, como podría hacer un pipeline pensado para el inglés, میشود se convierte en میشود; si lo sustituye por un espacio, una palabra pasa a ser dos tokens. El normalizador de hazm, un conjunto de herramientas de PLN para persa, incluye una «corrección del ZWNJ» que convierte کتاب های en کتابهای. En este sitio, el texto almacenado conserva el ZWNJ, y solo la unificación que se usa dentro de las comparaciones lo trata como un espacio.
Tres juegos de dígitos
El persa escribe ۱۲۳ con dígitos arábigo-índicos orientales (U+06F0 a U+06F9), el árabe escribe el mismo número con dígitos arábigo-índicos (U+0660 a U+0669) y el texto técnico suele escribir 123.
Unifique los dígitos solo para comparar, nunca en el texto almacenado; en este sitio, reescribir los dígitos persas almacenados cambiaría cómo se muestra el CV en persa. Eso sí, la unificación tiene que aplicarse en los dos lados: si unifica solo los dígitos de la pregunta, una pregunta escrita con dígitos persas deja de coincidir con el pasaje del que se copió. El artículo sobre guardrails para LLM en streaming aplica la misma unificación a los números que emite un modelo.
Los diacríticos son poco habituales: un solo signo suelto rompe la coincidencia exacta
Los requisitos del W3C lo dicen sin rodeos: «En los textos en escritura árabe no es habitual usar diacríticos para indicar las vocales ni la geminación de las consonantes». Por eso, basta con que un pasaje lleve una kasra (un signo de vocal breve), o con que una pregunta pegada incluya un tashdid (el signo de geminación), para que falle cualquier coincidencia exacta con la grafía sin signos. En la investigación sobre síntesis de voz en persa en la que trabajé para MCI, esa misma propiedad era la parte difícil vista desde el lado de la voz: las vocales breves y la ezāfe, que enlaza un sustantivo con su modificador, no se escriben. Para la recuperación, elimine los diacríticos dentro de las comparaciones y consérvelos en el texto almacenado.
Los usuarios preguntan en registro coloquial; los documentos se redactan en registro formal
«El persa es una de las lenguas con muchas diferencias entre sus estilos de escritura formal e informal», escriben Tajalli, Shamsfard y Kalantari al presentar un corpus paralelo de 50.000 pares de oraciones. Un usuario escribe میتونم donde el texto formal escribe میتوانم. Lo que deduzco de ahí: un sistema de coincidencia léxica ve poco solapamiento entre una pregunta coloquial y una cláusula formal, así que es el recuperador denso el que tiene alguna posibilidad, y un conjunto de prueba con preguntas bien formuladas ocultará esa brecha.
Normalización segura para el persa
Convierta la ye y la kaf árabes en las persas ی y ک al indexar, solo en el contenido en persa.
Conserve el ZWNJ en el texto almacenado.
Quite las marcas bidi de la copia que indexa y compara, no del texto que se muestra.
Unifique los dígitos y elimine los diacríticos solo dentro de las comparaciones, en los dos lados.
Aplique a las preguntas exactamente la misma función que a los pasajes.
No aplique nunca la unificación persa a texto en árabe, donde la ye árabe (U+064A) y la kaf árabe (U+0643) son las letras correctas.
El recuperador de este sitio todavía no cumple esa lista: unifica las formas de las letras persas en todos los idiomas, árabe incluido; su lista léxica puede no encontrar un pasaje escrito con dígitos persas, y los diacríticos de la escritura árabe se eliminan de los términos de la pregunta y de todas las comparaciones que hace la compuerta por frases, pero no del texto almacenado que recorre la búsqueda por palabras clave. Tómese esa lista como el objetivo.
Búsqueda de texto completo en persa con PostgreSQL: no hay configuración integrada
La búsqueda de texto completo es la mitad léxica obvia de un sistema híbrido, y la documentación de PostgreSQL dice que «incluye configuraciones predefinidas para muchos idiomas». El persa no es uno de ellos.
Snowball tiene un stemmer para árabe, pero no para persa
La lista de algoritmos de stemming de Snowball incluye el árabe y no tiene ninguna entrada para el persa, ni como Persian ni como Farsi. PostgreSQL construye sus diccionarios de stemming a partir de Snowball: la lista de idiomas del código fuente de PostgreSQL 18 incluye arabic y no persian, y el listado de diccionarios de la documentación muestra arabic_stem y simple, sin ningún diccionario persa. Un stemmer para árabe comparte con el persa la escritura, no la gramática. Fuera de la base de datos, hazm incluye un stemmer y un lematizador para persa; dentro, no hay ninguno.
Qué ofrece la configuración simple y qué no
La plantilla de diccionario simple, en palabras de la documentación de diccionarios de PostgreSQL, funciona «convirtiendo el token de entrada a minúsculas y cotejándolo con un archivo de palabras vacías». El diccionario integrado simple no tiene archivo de palabras vacías, así que se conservan todos los tokens, y no hay stemming: قرارداد y قراردادها siguen siendo dos lexemas, una pérdida de recall y una ganancia de previsibilidad.
Antes de que actúe cualquier diccionario, el parser decide qué es una palabra, y su noción de letra «está determinada por la configuración regional de la base de datos, en concreto por lc_ctype». Así que no dé por supuesto cómo divide su base de datos el texto en persa, y menos aún las palabras unidas por un ZWNJ. Pregúnteselo a la propia base de datos con la función ts_debug de la documentación sobre depuración de la búsqueda de texto:
-- How does this database tokenise Persian? Check; do not assume.
SELECT alias, token, lexemes
FROM ts_debug('simple', 'قراردادها را میتوان فسخ کرد');La búsqueda por palabras clave de este sitio usa simple para las filas en persa y english para las filas en inglés. La demo de búsqueda semántica frente a búsqueda por palabras clave la ejecuta junto al recuperador híbrido del asistente sobre el contenido de este sitio.
Similitud por trigramas para las erratas
La extensión pg_trgm compara secuencias de tres caracteres, así que una palabra mal escrita sigue compartiendo trigramas con la correcta. «Ignora los caracteres que no forman parte de palabras (los no alfanuméricos) al extraer trigramas», así que compruébelo antes de confiar en ella: SELECT show_trgm('قرارداد'); debería devolver trigramas formados por letras persas. La búsqueda por palabras clave del sitio añade una rama con word_similarity, que compara una consulta corta con el tramo continuo de un pasaje largo que mejor encaja con ella.
Cobertura de palabras completas: la lista léxica del recuperador de este sitio
El recuperador del asistente no usa la búsqueda de texto completo para su lista léxica, ya que no hay configuración para persa. Cuenta cuántas palabras de contenido de la pregunta aparecen en un pasaje como palabras completas, con los límites de palabra de las expresiones regulares. Elimina las palabras vacías de cada uno de los idiomas del sitio, descarta los términos muy cortos, mantiene los signos vocálicos del hindi dentro de la palabra a la que pertenecen y no aplica stemming. El resultado es una proporción de cobertura entre 0 y 1.
Los límites de palabra importan: una coincidencia de subcadena con «is» encuentra también «his», «this» e «Isfahan», y todos los pasajes empiezan a parecer evidencia. También importa que la proporción esté acotada, porque se puede comparar con un umbral mínimo de similitud. La proporción solo cuenta para el umbral de rechazo cuando coinciden al menos dos palabras de contenido distintas.
Vectores dispersos aprendidos: la opción documentada
La recuperación dispersa aprendida (learned sparse retrieval) empareja términos como una búsqueda léxica, pero con pesos que aprende un modelo. En BGE-M3 los términos son piezas del tokenizador (su ficha del modelo pondera 'De', 'fin' y 'ation'), y el vector tiene «un tamaño igual al del vocabulario», en palabras del artículo que presentó el modelo: 250.002 en la configuración del modelo. El recuperador de producción de este sitio no los usa: combina búsqueda densa y léxica, y la API de embeddings a la que llama no devuelve pesos dispersos. Si los añade, lea antes la sección sobre el error off-by-one que aparece más abajo.
Búsqueda híbrida con fusión recíproca de rangos (RRF): fusionar rangos, no puntuaciones
Un recuperador denso y uno léxico devuelven listas ordenadas cuyas puntuaciones significan cosas distintas. La fusión recíproca de rangos ignora las puntuaciones y combina las posiciones.
La fórmula y de dónde sale k = 60
Cormack, Clarke y Büttcher definen la puntuación fusionada de un documento como la suma, sobre todas las clasificaciones, de 1/(k + r(d)), donde r(d) es su posición en cada lista. La constante es menos delicada de lo que parece: «k = 60 se fijó durante una investigación piloto y no se modificó durante la validación posterior», y el piloto «indicó que k = 60 era casi óptimo, pero que la elección no era crítica».
El recuperador de este sitio usa k = 60, y cada lista aporta más candidatos de los que usará la respuesta, así que un pasaje situado a mitad de una lista y cerca del principio de la otra todavía puede aflorar. A grandes rasgos:
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 qué sumar puntuaciones exige pesos que hay que ajustar
La alternativa es sumar puntuaciones, como hace el artículo de BGE-M3: s_rank ← w1·s_dense + w2·s_lex + w3·s_mul, «donde los valores de w1, w2 y w3 dependen del escenario de aplicación». Para denso más disperso, los autores fijan w1 = 1 y w2 = 0,3 en MIRACL, y w1 = 0,2 y w2 = 0,8 en MLDR, un benchmark de documentos largos.
Una similitud del coseno, un producto interno disperso y una proporción de cobertura viven en escalas distintas, y el tipo de cambio entre ellas se mueve con el corpus. La fusión por rangos esquiva esa calibración. Es un compromiso, no una ventaja sin coste: el artículo presenta resultados sólidos con sumas ponderadas, y RRF no distingue una victoria aplastante de una por la mínima.
Por qué la lista léxica tiene la mitad de peso
En este sitio, la lista léxica sirve para hacer subir pasajes con cadenas exactas que un modelo denso puede difuminar: un nombre de dominio como booqai.app, un acrónimo, el nombre de un programa como SAMPAD. Ese es el motivo habitual para mantener una. En el artículo de Anthropic sobre Contextual Retrieval, un modelo de embeddings «podría encontrar contenido sobre códigos de error en general, pero pasar por alto la coincidencia exacta con “TS-999”».
La lista léxica contribuye con la mitad de peso, lo que hace subir un pasaje que la lista densa también ha devuelto sin dejar que la coincidencia de palabras clave se imponga a la clasificación densa. El precio es real: con k = 60 y el número de candidatos de este sitio, un pasaje que solo encuentra la lista léxica queda por debajo de todos los candidatos densos, así que la fusión no puede rescatar un pasaje que el modelo denso haya pasado por alto por completo. La única excepción es el pasaje con la mayor similitud, que siempre se devuelve, como explica la sección siguiente. Si falla la llamada de embeddings, la recuperación recurre solo a la lista léxica, y el umbral mínimo de similitud se sigue aplicando.
Una puntuación RRF no es una medida de confianza
Una puntuación RRF se construye solo a partir de posiciones, así que un corpus de puro ruido sigue produciendo un pasaje en primera posición con la mejor puntuación fusionada disponible. Por eso el umbral de rechazo de este sitio se compara con una similitud real de cada pasaje, nunca con la puntuación fusionada: la mayor entre su similitud del coseno con la pregunta tal como se formuló (o, en una pregunta de seguimiento, con la pregunta unida a la anterior, a la que se une también el elemento al que se refiere si está formulada como una posición en una lista, o el comienzo de la respuesta anterior si ese elemento no se puede leer) y su cobertura de palabras completas, que solo cuenta cuando coinciden al menos dos palabras de contenido. El umbral se comprueba antes de llamar al modelo de lenguaje.
El pasaje con la mayor similitud entre todos los candidatos siempre está entre los devueltos, aunque la fusión lo haya clasificado demasiado abajo para entrar en la selección. Por debajo del umbral, una pregunta que no trata sobre mí se rechaza sin llamar al modelo, y una pregunta sobre mí recibe en su lugar los registros del sitio; el artículo sobre guardrails contra las alucinaciones en RAG explica ambas cosas.
No filtre nunca la recuperación por idioma
Un filtro por idioma parece una cuestión de orden: las preguntas en persa buscan en pasajes en persa. En un corpus bilingüe, elimina evidencia.
Su mejor evidencia puede estar en el otro idioma
En este sitio, los pasajes de todos los idiomas conviven en una misma tabla, y cada pregunta busca en todos ellos. El CV en persa es el documento original y el CV en inglés es una adaptación, así que una pregunta en inglés sobre un puesto puede tener su mejor evidencia en persa. Kartez Contract presentaba el mismo patrón en sentido contrario: su asistente guiado de redacción tenía que trasladar requisitos expresados en persa a una biblioteca de textos en inglés para la redacción de contratos.
El idioma como desempate solo para empates exactos
El idioma sigue teniendo una utilidad en la clasificación: entre dos pasajes con exactamente la misma puntuación fusionada, va primero el que está disponible en el idioma de la pregunta, y después decide un id estable, como en la clave de ordenación del código anterior. Con una sola lista densa y la lista léxica a mitad de peso, los empates exactos no se dan prácticamente nunca. Pueden darse cuando una pregunta se busca en más de una versión, como se describe más abajo, y dos pasajes obtienen las mismas posiciones en las listas densas de esas versiones, que pesan lo mismo; incluso entonces, el idioma decide únicamente cuando solo uno de los dos existe en el idioma de la pregunta. Es deliberado: el idioma nunca debe pesar más que una puntuación. El idioma tiene además otra función: elegir qué traducción de un pasaje ve el lector.
Una pequeña bonificación para los pasajes en el mismo idioma no es un desempate. Las contribuciones adyacentes de una lista con peso completo difieren en 1/((k + r)(k + r + 1)), una diferencia que se reduce a medida que se baja por la lista, y las sumas de dos listas pueden quedar todavía más cerca. Una bonificación fija solo es segura mientras quede por debajo de todas las diferencias que puedan producir la profundidad y los pesos de sus listas, un límite que se mueve en cuanto cambia cualquiera de los dos. Cuando la bonificación supera ese límite, resultados que no estaban empatados intercambian posiciones a mitad de la lista, justo entre los candidatos que la fusión debe hacer subir.
Un pasaje, diez traducciones
Las páginas de este sitio se publican en hasta diez idiomas, y las copias de un mismo pasaje en distintos idiomas podían ocupar varios de los ocho puestos que se entregan al modelo. Ahora la recuperación trata las traducciones de un pasaje como un único candidato: se puntúa por su mejor traducción, se clasifica una sola vez y se muestra en el idioma del lector cuando existe esa traducción. Los artículos se agrupan por sección y no por fragmento, porque una sección larga se divide en un número distinto de fragmentos en cada idioma; una traducción cuyas secciones no coinciden con las de la versión en inglés se mantiene aparte en lugar de agruparse mal. El idioma del lector decide qué traducción se muestra; salvo en un empate exacto, nunca decide en qué puesto queda el pasaje.
Preguntas en persa sobre documentos en inglés
Solo la parte densa puede cruzar de un idioma a otro: una pregunta en persa y un pasaje en inglés apenas comparten palabras, salvo términos en alfabeto latino como RAG. La propia evaluación de BGE-M3 no lo resuelve para el persa: su benchmark interlingüe, MKQA, recupera pasajes de la Wikipedia en inglés para consultas en 25 idiomas distintos del inglés, entre ellos el árabe, pero no el persa. Pruebe sus propios pares persa–inglés.
Preguntas sin palabras clave
La pregunta de un visitante «¿Qué ha hecho recientemente?» no contiene ninguna palabra con la que el corpus pueda coincidir, y «¿Y antes?» no contiene nada en absoluto. Cuando una pregunta trata sobre mí, también se busca con mi nombre delante, y una pregunta de seguimiento también se busca unida a la pregunta anterior, con el elemento al que se refiere entre ambas cuando está formulada como una posición en una lista, como «el segundo», o con el comienzo de la respuesta anterior si ese elemento no se puede leer. Todas las versiones se convierten en embeddings en una sola llamada y se fusionan por rango. La versión con mi nombre ordena los pasajes, pero nunca cuenta para el umbral de rechazo, porque mi nombre por sí solo puede hacer que una pregunta sobre mí que nada tiene que ver con mi trabajo parezca cercana a mi CV. Las preguntas sobre mi trabajo en su conjunto no dependen de la recuperación: se responden a partir de registros fechados que el sitio construye con mi CV, mis casos de estudio, mis artículos y mis servicios, como explica el artículo sobre guardrails contra las alucinaciones en RAG.
Fragmentación de contratos en persa por artículo, cláusula y nota
Los contratos en persa tienen su propia estructura de artículos (ماده), cláusulas (بند) y notas (تبصره). En el copiloto de Kartez Contract, el propio contrato del usuario se dividía siguiendo esa estructura, y para responder a una pregunta solo se usaban las secciones relevantes para ella; las respuestas «se basan únicamente en las cláusulas seleccionadas». El caso de estudio de Kartez Contract recoge el resto del proyecto, y qué construir frente a las alucinaciones de la IA jurídica trata ese ámbito.
El mismo principio guía la fragmentación de este sitio: un fragmento es un registro semántico. Un puesto del CV nunca se separa de sus fechas, un caso de estudio se divide por las secciones que escribió su autor, una entrada del blog por sus encabezados, y una sección larga solo se parte en un límite de párrafo. Cada fragmento de sección lleva el título del documento, de modo que un fragmento recuperado por sí solo sigue indicando de dónde procede.
Para los contratos en persa, eso sugiere algunas reglas, que se ofrecen como consejo general y no como descripción de cómo se construyó Kartez:
Mantenga el número junto al texto. «ماده ۵» y su cuerpo van en el mismo fragmento.
Incluya el encabezado del artículo padre en cada fragmento de cláusula y de nota.
No corte nunca una cláusula por número de tokens. Una nota separada de su cláusula puede cambiar lo que la cláusula parece decir.
Guarde la ruta estructural, artículo › cláusula › nota, para que una respuesta pueda indicar a un profesional el lugar exacto.
pgvector: ¿escaneo exacto o HNSW?
pgvector responde por sí mismo a la mitad de la pregunta: «De forma predeterminada, pgvector realiza una búsqueda exacta del vecino más cercano, que proporciona un recall perfecto». Un índice aproximado «sacrifica algo de recall a cambio de velocidad».
Los filtros son el coste menos evidente. «Con los índices aproximados, el filtrado se aplica después de recorrer el índice. Si una condición coincide con el 10 % de las filas, con HNSW y el valor predeterminado de hnsw.ef_search, que es 40, de media solo coincidirán 4 filas». pgvector 0.8.0 y las versiones posteriores pueden recorrer el índice de forma iterativa hasta que coincidan suficientes filas, siempre que se active esa opción.
La tabla de recuperación de este sitio no tiene ningún índice aproximado: la búsqueda vectorial es un escaneo exacto. El estado de publicación forma parte de la cláusula WHERE de SQL; nunca es un filtro aplicado después de LIMIT, donde reduciría en silencio el conjunto de resultados. Empiece con un escaneo exacto, añada un índice cuando la latencia lo exija y, antes de confiar en él, compare su top-k con el de un escaneo exacto sobre sus propias preguntas.
El error off-by-one de sparsevec en pgvector
Esto solo importa si añade vectores dispersos aprendidos a PostgreSQL, y el fallo es silencioso.
El formato de texto de los vectores dispersos en pgvector es {index1:value1,index2:value2}/dimensions, «y los índices empiezan en 1, como en los arrays de SQL». Los ids del tokenizador empiezan en 0. El cliente de Python recibe índices de base 0 («Indices start at 0») y suma 1 por su cuenta cuando escribe la 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"Desplace los índices exactamente una vez, allí donde construya a mano un literal SQL. La entrada de texto de pgvector convierte cada índice a base 0 y lanza «sparsevec index out of bounds» solo cuando el resultado es menor que 0, o igual o mayor que la dimensión. Así, un desplazamiento doble solo falla en el extremo superior del vocabulario; uno omitido, solo con el token de id 0; y cualquier otro peso cae sin avisar en el token vecino.
Cómo elegir un modelo de embeddings para persa (farsi)
Qué dice MIRACL sobre el persa y el árabe
MIRACL es un benchmark de recuperación en 18 idiomas, entre ellos el persa y el árabe. El artículo de BGE-M3 publica el nDCG@10, una puntuación de la calidad de la clasificación en los diez primeros resultados, sobre su conjunto de desarrollo (versión de ACL; los autores actualizaron estos resultados en julio de 2024):
Recuperador | Persa (fa) | Árabe (ar) |
|---|---|---|
BM25 sobre tokens de XLM-RoBERTa | 28,7 | 39,5 |
mE5-large (E5 multilingüe) | 59,0 | 76,0 |
BGE-M3 disperso | 45,1 | 67,1 |
BGE-M3 denso | 60,9 | 78,4 |
BGE-M3 denso + disperso + multivector | 63,4 | 80,2 |
En persa, la recuperación densa puntúa muy por encima de este BM25, la dispersa aprendida queda entre las dos y combinar los tres modos añade un poco sobre la densa. Estas cifras vienen con cuatro salvedades:
Este BM25 usa tokens de subpalabra. Los autores lo ejecutaron con el tokenizador de XLM-RoBERTa para alinearlo con el modelo; su apéndice muestra que el analizador de Lucene eleva la media de MIRACL en todos los idiomas de 31,9 a 38,5. La línea base BM25 del propio MIRACL, con analizadores específicos de cada idioma cuando existen, alcanza 0,333 de nDCG@10 en persa y 0,481 en árabe, en una escala de 0 a 1.
MIRACL es monolingüe. Está «diseñado para dar soporte a tareas de recuperación monolingüe, en las que las consultas y los corpus están en el mismo idioma», así que no dice nada sobre las preguntas en persa que buscan en documentos en inglés.
El persa y el árabe no forman una comparación controlada. Usan preguntas y corpus de Wikipedia distintos; la página del proyecto indica 632 consultas de desarrollo para el persa y 2896 para el árabe, así que la diferencia es una señal, no una medida de dificultad.
BGE-M3 se afinó con los datos de entrenamiento de MIRACL, así que sus filas son puntuaciones dentro del dominio.
FaMTEB y Hakim
FaMTEB, publicado en Findings of EMNLP 2025, es un benchmark para persa construido sobre MTEB, con «63 conjuntos de datos que abarcan siete tareas distintas», entre ellas la recuperación.
Hakim es un modelo de embeddings para persa descrito en un preprint de arXiv, publicado por primera vez en mayo de 2025 y revisado en octubre de 2025. Sus autores informan de una mejora del 8,5 % sobre los enfoques existentes en FaMTEB. Junto a ese resultado conviene poner dos hechos: los autores de Hakim son también los de FaMTEB, y entre los datos de entrenamiento de la segunda etapa de Hakim figura MIRACL_fa. Ninguno de los dos invalida el resultado; ambos son motivos para probar con sus propios datos.
Qué probar antes de elegir
Sus propias preguntas en persa y en inglés, incluidas las formulaciones coloquiales.
Pares interlingües: preguntas en persa respondidas por texto en inglés, y al revés.
Variantes ortográficas de una misma pregunta: formas de la ye y la kaf, ZWNJ o espacio, cada juego de dígitos.
La licencia. La ficha del modelo BGE-M3 indica MIT.
La longitud máxima de entrada frente al tamaño de sus fragmentos, en tokens. Los autores de BGE-M3 describen documentos largos de hasta 8192 tokens, y el persa puede necesitar más tokens que el inglés para el mismo contenido; el artículo sobre el impuesto de tokens del persa comprobó que ese sobrecoste era pequeño con el propio tokenizador de BGE-M3 y mucho mayor con los tokenizadores de los modelos de generación que midió.
La dimensión del vector. En este sitio está fijada en el esquema, así que cambiarla supone una migración y una reindexación completa.
El explorador de embeddings del sitio no es neuronal a propósito: sus vectores de n-gramas de caracteres agrupan las palabras por su ortografía, no por su significado, y esa es la brecha que un modelo denso tiene que cerrar en el persa coloquial.
Evaluación de RAG en persa con un conjunto de referencia (golden set) bilingüe
Un conjunto de referencia para RAG en persa necesita preguntas en los dos idiomas: preguntas con respuesta, junto con los pasajes que deberían recuperar; preguntas que deben rechazarse porque el corpus no contiene evidencia; y preguntas cuya mejor evidencia está en el otro idioma. Mida el recall@k en las preguntas con respuesta, compruebe que todas las preguntas sin evidencia quedan por debajo del umbral mínimo de similitud y añada una prueba que falle si ninguna respuesta se apoya en un pasaje del otro idioma. Esa prueba detecta un filtro por idioma el mismo día en que alguien vuelva a añadirlo.
En este sitio, el conjunto de referencia, en los diez idiomas del sitio y con la mayoría de las preguntas en inglés y en persa, se ejecuta en CI a través del código real de fusión, agrupación, ordenación y top-k, pero sobre un corpus en memoria y con un modelo de embeddings sustituto basado en bolsa de tokens, no con el de producción. El sustituto alimenta además una lista dispersa que no se ejecuta en producción, y el conjunto de preguntas sin evidencia es pequeño. CI detecta regresiones en la maquinaria de clasificación, no el recall del asistente en funcionamiento. Un comando que solo ejecuta la recuperación, sin llamar nunca al modelo de lenguaje, comprueba el umbral de rechazo con el modelo de embeddings desplegado; no se ha hecho ninguna ejecución de recall con ese modelo, así que no publico ninguna cifra de recall para este recuperador.
Qué se aplica también al árabe
No he diseñado ni evaluado la recuperación para texto en árabe. El recuperador de este sitio indexa su contenido en árabe, pero nada en él se construyó ni se probó para el árabe. Esta sección recoge lo que el diseño para el persa implica sobre el papel.
La mayor parte se aplica igual: la fusión por rangos, la ausencia de filtro por idioma, la fragmentación según la estructura, la eliminación de diacríticos dentro de las comparaciones (la afirmación del W3C abarca la escritura árabe en general), empezar con un escaneo exacto y un conjunto de referencia bilingüe. Hay tres diferencias:
PostgreSQL sí incluye el árabe. Snowball tiene un stemmer para árabe y PostgreSQL, un diccionario
arabic_stem; si el stemming ayuda o no, lo decidirá su conjunto de referencia.La unificación de letras persas no debe ejecutarse. En árabe, la ye (U+064A) y la kaf (U+0643) son las letras correctas, así que un corpus mixto tiene que normalizar por idioma.
Los dígitos son distintos. Los dígitos arábigo-índicos van de U+0660 a U+0669, no del rango U+06F0 que usa el persa.
El árabe tiene además más cobertura en benchmarks, en MIRACL y en MKQA. En la tabla de MKQA del artículo de BGE-M3, las preguntas en árabe sobre la Wikipedia en inglés alcanzan un Recall@100 de 71,1 con BGE-M3 denso, frente a 18,9 del BM25 de subpalabras del artículo; Recall@100 cuenta una pregunta como encontrada cuando un pasaje que contiene su respuesta está entre los 100 primeros resultados. El persa no tiene una cifra interlingüe equivalente.
Lista de comprobación: búsqueda híbrida para RAG en persa
Unifique las formas de las letras persas al indexar, solo en el contenido en persa; quite las marcas bidi de la copia de recuperación.
Aplique el mismo normalizador a cada pregunta y conserve el ZWNJ en el texto almacenado.
Mantenga la parte léxica sin stemming y añada similitud por trigramas para las erratas.
Fusione los rangos densos y léxicos con RRF, k = 60, con la lista léxica a mitad de peso, sabiendo que esa mitad de peso no puede rescatar un pasaje que la lista densa no haya encontrado.
Aplique el umbral de rechazo sobre una similitud, nunca sobre la puntuación RRF.
No filtre nunca por idioma; deje que el idioma decida solo los empates exactos y qué traducción de un pasaje se muestra.
Fragmente siguiendo la estructura del documento, con el título en cada fragmento.
Empiece con un escaneo exacto y contraste con él cualquier índice aproximado.
Evalúe con un conjunto de referencia en cada idioma que ofrezca, que incluya preguntas interlingües y preguntas sin evidencia.
Pruebe los modelos de embeddings con sus propias preguntas en persa, incluidas las coloquiales.
Si está planificando la recuperación sobre contenido en persa o en varios idiomas, el Diagnóstico de proyecto convierte cinco preguntas en un primer esbozo de arquitectura.
Preguntas frecuentes
¿Basta con BM25 para la búsqueda en persa?
Por sí solo, no. BM25 encuentra nombres, números y códigos exactos, pero se le escapan las paráfrasis, y una pregunta coloquial en persa puede compartir poco vocabulario con un documento formal. En el conjunto persa de MIRACL, los resultados densos publicados quedan muy por encima de BM25, tanto si BM25 usa tokens de subpalabra como si usa los analizadores de idioma de Lucene. Use BM25, o la cobertura de palabras completas, como la mitad léxica de un sistema híbrido.
¿Debo traducir al inglés las consultas en persa antes de la recuperación?
No por defecto. Un modelo de embeddings multilingüe ya proyecta ambos idiomas en un mismo espacio vectorial, y la traducción añade una llamada a un modelo que puede estropear nombres y términos exactos. El benchmark interlingüe que usaron los autores de BGE-M3 incluye el árabe, pero no el persa, así que pruebe los dos enfoques con sus propios pares persa–inglés.
¿Cuál es el mejor modelo de embeddings para RAG en persa?
Los benchmarks dan una lista corta, no una respuesta: los resultados de MIRACL cubren BGE-M3 y E5 multilingüe en persa, FaMTEB abarca 63 conjuntos de datos y los autores de Hakim informan de mejoras en FaMTEB. Pruebe esa lista corta con sus propias preguntas coloquiales e interlingües, después de comprobar la licencia y la dimensión del vector.
¿Necesito una base de datos vectorial aparte para RAG en persa?
Para empezar, no. pgvector, en el PostgreSQL que ya tiene, realiza por defecto una búsqueda exacta del vecino más cercano, con recall perfecto, y mantiene filtros como el estado de publicación dentro de la consulta SQL. Añada un índice aproximado solo cuando la latencia lo requiera.
¿Por qué mi búsqueda en persa no encuentra una palabra que está claramente en el texto?
Normalmente porque las cadenas difieren de una forma que el ojo no ve: la ye o la kaf árabes (U+064A, U+0643) frente a la ی o la ک persas, un ZWNJ escrito como espacio u omitido, otro juego de dígitos o un diacrítico suelto. Normalice los pasajes almacenados y las preguntas con la misma función, y unifique los dígitos y quite los diacríticos en los dos lados de la comparación.
Fuentes
G. V. Cormack, C. L. A. Clarke y S. Büttcher, «Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods», SIGIR 2009.
Chen, Xiao, Zhang, Luo, Lian y Liu, «M3-Embedding: Multi-Linguality, Multi-Functionality, Multi-Granularity Text Embeddings Through Self-Knowledge Distillation», Findings of ACL 2024.
BAAI, ficha del modelo BGE-M3.
Zhang et al., «MIRACL: A Multilingual Retrieval Dataset Covering 18 Diverse Languages», TACL 2023 (PDF con la tabla de líneas base), y la página del proyecto MIRACL.
Zinvandi et al., «FaMTEB: Massive Text Embedding Benchmark in Persian Language», Findings of EMNLP 2025.
Sarmadi et al., «Hakim: Farsi Text Embedding Model», preprint de arXiv, 2025.
README de pgvector, código fuente de los vectores dispersos de pgvector y README de pgvector-python.
Documentación de PostgreSQL: introducción a la búsqueda de texto, diccionarios, soporte en psql, parsers, pruebas y depuración de la búsqueda de texto y pg_trgm.
Código fuente de PostgreSQL (REL_18_STABLE): snowball_create.pl y pg_ts_dict.dat.
W3C, Arabic & Persian Layout Requirements, Group Draft Note, 2 de octubre de 2025.
El estándar Unicode 18.0, capítulo 23: Special Areas and Format Characters, y la Unicode Character Database.
Tajalli, Shamsfard y Kalantari, «Developing an Informal-Formal Persian Corpus: Highlighting the Differences between Two Writing Styles», AbjadNLP 2025.
Anthropic, «Introducing Contextual Retrieval», 19 de septiembre de 2024.