"""Regresión de la lógica de recuperación híbrida + reranking (Tier 2). Prueba las piezas puras (RRF, reranking, fallback híbrido) con dobles de prueba; la integración real con LanceDB/cross-encoder se valida tras `make ingest`. """ from __future__ import annotations from app.rag import retriever as R def _fila(libro, pagina, texto): return {"libro": libro, "pagina": pagina, "texto": texto, "especie": ""} def test_rrf_prioriza_lo_alto_en_ambas_listas(): a = _fila("L", "1", "anemia regenerativa reticulocitosis") b = _fila("L", "2", "colestasis hepatica") c = _fila("L", "3", "azotemia renal") densa = [a, b, c] lexica = [b, a, c] # b y a arriba en ambas fus = R.fusion_rrf([densa, lexica], n=3) # 'a' o 'b' (altos en ambas) deben ir por delante de 'c' assert R._clave_fila(fus[-1]) == R._clave_fila(c) def test_rrf_deduplica_por_clave(): a = _fila("L", "1", "texto uno") fus = R.fusion_rrf([[a], [a]], n=5) assert len(fus) == 1 def test_rrf_respeta_n(): filas = [_fila("L", str(i), f"t{i}") for i in range(10)] assert len(R.fusion_rrf([filas], n=4)) == 4 def test_reordenar_sin_reranker_conserva_orden(monkeypatch): monkeypatch.setattr(R, "_cargar_reranker", lambda: None) filas = [_fila("L", str(i), f"t{i}") for i in range(5)] assert R._reordenar("consulta", filas, k=3) == filas[:3] def test_reordenar_con_reranker_ordena_por_score(monkeypatch): # Stub: puntúa por la posición del dígito en el texto (mayor = más relevante). class StubCE: def predict(self, pares): return [float(t.split("t")[-1]) for _, t in pares] monkeypatch.setattr(R, "_cargar_reranker", lambda: StubCE()) filas = [_fila("L", str(i), f"t{i}") for i in range(5)] # t0..t4 top = R._reordenar("consulta", filas, k=2) assert [f["texto"] for f in top] == ["t4", "t3"] def test_diversidad_prefiere_no_repetir_libro(): filas = [_fila("A", "1", "t1"), _fila("A", "2", "t2"), _fila("A", "3", "t3"), _fila("B", "1", "t4")] top = R._aplicar_diversidad(filas, k=3, max_por_libro=2) assert [f["libro"] for f in top] == ["A", "A", "B"] def test_diversidad_es_preferencia_no_limite_duro(): """Si no hay material de otras fuentes, se rellena igual: mejor 3 del mismo libro que 2.""" filas = [_fila("A", str(i), f"t{i}") for i in range(4)] assert len(R._aplicar_diversidad(filas, k=3, max_por_libro=2)) == 3 def test_suelo_de_relevancia_descarta_los_flojos(): fuerte, flojo = _fila("A", "1", "t1"), _fila("A", "2", "t2") fuerte["_rerank_score"], flojo["_rerank_score"] = 2.0, -3.0 assert R._filtrar_por_score([fuerte, flojo], 0.0) == [fuerte] def test_suelo_de_relevancia_no_toca_lo_que_no_pasó_por_el_reranker(): """RRF y distancia densa están en otra escala: aplicarles el umbral sería mezclar métricas.""" filas = [_fila("A", "1", "t1")] filas[0]["_rrf_score"] = 0.016 assert R._filtrar_por_score(filas, 0.5) == filas def test_suelo_desactivado_por_defecto_no_filtra(): filas = [_fila("A", "1", "t1")] filas[0]["_rerank_score"] = -9.0 assert R._filtrar_por_score(filas, None) == filas def test_candidatos_sin_fts_cae_a_vectorial(monkeypatch): class Cfg: rag_hibrido = True densa = [_fila("L", "1", "densa")] monkeypatch.setattr(R, "_buscar_vectorial", lambda *a, **k: densa) monkeypatch.setattr(R, "_buscar_lexico", lambda *a, **k: []) # sin FTS assert R._recuperar_candidatos(Cfg(), None, None, "q", 10) == densa