Update index.py
This commit is contained in:
parent
3f2f13b65f
commit
89d167a2f8
100
index.py
100
index.py
@ -1,32 +1,43 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Indexation du répertoire Fiches avec BGE‑M3 (FlagEmbedding) + FAISS.
|
Indexation incrémentale des 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.
|
• 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
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
import faiss
|
import faiss
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from FlagEmbedding import BGEM3FlagModel
|
from FlagEmbedding import BGEM3FlagModel
|
||||||
|
|
||||||
# --- Paramètres -------------------------------------------------------------
|
# --- Paramètres -------------------------------------------------------------
|
||||||
# Si vous exécutez ce script SUR L’HÔTE, mettez le chemin local
|
ROOT = Path("Fiches") # répertoire local des fiches
|
||||||
ROOT = Path("Fiches") # ou Path("/home/fabnum/fabnum-dev/Fiches") # dossier monté contenant les fiches
|
MODEL_NAME = "BAAI/bge-m3" # embedder multilingue, MIT
|
||||||
MODEL_NAME = "BAAI/bge-m3" # embedding multilingue, licence MIT
|
CHUNK = 800 # taille cible d'un bloc (≈600 mots)
|
||||||
CHUNK = 800 # taille cible (≈600 mots)
|
OVERLAP = 100 # chevauchement pour la cohérence
|
||||||
OVERLAP = 100 # chevauchement pour la cohésion
|
|
||||||
INDEX_FILE = "corpus.idx"
|
INDEX_FILE = "corpus.idx"
|
||||||
META_FILE = "corpus.meta.json"
|
META_FILE = "corpus.meta.json"
|
||||||
EXTENSIONS = ["*.md", "*.MD", "*.markdown", "*.txt"]
|
EXTENSIONS = ["*.md", "*.MD", "*.markdown", "*.txt"]
|
||||||
|
BATCH = 128 # plus grand batch : encode plus vite
|
||||||
|
|
||||||
# --- Fonctions utilitaires --------------------------------------------------
|
# --- Fonctions utilitaires --------------------------------------------------
|
||||||
|
|
||||||
def split(text: str, chunk_size: int = CHUNK, overlap: int = OVERLAP):
|
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."""
|
"""Découpe un texte en morceaux de chunk_size mots avec overlap mots."""
|
||||||
sentences = re.split(r"(?<=[\.!?])\s+", text)
|
sentences = re.split(r"(?<=[\.\!\?])\s+", text)
|
||||||
chunks, buf = [], []
|
chunks, buf = [], []
|
||||||
for s in sentences:
|
for s in sentences:
|
||||||
buf.append(s)
|
buf.append(s)
|
||||||
@ -37,45 +48,76 @@ def split(text: str, chunk_size: int = CHUNK, overlap: int = OVERLAP):
|
|||||||
chunks.append(" ".join(buf))
|
chunks.append(" ".join(buf))
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
# --- Pipeline principal -----------------------------------------------------
|
|
||||||
|
|
||||||
def gather_files(root: Path):
|
def gather_files(root: Path):
|
||||||
for pattern in EXTENSIONS:
|
for pattern in EXTENSIONS:
|
||||||
yield from root.rglob(pattern)
|
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():
|
def main():
|
||||||
docs, meta = [], []
|
index, meta, existing = load_existing()
|
||||||
files_count = 0
|
|
||||||
|
# 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):
|
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")
|
text = fp.read_text(encoding="utf-8", errors="ignore")
|
||||||
for i, chunk in enumerate(split(text)):
|
for i, chunk in enumerate(split(text)):
|
||||||
docs.append(chunk)
|
new_docs.append(chunk)
|
||||||
meta.append({"file": fp.relative_to(ROOT).as_posix(), "part": i})
|
new_meta.append({"file": rel, "part": i, "mtime": mtime})
|
||||||
|
|
||||||
if not docs:
|
if not new_docs:
|
||||||
raise SystemExit("Aucun fichier trouvé dans /app/Fiches. Vérifiez le montage ou les extensions.")
|
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")
|
model = BGEM3FlagModel(MODEL_NAME, device="cpu")
|
||||||
emb = model.encode(docs, batch_size=64) # pas de normalisation interne
|
emb = model.encode(new_docs, batch_size=BATCH, return_dict=False)
|
||||||
|
|
||||||
# Normalisation manuelle (cosine)
|
|
||||||
emb = emb.astype("float32")
|
emb = emb.astype("float32")
|
||||||
norms = np.linalg.norm(emb, axis=1, keepdims=True)
|
emb /= np.linalg.norm(emb, axis=1, keepdims=True) + 1e-12
|
||||||
emb = emb / np.maximum(norms, 1e-12)
|
|
||||||
|
|
||||||
index = faiss.IndexFlatIP(emb.shape[1])
|
if index is None:
|
||||||
|
index = faiss.IndexFlatIP(emb.shape[1])
|
||||||
index.add(emb)
|
index.add(emb)
|
||||||
|
|
||||||
|
# Mettre à jour les métadonnées (anciens + nouveaux)
|
||||||
|
meta.extend(new_meta)
|
||||||
faiss.write_index(index, INDEX_FILE)
|
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:
|
print(
|
||||||
json.dump(meta, f, ensure_ascii=False, indent=2)
|
f"Index mis à jour : {len(meta)} vecteurs au total ("\
|
||||||
|
f"+{len(new_docs)}). Dernière maj : {datetime.now().isoformat(timespec='seconds')}"
|
||||||
print(f"Index écrit dans {INDEX_FILE} avec {len(docs)} vecteurs.")
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
t0 = time.time()
|
||||||
main()
|
main()
|
||||||
|
print(f"Terminé en {time.time() - t0:.1f} s")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user