Update index.py

This commit is contained in:
Stéphan Peccini 2025-05-19 07:40:57 +02:00
parent 2c4931bdfe
commit e26fc3e20d

122
index.py
View File

@ -1,86 +1,111 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
index.py Indexation « minifiches » SANS découpage index.py indexation hybride des minifiches
==================================================== ===========================================
Objectif : chaque fichier (chapitre) devient **un seul** passage, afin de 1 fichier = 1 passage **si** le fichier WORD_LIMIT mots (par défaut : 600).
préserver lintégrité des tableaux, listes, etc. conformément à votre Audelà (rare : fiche ICS, ISG, etc.), on découpe en blocs ~CHUNK mots
organisation manuelle. avec chevauchement OVERLAP pour isoler les tableaux et valeurs numériques.
Incrémental : encode uniquement les fichiers nouveaux ou modifiés.
Caractéristiques :
Incrémental : seuls les fichiers nouveaux ou modifiés sont encodés.
Paramètres CLI :
--root racine des fiches (défaut : Corpus)
--index nom du fichier idx (défaut : corpus.idx)
--meta nom du fichier méta (défaut : corpus.meta.json)
Extensions prises : .md .markdown .txt
Embeddings : BGEM3 (FlagEmbedding) en CPU, normalisés L2. Embeddings : BGEM3 (FlagEmbedding) en CPU, normalisés L2.
Usage : Usage :
python index.py # première indexation (tous les fichiers) python index.py --root Corpus # première construction
python index.py # relance instantanée (rien à faire) python index.py # relance rapide (0 s si rien)
touch Corpus//nouveau.md
python index.py # encode seulement 1 fichier Arguments :
--root dossier des fiches (déf. Corpus)
--index nom du fichier FAISS (déf. corpus.idx)
--meta fichier méta JSON (déf. corpus.meta.json)
--word WORD_LIMIT (déf. 600)
--chunk CHUNK (déf. 350)
""" """
import argparse, json, os, time import argparse, json, re, sys
from pathlib import Path from pathlib import Path
import faiss, numpy as np import faiss, numpy as np
from FlagEmbedding import BGEM3FlagModel from FlagEmbedding import BGEM3FlagModel
from rich import print from rich import print
# --------------------- CLI -------------------------------------------------- # --------------------- CLI --------------------------------------------------
parser = argparse.ArgumentParser(description="Indexation incrémentale des minifiches (1 fichier = 1 passage).") parser = argparse.ArgumentParser(description="Indexation hybride : 1 passage par fiche courte, découpe douce pour les longues.")
parser.add_argument("--root", default="Corpus", help="Répertoire racine des fiches") parser.add_argument("--root", default="Corpus", help="Répertoire racine des fiches")
parser.add_argument("--index", default="corpus.idx", help="Nom du fichier FAISS") parser.add_argument("--index", default="corpus.idx", help="Nom du fichier FAISS")
parser.add_argument("--meta", default="corpus.meta.json", help="Nom du méta JSON") parser.add_argument("--meta", default="corpus.meta.json", help="Nom du méta JSON")
parser.add_argument("--word", type=int, default=600, help="WORD_LIMIT : audelà on découpe (mots)")
parser.add_argument("--chunk", type=int, default=350, help="Taille des chunks quand on découpe (mots)")
args = parser.parse_args() args = parser.parse_args()
ROOT = Path(args.root).expanduser() ROOT = Path(args.root)
INDEX_F = Path(args.index) INDEX_F = Path(args.index)
META_F = Path(args.meta) META_F = Path(args.meta)
WORD_LIMIT= args.word
CHUNK = args.chunk
OVERLAP = 50
EXTS = {".md", ".markdown", ".txt"} EXTS = {".md", ".markdown", ".txt"}
print(f"[dim]Racine : {ROOT} | Index : {INDEX_F}[/]") print(f"[dim]Racine : {ROOT} | Index : {INDEX_F}[/]")
# ------------------------ lire méta existant ------------------------------- # ---------------- split helper --------------------------------------------
old_meta = [] def split_long(text: str):
old_mtime = {} """Découpe douce : blocs ~CHUNK mots, préserve tableaux."""
sentences = re.split(r"(?<=[.!?])\s+", text)
chunks, buf = [], []
for s in sentences:
if "|" in s or re.fullmatch(r"\s*-{3,}\s*", s):
if buf:
chunks.append(" ".join(buf))
buf = []
chunks.append(s)
continue
buf.append(s)
if len(" ".join(buf).split()) >= CHUNK:
chunks.append(" ".join(buf))
buf = buf[-OVERLAP:]
if buf:
chunks.append(" ".join(buf))
return chunks
# ------------------------ lire méta existant ------------------------------
old_meta = {}
if INDEX_F.exists() and META_F.exists(): if INDEX_F.exists() and META_F.exists():
try: try:
old_meta = json.load(META_F.open()) for m in json.load(META_F.open()):
old_mtime = {m["path"]: m["mtime"] for m in old_meta} old_meta[m["path"]] = m
except Exception as e: except Exception as e:
print(f"[yellow]Avertissement : impossible de lire l'ancien méta : {e}. On repart de zéro.[/]") print(f"[yellow]Avertissement : méta illisible ({e}), reconstruction complète.[/]")
old_meta = [] old_meta = {}
old_mtime = {}
# ------------------------ scanner les fichiers ----------------------------- # ------------------------ scanner les fichiers ----------------------------
files = [fp for fp in ROOT.rglob("*") if fp.suffix.lower() in EXTS] files = [fp for fp in ROOT.rglob("*") if fp.suffix.lower() in EXTS]
files.sort() files.sort()
new_docs, new_meta = [], [] new_docs, new_meta, kept_meta = [], [], []
kept_meta = [] # meta non modifiés
for fp in files: for fp in files:
path_str = str(fp.relative_to(ROOT)) rel = str(fp.relative_to(ROOT))
mtime = int(fp.stat().st_mtime) mtime = int(fp.stat().st_mtime)
if path_str in old_mtime and old_mtime[path_str] == mtime: prev = old_meta.get(rel)
# déjà indexé, rien à faire if prev and prev["mtime"] == mtime:
kept_meta.append(next(m for m in old_meta if m["path"] == path_str)) kept_meta.append(prev)
continue continue
# fichier nouveau ou modifié
txt = fp.read_text(encoding="utf-8")
new_docs.append(txt)
new_meta.append({"path": path_str, "mtime": mtime})
print(f"Nouveaux/Modifiés : {len(new_docs)} | Conservés : {len(kept_meta)}") txt = fp.read_text(encoding="utf-8")
if not new_docs and INDEX_F.exists(): words = len(txt.split())
if words <= WORD_LIMIT:
new_docs.append(txt)
new_meta.append({"path": rel, "part": 0, "mtime": mtime})
else:
for i, chunk in enumerate(split_long(txt)):
new_docs.append(chunk)
new_meta.append({"path": rel, "part": i, "mtime": mtime})
print(f"Nouveaux/Modifiés : {len(new_meta)} | Conservés : {len(kept_meta)}")
if not new_meta and INDEX_F.exists():
print("Index déjà à jour ✔︎") print("Index déjà à jour ✔︎")
exit(0) sys.exit(0)
# ------------------------ embeddings BGEM3 --------------------------------- # ------------------------ embeddings --------------------------------------
model = BGEM3FlagModel("BAAI/bge-m3", device="cpu") model = BGEM3FlagModel("BAAI/bge-m3", device="cpu")
emb = model.encode(new_docs) emb = model.encode(new_docs)
if isinstance(emb, dict): if isinstance(emb, dict):
@ -88,7 +113,7 @@ if isinstance(emb, dict):
emb = emb / np.linalg.norm(emb, axis=1, keepdims=True) emb = emb / np.linalg.norm(emb, axis=1, keepdims=True)
emb = emb.astype("float32") emb = emb.astype("float32")
# ------------------------ mise à jour FAISS -------------------------------- # ------------------------ FAISS update ------------------------------------
if INDEX_F.exists(): if INDEX_F.exists():
idx = faiss.read_index(str(INDEX_F)) idx = faiss.read_index(str(INDEX_F))
else: else:
@ -97,8 +122,7 @@ else:
idx.add(emb) idx.add(emb)
faiss.write_index(idx, str(INDEX_F)) faiss.write_index(idx, str(INDEX_F))
# ------------------------ enregistrer le nouveau méta ---------------------- # ------------------------ save meta ---------------------------------------
all_meta = kept_meta + new_meta all_meta = kept_meta + new_meta
json.dump(all_meta, META_F.open("w"), ensure_ascii=False, indent=2) json.dump(all_meta, META_F.open("w"), ensure_ascii=False, indent=2)
print(f"Index mis à jour ✔︎ | Total passages : {idx.ntotal}")
print(f"Index mis à jour ✔︎ | Total passages : {idx.ntotal}")