QModel / scripts /update_fatwa_index.py
aelgendy's picture
QModel v8.5: accuracy release (code, 3,598-fatwa index, quran.com links)
c634c96
Raw History Blame Contribute Delete
4.6 kB
#!/usr/bin/env python3
"""
Replace the fatwa block of QModel.index + metadata.json without re-embedding Quran/Hadith.
Fatwas are the last block of both files (build_index.py writes quran + hadith + fatwa),
so this keeps every earlier vector, drops the old fatwa rows, and appends freshly
embedded fatwas. By default it loads the same fatwas as build_index.py (FATWA_DIRS
minus FATWA_EXCLUDE), so a fatwa-only update matches what a full rebuild produces.
Usage:
python scripts/update_fatwa_index.py # build_index.py's fatwa sources
python scripts/update_fatwa_index.py --in-place
python scripts/update_fatwa_index.py --fatwa-dir data/fatwas_scraped # override sources
Without --in-place, writes QModel.fatwa-update.index / metadata.fatwa-update.json next to
the originals; with it, replaces them and keeps .bak copies.
"""
import argparse
import json
import shutil
import sys
from pathlib import Path
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from build_index import ( # noqa: E402
DEFAULT_EMBED_MODEL,
EMBED_BATCH_SIZE,
FATWA_DIRS,
FATWA_EXCLUDE,
INDEX_PATH,
METADATA_PATH,
load_fatwa_entries,
passage_text,
)
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--fatwa-dir", action="append", type=Path,
help="Directory of fatwa JSON files (repeatable; default: build_index.FATWA_DIRS)")
parser.add_argument("--exclude-file", action="append", type=Path,
help="JSON file to skip (repeatable; default: build_index.FATWA_EXCLUDE)")
parser.add_argument("--model", default=DEFAULT_EMBED_MODEL)
parser.add_argument("--in-place", action="store_true", help="Overwrite QModel.index / metadata.json (keeps .bak)")
args = parser.parse_args()
print(f"Loading {INDEX_PATH.name} and {METADATA_PATH.name} …")
index = faiss.read_index(str(INDEX_PATH))
metadata = json.loads(METADATA_PATH.read_text(encoding="utf-8"))
first_fatwa = next((i for i, d in enumerate(metadata) if d.get("type") == "fatwa"), len(metadata))
if any(d.get("type") == "fatwa" for d in metadata[:first_fatwa]) or \
any(d.get("type") != "fatwa" for d in metadata[first_fatwa:]):
sys.exit("Fatwas are not a contiguous tail block of metadata.json — run build_index.py instead.")
if index.ntotal < first_fatwa:
sys.exit(f"Index has {index.ntotal} vectors but {first_fatwa} non-fatwa records — run build_index.py instead.")
print(f" keeping {first_fatwa:,} Quran/Hadith rows; dropping {len(metadata) - first_fatwa} fatwa records "
f"and {index.ntotal - first_fatwa} fatwa vectors")
fatwas = load_fatwa_entries(args.fatwa_dir or FATWA_DIRS, args.exclude_file or FATWA_EXCLUDE)
if not fatwas:
sys.exit("No fatwas found.")
import torch
device = "mps" if torch.backends.mps.is_available() else ("cuda" if torch.cuda.is_available() else "cpu")
print(f"Embedding {len(fatwas):,} fatwas with {args.model} on {device} …")
model = SentenceTransformer(args.model, device=device)
vectors = model.encode(
[passage_text(f) for f in fatwas], batch_size=EMBED_BATCH_SIZE,
convert_to_numpy=True, show_progress_bar=True,
).astype(np.float32)
faiss.normalize_L2(vectors)
if vectors.shape[1] != index.d:
sys.exit(f"Embedding dim {vectors.shape[1]} != index dim {index.d} — use the model the index was built with.")
new_index = faiss.IndexFlatIP(index.d)
new_index.add(index.reconstruct_n(0, first_fatwa))
new_index.add(vectors)
new_metadata = metadata[:first_fatwa] + fatwas
assert new_index.ntotal == len(new_metadata)
if args.in_place:
out_index, out_meta = INDEX_PATH, METADATA_PATH
shutil.copy2(INDEX_PATH, INDEX_PATH.with_suffix(".index.bak"))
shutil.copy2(METADATA_PATH, METADATA_PATH.with_suffix(".json.bak"))
else:
out_index = INDEX_PATH.with_name("QModel.fatwa-update.index")
out_meta = METADATA_PATH.with_name("metadata.fatwa-update.json")
faiss.write_index(new_index, str(out_index))
out_meta.write_text(json.dumps(new_metadata, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Wrote {out_index.name} ({new_index.ntotal:,} vectors) and {out_meta.name} "
f"({len(new_metadata):,} records, {len(fatwas):,} fatwas)")
if __name__ == "__main__":
main()