""" Explore ChromaDB store ====================== Lecture seule (aucun appel Azure). Donne un état complet de la base : - total de chunks, nombre de mémoires distincts - répartition par catégorie - chunks par mémoire (min / max / moyenne) - DOUBLONS : (source, chunk_index) répétés = fichier ingéré plusieurs fois - DOUBLONS de texte exact (chunks identiques) - mémoires manquants vs train_data/ Usage : ./.venv/Scripts/python.exe explore_store.py """ import os import hashlib from pathlib import Path from collections import Counter, defaultdict os.environ.setdefault("ANONYMIZED_TELEMETRY", "False") import chromadb from chromadb.config import Settings PROJECT_ROOT = Path(__file__).parent DATA_DIR = Path("/data") if Path("/data").is_dir() else PROJECT_ROOT / "data" TRAIN_DATA_DIR = Path("/train_data") if Path("/train_data").is_dir() else PROJECT_ROOT / "train_data" COLLECTION_NAME = "rag_documents" client = chromadb.PersistentClient(path=str(DATA_DIR / "chroma_db"), settings=Settings(anonymized_telemetry=False)) col = client.get_or_create_collection(name=COLLECTION_NAME, metadata={"hnsw:space": "cosine"}) total = col.count() print("=" * 70) print(f"TOTAL CHUNKS EN BASE : {total}") print("=" * 70) per_source = Counter() # chunks par mémoire src_idx = Counter() # (source, chunk_index) -> nb d'occurrences text_hashes = Counter() # hash texte -> nb d'occurrences id_counter = Counter() # ids -> détecte doublons d'id empty_texts = 0 short_texts = 0 hash_to_example = {} off, step = 0, 2000 while off < total: got = col.get(include=["metadatas", "documents"], limit=step, offset=off) ids = got["ids"] metas = got["metadatas"] docs = got["documents"] for _id, m, d in zip(ids, metas, docs): id_counter[_id] += 1 src = m.get("source", "??") idx = m.get("chunk_index", -1) per_source[src] += 1 src_idx[(src, idx)] += 1 d = d or "" if not d.strip(): empty_texts += 1 elif len(d.strip()) < 30: short_texts += 1 h = hashlib.md5(d.encode("utf-8", errors="ignore")).hexdigest() text_hashes[h] += 1 if h not in hash_to_example: hash_to_example[h] = (src, d[:80].replace("\n", " ")) off += step print(f" ...lu {min(off, total)}/{total}") print() print(f"Mémoires distincts (sources) : {len(per_source)}") print(f"IDs en double : {sum(1 for v in id_counter.values() if v > 1)}") print() # --- DOUBLONS (source, chunk_index) -> fichier ingéré plusieurs fois --- dup_pairs = {k: v for k, v in src_idx.items() if v > 1} dup_sources = sorted({src for (src, _idx) in dup_pairs}) print("-" * 70) print(f"DOUBLONS (source, chunk_index) répétés : {len(dup_pairs)} paires") print(f" -> mémoires concernés (ingérés >1 fois) : {len(dup_sources)}") for s in dup_sources[:30]: occ = max(v for (src, _i), v in src_idx.items() if src == s) print(f" x{occ} {s} ({per_source[s]} chunks au total)") print() # --- DOUBLONS de texte exact --- dup_text = {h: v for h, v in text_hashes.items() if v > 1} nb_extra = sum(v - 1 for v in dup_text.values()) print("-" * 70) print(f"Textes EXACTEMENT identiques : {len(dup_text)} contenus dupliqués " f"= {nb_extra} chunks redondants") for h, v in sorted(dup_text.items(), key=lambda x: -x[1])[:10]: src, ex = hash_to_example[h] print(f" x{v} [{src}] «{ex}…»") print() print(f"Chunks vides : {empty_texts}") print(f"Chunks < 30 car: {short_texts}") print() # --- Répartition par catégorie --- cat = Counter() cat_docs = defaultdict(set) for src, n in per_source.items(): c = src.split("\\")[0].split("/")[0] cat[c] += n cat_docs[c].add(src) print("-" * 70) print("RÉPARTITION PAR CATÉGORIE :") for c, n in cat.most_common(): print(f" {c:<22} {len(cat_docs[c]):>3} mémoires | {n:>7} chunks") print() # --- chunks par mémoire --- counts = sorted(per_source.values()) if counts: print("-" * 70) print(f"Chunks par mémoire : min={counts[0]} | max={counts[-1]} | " f"moyenne={sum(counts)//len(counts)}") # --- mémoires manquants vs disque --- if TRAIN_DATA_DIR.exists(): allfiles = set(str(p.relative_to(TRAIN_DATA_DIR)) for p in TRAIN_DATA_DIR.rglob("*.pdf")) missing = sorted(allfiles - set(per_source.keys())) print() print("-" * 70) print(f"PDF sur disque : {len(allfiles)} | en base : {len(per_source)} | MANQUANTS : {len(missing)}") for m in missing[:50]: print(f" - {m}")