Issue #185 - Analyse du problème de matching¶
Problème identifié¶
Symptômes (Émission 09/03/2025)¶
- 9 livres dans MongoDB
- 10 avis extraits (1 de trop)
- "La Chaises" non matché (
livre_oid=None) - Badge 🟡 (unmatched)
Cause racine¶
Approche actuelle : Matching avis par avis séquentiel (24 avis traités un par un)
# ACTUEL: Traitement séquentiel des avis
for avis in avis_list: # 24 avis
livre_matched = match_book(avis, all_books)
if livre_matched:
avis["livre_oid"] = livre_matched["_id"]
# ⚠️ Aucun mécanisme pour "réserver" le livre
Conséquence : Un livre peut être matché plusieurs fois par des avis différents
Scénario réel reproduit¶
Avis #12 : "Trésors Cachés" (2ème avis de 4) - Titre extrait : "Trésors Cachés" (pluriel) - Auteur extrait : "Pascal Quignard" - Phase 3 (accent-insensitive regex) : Match "La chaise" ❌ - Regex tolère le "s" final - Aucune validation de cohérence auteur/éditeur en Phase 3 - "La chaise" devient "claimed"
Avis #23 : "La Chaises" (coup de cœur)
- Titre extrait : "La Chaises" (avec S)
- Auteur extrait : "Jean-Louis Aislin" (faute de frappe)
- Résultat : livre_oid=None
- "La chaise" déjà matché par avis #12
- Phase 1-3 échouent (titre + auteur différents)
- Phase 4 (fuzzy) pas encore implémentée
Tests documentant le problème¶
Test complet : 24 avis réels¶
Fichier : tests/test_avis_extraction_emission_20250309.py::test_should_match_all_24_avis_like_real_extraction
Résultat :
'Trésors Cachés': 4 avis
- 3 matchent "Trésor caché" (68e9192d066cb40c25d5d2d9) ✅
- 1 matche "La chaise" (68e919cd066cb40c25d5d2e3) ❌
'La Chaises': 1 avis
- livre_oid=None ❌
Solutions proposées¶
Solution 1 : Fix rapide - Validation cohérence en Phase 3¶
Principe : Ajouter validation auteur/éditeur en Phase 3
def _match_phase3_accent_insensitive(avis, books):
"""Phase 3 avec validation de cohérence."""
for book in books:
if title_matches_accent_insensitive(avis["titre"], book["titre"]):
# ✅ NOUVEAU: Valider cohérence auteur OU éditeur
if (
author_similar(avis["auteur"], book["auteur_nom"])
or avis["editeur"] == book["editeur"]
):
return book
return None
Avantages : - Fix rapide (1 fonction à modifier) - Réduit les faux matches de Phase 3
Inconvénients : - Ne résout pas le problème fondamental (matching avis par avis) - Complexifie la logique de Phase 3
Solution 2 : Refactoring - Matching livre par livre¶
Principe : Grouper les avis par livre avant le matching
def resolve_entities_v2(avis_list, books, critiques):
"""Nouvelle approche: matching livre par livre."""
# 1. Grouper les avis par (titre, auteur)
grouped_avis = {}
for avis in avis_list:
key = (avis["livre_titre_extrait"], avis["auteur_nom_extrait"])
if key not in grouped_avis:
grouped_avis[key] = []
grouped_avis[key].append(avis)
# 2. Matcher chaque groupe de livres (pas chaque avis)
livre_matches = {} # (titre, auteur) → livre_oid
for (titre, auteur), avis_group in grouped_avis.items():
# Créer un avis "représentatif" du groupe
repr_avis = {
"livre_titre_extrait": titre,
"auteur_nom_extrait": auteur,
"editeur_extrait": avis_group[0]["editeur_extrait"],
}
# Matcher UNE SEULE FOIS
matched_book = _match_book_multi_phase(repr_avis, books)
if matched_book:
livre_matches[(titre, auteur)] = str(matched_book["_id"])
# 3. Propager le match à tous les avis du groupe
resolved_avis = []
for avis in avis_list:
key = (avis["livre_titre_extrait"], avis["auteur_nom_extrait"])
avis["livre_oid"] = livre_matches.get(key)
# Matcher critique (indépendant du livre)
avis["critique_oid"] = _match_critique(avis, critiques)
resolved_avis.append(avis)
return resolved_avis
Avantages : - ✅ Un livre ne peut être matché qu'une seule fois - ✅ Matching plus rapide (9 matchings au lieu de 24) - ✅ Plus logique métier (on matche des livres, pas des avis) - ✅ Simplifie la détection de problèmes
Inconvénients : - Refactoring plus important - Tests existants à adapter
Solution 3 : Hybrid - Track claimed books¶
Principe : Garder l'approche actuelle + tracking des livres déjà matchés
def resolve_entities_v1_fixed(avis_list, books, critiques):
"""Approche actuelle avec tracking."""
claimed_books = set() # Livres déjà matchés
for avis in avis_list:
# Filtrer les livres déjà "claimed"
available_books = [b for b in books if str(b["_id"]) not in claimed_books]
matched_book = _match_book_multi_phase(avis, available_books)
if matched_book:
livre_oid = str(matched_book["_id"])
avis["livre_oid"] = livre_oid
claimed_books.add(livre_oid) # Marquer comme claimed
else:
avis["livre_oid"] = None
return avis_list
Avantages : - Fix simple (ajout d'un set) - Garde l'approche actuelle
Inconvénients : - Ne résout pas le problème si plusieurs avis du même livre sont dispersés - Dépend de l'ordre de traitement
Recommandation¶
Solution 2 (Matching livre par livre) est la plus robuste et logique métier.
Plan d'implémentation :
1. Créer resolve_entities_v2() en parallèle de l'existant
2. Adapter les tests pour vérifier les deux versions
3. Valider que tous les tests passent avec v2
4. Remplacer v1 par v2
5. Nettoyer le code
Impact sur les tests¶
Tests à garder¶
test_should_extract_all_avis_from_real_summary_section1_and_section2✅test_should_match_all_24_avis_like_real_extraction✅ (documente le problème)
Tests à adapter après refactoring¶
test_should_resolve_all_9_books_from_real_emission(9 avis → OK, mais logique changera)- Tests unitaires de matching (phases 1-4)
Prochaines étapes¶
- ✅ Documenter le problème (ce fichier)
- ⬜ Implémenter Solution 2 (
resolve_entities_v2) - ⬜ Vérifier tous les tests passent
- ⬜ Remplacer l'ancienne version
- ⬜ Vérifier l'émission 09/03/2025 devient 🟢