Update index.py
This commit is contained in:
parent
3f2f13b65f
commit
89d167a2f8
98
index.py
98
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
|
||||
|
||||
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")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user