Files
aivanov_CIM/tests/test_vectorization.py
2026-03-05 01:20:14 +01:00

486 lines
17 KiB
Python

"""
Tests pour la vectorisation et l'indexation des référentiels.
Ce module teste:
- Le chargement du modèle d'embeddings
- La vectorisation des chunks
- La construction de l'index HNSW avec FAISS
- La vectorisation des index alphabétiques
- La génération de hash d'index pour versionnement
"""
import json
import tempfile
from pathlib import Path
import faiss
import numpy as np
import pytest
from pipeline_mco_pmsi.rag.referentiels_manager import Chunk, ReferentielsManager, VectorIndex
class TestEmbeddingsModel:
"""Tests pour le chargement du modèle d'embeddings."""
def test_load_embeddings_model_success(self):
"""Test que le modèle d'embeddings se charge correctement."""
manager = ReferentielsManager()
model = manager._load_embeddings_model()
assert model is not None
# Vérifier que le modèle peut encoder du texte
embedding = model.encode("Test médical", convert_to_numpy=True)
assert embedding is not None
assert len(embedding.shape) == 1 # Vecteur 1D
assert embedding.shape[0] > 0 # Dimension > 0
def test_embeddings_model_produces_consistent_vectors(self):
"""Test que le modèle produit des vecteurs cohérents pour le même texte."""
manager = ReferentielsManager()
model = manager._load_embeddings_model()
text = "Gastrite aiguë avec hémorragie"
embedding1 = model.encode(text, convert_to_numpy=True, normalize_embeddings=True)
embedding2 = model.encode(text, convert_to_numpy=True, normalize_embeddings=True)
# Les embeddings doivent être identiques (ou très proches)
np.testing.assert_array_almost_equal(embedding1, embedding2, decimal=5)
def test_embeddings_model_normalized(self):
"""Test que les embeddings sont normalisés (L2 norm = 1)."""
manager = ReferentielsManager()
model = manager._load_embeddings_model()
text = "Diagnostic principal"
embedding = model.encode(text, convert_to_numpy=True, normalize_embeddings=True)
# Calculer la norme L2
norm = np.linalg.norm(embedding)
# La norme doit être proche de 1 (normalisé)
assert abs(norm - 1.0) < 0.01
class TestVectorization:
"""Tests pour la vectorisation des chunks."""
def test_vectorize_single_chunk(self):
"""Test la vectorisation d'un seul chunk."""
manager = ReferentielsManager()
chunk = Chunk(
chunk_id="test_001",
referentiel_type="cim10",
referentiel_version="2026",
content="K29.7 Gastrite, sans précision",
metadata={"chunk_type": "code_block"},
chunk_index=0,
)
# Vectoriser le chunk
model = manager._load_embeddings_model()
vector = model.encode(chunk.content, convert_to_numpy=True, normalize_embeddings=True)
assert vector is not None
assert vector.shape[0] > 0
assert isinstance(vector, np.ndarray)
def test_vectorize_multiple_chunks(self):
"""Test la vectorisation de plusieurs chunks."""
manager = ReferentielsManager()
chunks = [
Chunk(
chunk_id=f"test_{i:03d}",
referentiel_type="cim10",
referentiel_version="2026",
content=f"Code test {i}",
metadata={"chunk_type": "code_block"},
chunk_index=i,
)
for i in range(10)
]
model = manager._load_embeddings_model()
vectors = [
model.encode(chunk.content, convert_to_numpy=True, normalize_embeddings=True)
for chunk in chunks
]
assert len(vectors) == 10
# Tous les vecteurs doivent avoir la même dimension
dimensions = [v.shape[0] for v in vectors]
assert len(set(dimensions)) == 1 # Toutes les dimensions sont identiques
class TestBuildIndex:
"""Tests pour la construction de l'index HNSW."""
def test_build_index_success(self, tmp_path):
"""Test la construction réussie d'un index HNSW."""
manager = ReferentielsManager(data_dir=tmp_path)
# Créer des chunks de test
chunks = [
Chunk(
chunk_id=f"cim10_2026_{i}",
referentiel_type="cim10",
referentiel_version="2026",
content=f"K29.{i} Gastrite type {i}",
metadata={"chunk_type": "code_block"},
chunk_index=i,
)
for i in range(20)
]
# Construire l'index
vector_index = manager.build_index(chunks)
# Vérifications
assert isinstance(vector_index, VectorIndex)
assert vector_index.index_hash is not None
assert len(vector_index.index_hash) == 64 # SHA-256 hex
assert vector_index.dimension > 0
assert vector_index.num_vectors == 20
assert vector_index.index_type == "HNSW"
assert vector_index.created_at is not None
def test_build_index_saves_to_disk(self, tmp_path):
"""Test que l'index est sauvegardé sur disque."""
manager = ReferentielsManager(data_dir=tmp_path)
chunks = [
Chunk(
chunk_id=f"cim10_2026_{i}",
referentiel_type="cim10",
referentiel_version="2026",
content=f"Code {i}",
metadata={},
chunk_index=i,
)
for i in range(10)
]
manager.build_index(chunks)
# Vérifier que les fichiers sont créés
index_file = tmp_path / "cim10_2026_index.faiss"
chunks_file = tmp_path / "cim10_2026_chunks.json"
assert index_file.exists()
assert chunks_file.exists()
# Vérifier que l'index peut être rechargé
loaded_index = faiss.read_index(str(index_file))
assert loaded_index.ntotal == 10
def test_build_index_chunks_json_valid(self, tmp_path):
"""Test que le fichier JSON des chunks est valide."""
manager = ReferentielsManager(data_dir=tmp_path)
chunks = [
Chunk(
chunk_id=f"ccam_2025_{i}",
referentiel_type="ccam",
referentiel_version="2025",
content=f"YYYY00{i} Acte {i}",
metadata={"section": "Section A"},
chunk_index=i,
)
for i in range(5)
]
manager.build_index(chunks)
# Charger et vérifier le JSON
chunks_file = tmp_path / "ccam_2025_chunks.json"
with open(chunks_file, "r", encoding="utf-8") as f:
loaded_chunks = json.load(f)
assert len(loaded_chunks) == 5
assert loaded_chunks[0]["chunk_id"] == "ccam_2025_0"
assert loaded_chunks[0]["referentiel_type"] == "ccam"
assert loaded_chunks[0]["content"] == "YYYY000 Acte 0"
def test_build_index_empty_chunks_raises_error(self):
"""Test qu'une liste vide de chunks lève une erreur."""
manager = ReferentielsManager()
with pytest.raises(ValueError, match="ne peut pas être vide"):
manager.build_index([])
def test_build_index_hash_consistency(self, tmp_path):
"""Test que le hash de l'index est cohérent."""
manager = ReferentielsManager(data_dir=tmp_path)
chunks = [
Chunk(
chunk_id=f"test_{i}",
referentiel_type="cim10",
referentiel_version="2026",
content=f"Content {i}",
metadata={},
chunk_index=i,
)
for i in range(10)
]
# Construire l'index deux fois
index1 = manager.build_index(chunks)
# Le hash devrait être le même (même paramètres, même nombre de vecteurs)
# Note: En pratique, le hash dépend des paramètres, pas du contenu exact
assert len(index1.index_hash) == 64
class TestAlphabeticalIndexVectorization:
"""Tests pour la vectorisation des index alphabétiques."""
def test_vectorize_alphabetical_index_cim10(self):
"""Test la vectorisation d'un index alphabétique CIM-10."""
manager = ReferentielsManager()
# Simuler un index alphabétique CIM-10
alpha_text = """A
Abcès - voir A00.1
Abdomen aigu - voir R10.0
B
Bronchite - voir J20.9
Bronchopneumonie - voir J18.0
C
Cataracte - voir H26.9
Cholécystite - voir K81.9
"""
chunks = manager.vectorize_alphabetical_indexes(
alpha_text, "cim10", "2026"
)
assert len(chunks) > 0
# Vérifier que les chunks ont le bon type
for chunk in chunks:
assert chunk.referentiel_type == "cim10"
assert chunk.referentiel_version == "2026"
assert chunk.metadata["chunk_type"] == "alphabetical_index"
assert "letter" in chunk.metadata
def test_vectorize_alphabetical_index_ccam(self):
"""Test la vectorisation d'un index alphabétique CCAM."""
manager = ReferentielsManager()
# Simuler un index alphabétique CCAM
alpha_text = """A
Ablation - YYYY001
Amputation - YYYY002
B
Biopsie - ZZZZ001
Bronchoscopie - ZZZZ002
"""
chunks = manager.vectorize_alphabetical_indexes(
alpha_text, "ccam", "2025"
)
assert len(chunks) > 0
for chunk in chunks:
assert chunk.referentiel_type == "ccam"
assert chunk.referentiel_version == "2025"
assert chunk.metadata["chunk_type"] == "alphabetical_index"
def test_alphabetical_index_extracts_codes(self):
"""Test que les codes sont extraits dans les métadonnées."""
manager = ReferentielsManager()
alpha_text = """Gastrite - voir K29.7
Gastro-entérite - voir A09
Glaucome - voir H40.9
"""
chunks = manager.vectorize_alphabetical_indexes(
alpha_text, "cim10", "2026"
)
# Au moins un chunk devrait contenir des codes dans les métadonnées
codes_found = False
for chunk in chunks:
if chunk.metadata.get("codes"):
codes_found = True
# Vérifier que les codes sont au format CIM-10
codes = chunk.metadata["codes"].split(",")
for code in codes:
assert len(code) >= 3 # Au moins A00
assert codes_found
def test_alphabetical_index_chunk_size(self):
"""Test que les chunks d'index alphabétique respectent la taille cible."""
manager = ReferentielsManager()
# Créer un long index alphabétique
entries = []
for i in range(100):
entries.append(f"Terme médical {i} - voir K{i:02d}.{i % 10}")
alpha_text = "\n".join(entries)
chunks = manager.vectorize_alphabetical_indexes(
alpha_text, "cim10", "2026"
)
# Vérifier que les chunks ne sont pas trop grands
for chunk in chunks:
assert len(chunk.content) <= 2048 # max_chunk_size
class TestIntegrationVectorizationIndexation:
"""Tests d'intégration pour le workflow complet."""
def test_full_workflow_import_chunk_vectorize_index(self, tmp_path):
"""Test le workflow complet: import → chunk → vectorize → index."""
# Créer un fichier PDF de test
pdf_path = tmp_path / "test_ref.pdf"
# Pour ce test, on va créer un fichier texte au lieu d'un PDF
# car créer un PDF valide est complexe
text_content = """CHAPITRE I - Maladies infectieuses
A00-A09 Maladies intestinales infectieuses
A00 Choléra
A00.0 Choléra dû à Vibrio cholerae 01, biovar cholerae
A00.1 Choléra dû à Vibrio cholerae 01, biovar El Tor
A00.9 Choléra, sans précision
Inclus: infection à Vibrio cholerae
Exclut: intoxication alimentaire (A05.-)
"""
# Sauvegarder le texte directement (simuler l'extraction PDF)
manager = ReferentielsManager(data_dir=tmp_path)
text_file = tmp_path / "cim10_2026_text.txt"
with open(text_file, "w", encoding="utf-8") as f:
f.write(text_content)
# Créer une version de référentiel manuellement
from pipeline_mco_pmsi.models.metadata import ReferentielVersion
from datetime import datetime
import hashlib
ref_version = ReferentielVersion(
type="cim10",
version="2026",
import_date=datetime.now(),
file_hash=hashlib.sha256(text_content.encode()).hexdigest(),
chunk_count=0,
index_hash="0" * 64,
)
# Chunker
chunks = manager.chunk_referentiel(ref_version)
assert len(chunks) > 0
# Construire l'index
vector_index = manager.build_index(chunks)
assert vector_index.num_vectors == len(chunks)
# Vérifier que les fichiers sont créés
assert (tmp_path / "cim10_2026_index.faiss").exists()
assert (tmp_path / "cim10_2026_chunks.json").exists()
class TestIndexSearch:
"""Tests pour la recherche dans l'index vectoriel."""
def test_index_search_basic(self, tmp_path):
"""Test une recherche basique dans l'index."""
manager = ReferentielsManager(data_dir=tmp_path)
# Créer des chunks avec du contenu médical
chunks = [
Chunk(
chunk_id=f"cim10_2026_{i}",
referentiel_type="cim10",
referentiel_version="2026",
content=content,
metadata={},
chunk_index=i,
)
for i, content in enumerate([
"K29.7 Gastrite, sans précision",
"K29.0 Gastrite hémorragique aiguë",
"J18.0 Bronchopneumonie, sans précision",
"I10 Hypertension essentielle",
"E11 Diabète sucré non insulino-dépendant",
])
]
# Construire l'index
manager.build_index(chunks)
# Charger l'index
index_file = tmp_path / "cim10_2026_index.faiss"
index = faiss.read_index(str(index_file))
# Effectuer une recherche
model = manager._load_embeddings_model()
query = "gastrite"
query_vector = model.encode(query, convert_to_numpy=True, normalize_embeddings=True)
query_vector = query_vector.reshape(1, -1).astype(np.float32)
# Rechercher les 2 plus proches voisins
distances, indices = index.search(query_vector, 2)
# Les résultats devraient inclure les chunks sur la gastrite
assert len(indices[0]) == 2
# Les indices devraient être 0 ou 1 (les deux chunks sur la gastrite)
assert indices[0][0] in [0, 1]
assert indices[0][1] in [0, 1]
def test_index_search_similarity_scores(self, tmp_path):
"""Test que les scores de similarité sont cohérents."""
manager = ReferentielsManager(data_dir=tmp_path)
chunks = [
Chunk(
chunk_id=f"test_{i}",
referentiel_type="cim10",
referentiel_version="2026",
content=content,
metadata={},
chunk_index=i,
)
for i, content in enumerate([
"Gastrite aiguë avec hémorragie",
"Gastrite chronique",
"Pneumonie bactérienne",
])
]
manager.build_index(chunks)
# Charger l'index
index_file = tmp_path / "cim10_2026_index.faiss"
index = faiss.read_index(str(index_file))
# Rechercher avec une requête proche du premier chunk
model = manager._load_embeddings_model()
query = "gastrite hémorragique"
query_vector = model.encode(query, convert_to_numpy=True, normalize_embeddings=True)
query_vector = query_vector.reshape(1, -1).astype(np.float32)
distances, indices = index.search(query_vector, 3)
# Le premier résultat devrait être le plus proche (distance la plus petite)
# Avec des embeddings normalisés, la distance est 1 - cosine_similarity
# Donc une distance plus petite = plus similaire
assert distances[0][0] <= distances[0][1]
assert distances[0][1] <= distances[0][2]
if __name__ == "__main__":
pytest.main([__file__, "-v"])