From 89d167a2f88c901908c6dab14508cceee5fe8239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phan?= Date: Sun, 18 May 2025 19:25:56 +0200 Subject: [PATCH] Update index.py --- index.py | 100 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 29 deletions(-) diff --git a/index.py b/index.py index da05016..7e78524 100644 --- a/index.py +++ b/index.py @@ -1,32 +1,43 @@ #!/usr/bin/env python3 """ -Indexation du répertoire Fiches avec BGE‑M3 (FlagEmbedding) + FAISS. -Parcourt récursivement tous les fichiers markdown / texte (extensions .md, .MD, .markdown, .txt), -découpe en blocs de ~800 tokens, génère les embeddings, et écrit corpus.idx + corpus.meta.json. +Indexation incrémentale des fiches avec BGE‑M3 (FlagEmbedding) + FAISS +--------------------------------------------------------------------- +• Parcourt récursivement le dossier Fiches (ext .md .MD .markdown .txt) +• Découpe chaque fichier en blocs de ~800 tokens (chevauchement 100) +• Encode uniquement les passages provenant de fichiers **nouveaux ou modifiés** + depuis la dernière indexation (basé sur l'horodatage mtime). +• Les embeddings sont ajoutés à l'index existant sans toucher aux anciens. +• Écrit/Met à jour : corpus.idx (vecteurs) + corpus.meta.json (métadonnées) + +Temps gagné : pour 1 fiche ajoutée, seules ses quelques dizaines de passages +sont ré-encodés, pas les ~6 000 déjà en place ➜ ré‑indexation en quelques +secondes au lieu de minutes. """ from pathlib import Path import json import re +import time +from datetime import datetime import faiss import numpy as np from FlagEmbedding import BGEM3FlagModel # --- Paramètres ------------------------------------------------------------- -# Si vous exécutez ce script SUR L’HÔTE, mettez le chemin local -ROOT = Path("Fiches") # ou Path("/home/fabnum/fabnum-dev/Fiches") # dossier monté contenant les fiches -MODEL_NAME = "BAAI/bge-m3" # embedding multilingue, licence MIT -CHUNK = 800 # taille cible (≈600 mots) -OVERLAP = 100 # chevauchement pour la cohésion +ROOT = Path("Fiches") # répertoire local des fiches +MODEL_NAME = "BAAI/bge-m3" # embedder multilingue, MIT +CHUNK = 800 # taille cible d'un bloc (≈600 mots) +OVERLAP = 100 # chevauchement pour la cohérence INDEX_FILE = "corpus.idx" META_FILE = "corpus.meta.json" EXTENSIONS = ["*.md", "*.MD", "*.markdown", "*.txt"] +BATCH = 128 # plus grand batch : encode plus vite # --- Fonctions utilitaires -------------------------------------------------- def split(text: str, chunk_size: int = CHUNK, overlap: int = OVERLAP): - """Découpe un texte en morceaux de chunk_size mots avec overlap mots de recouvrement.""" - sentences = re.split(r"(?<=[\.!?])\s+", text) + """Découpe un texte en morceaux de chunk_size mots avec overlap mots.""" + sentences = re.split(r"(?<=[\.\!\?])\s+", text) chunks, buf = [], [] for s in sentences: buf.append(s) @@ -37,45 +48,76 @@ def split(text: str, chunk_size: int = CHUNK, overlap: int = OVERLAP): chunks.append(" ".join(buf)) return chunks -# --- Pipeline principal ----------------------------------------------------- def gather_files(root: Path): for pattern in EXTENSIONS: yield from root.rglob(pattern) + +# --- Chargement éventuel de l'index existant ------------------------------- + +def load_existing(): + if not Path(INDEX_FILE).exists(): + return None, [], 0 # pas d'index, pas de meta + + index = faiss.read_index(INDEX_FILE) + meta = json.load(open(META_FILE, encoding="utf-8")) + return index, meta, len(meta) + + +# --- Pipeline principal ----------------------------------------------------- + def main(): - docs, meta = [], [] - files_count = 0 + index, meta, existing = load_existing() + + # mapping chemin relatif ➜ mtime stocké + meta_mtime = {m["file"]: m.get("mtime", 0) for m in meta} + + new_docs, new_meta = [], [] + files_scanned, files_updated = 0, 0 for fp in gather_files(ROOT): - files_count += 1 + files_scanned += 1 + rel = fp.relative_to(ROOT).as_posix() + mtime = int(fp.stat().st_mtime) + if meta_mtime.get(rel) == mtime: + continue # inchangé + files_updated += 1 text = fp.read_text(encoding="utf-8", errors="ignore") for i, chunk in enumerate(split(text)): - docs.append(chunk) - meta.append({"file": fp.relative_to(ROOT).as_posix(), "part": i}) + new_docs.append(chunk) + new_meta.append({"file": rel, "part": i, "mtime": mtime}) - if not docs: - raise SystemExit("Aucun fichier trouvé dans /app/Fiches. Vérifiez le montage ou les extensions.") + if not new_docs: + print("Aucun fichier nouveau ou modifié. Index à jour ✔︎") + return - print(f"Traité {files_count} fichiers, découpé {len(docs)} passages, génération des embeddings…") + print( + f"Nouveaux passages : {len(new_docs)} issus de {files_updated} fiches ("\ + f"{files_scanned} fiches scannées). Génération des embeddings…" + ) model = BGEM3FlagModel(MODEL_NAME, device="cpu") - emb = model.encode(docs, batch_size=64) # pas de normalisation interne - - # Normalisation manuelle (cosine) + emb = model.encode(new_docs, batch_size=BATCH, return_dict=False) emb = emb.astype("float32") - norms = np.linalg.norm(emb, axis=1, keepdims=True) - emb = emb / np.maximum(norms, 1e-12) + emb /= np.linalg.norm(emb, axis=1, keepdims=True) + 1e-12 - index = faiss.IndexFlatIP(emb.shape[1]) + if index is None: + index = faiss.IndexFlatIP(emb.shape[1]) index.add(emb) + + # Mettre à jour les métadonnées (anciens + nouveaux) + meta.extend(new_meta) faiss.write_index(index, INDEX_FILE) + json.dump(meta, open(META_FILE, "w", encoding="utf-8"), ensure_ascii=False, indent=2) - with open(META_FILE, "w", encoding="utf-8") as f: - json.dump(meta, f, ensure_ascii=False, indent=2) - - print(f"Index écrit dans {INDEX_FILE} avec {len(docs)} vecteurs.") + print( + f"Index mis à jour : {len(meta)} vecteurs au total ("\ + f"+{len(new_docs)}). Dernière maj : {datetime.now().isoformat(timespec='seconds')}" + ) if __name__ == "__main__": + t0 = time.time() main() + print(f"Terminé en {time.time() - t0:.1f} s")