- build_section_docs.py: indications, contraindications, warnings and interactions only, one representative per active-substance set - ingest: --dedupe, --retry-failed - embedding server: bounded PyTorch allocator cache and serialized encoding (fixes memory growth that crashed long ingests) - rebuild_vdb.py: recompute vector stores from the graph after corruption
153 lines
5.4 KiB
Python
153 lines
5.4 KiB
Python
"""Rebuild LightRAG vector stores from the knowledge graph and text chunks.
|
|
|
|
The vector files (vdb_entities.json, vdb_relationships.json, vdb_chunks.json)
|
|
are derived data: every record is an embedding of an entity or relation stored
|
|
in graph_chunk_entity_relation.graphml, or of a chunk in
|
|
kv_store_text_chunks.json. If they get corrupted (e.g. the process
|
|
ran out of memory while saving), they can be recomputed locally without the
|
|
LLM. Records use the same ids, fields and content format as LightRAG
|
|
(lightrag/operate.py), so the server loads them as if it wrote them itself.
|
|
|
|
Stop the LightRAG server before running this script.
|
|
|
|
python scripts/lightrag_ingest/rebuild_vdb.py --storage lightrag/rag_storage_qwen36
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
# Prevents the PyTorch CPU allocator cache from growing with varying input shapes.
|
|
os.environ.setdefault("LRU_CACHE_CAPACITY", "1")
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import shutil
|
|
import sys
|
|
import time
|
|
import zlib
|
|
from pathlib import Path
|
|
|
|
import networkx as nx
|
|
import numpy as np
|
|
from nano_vectordb import NanoVectorDB
|
|
from sentence_transformers import SentenceTransformer
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "lightrag"))
|
|
from lightrag.utils import compute_mdhash_id # noqa: E402
|
|
|
|
MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2" # same model as embedding_server.py
|
|
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
|
|
|
|
def entity_records(graph: nx.Graph) -> dict[str, dict]:
|
|
records = {}
|
|
for name, data in graph.nodes(data=True):
|
|
records[compute_mdhash_id(name, prefix="ent-")] = {
|
|
"entity_name": name,
|
|
"content": f"{name}\n{data.get('description', '')}",
|
|
"source_id": data.get("source_id", ""),
|
|
"file_path": data.get("file_path", ""),
|
|
}
|
|
return records
|
|
|
|
|
|
def relation_records(graph: nx.Graph) -> dict[str, dict]:
|
|
records = {}
|
|
for u, v, data in graph.edges(data=True):
|
|
src, tgt = (u, v) if u <= v else (v, u)
|
|
keywords = data.get("keywords", "")
|
|
records[compute_mdhash_id(src + tgt, prefix="rel-")] = {
|
|
"src_id": src,
|
|
"tgt_id": tgt,
|
|
"content": f"{keywords}\t{src}\n{tgt}\n{data.get('description', '')}",
|
|
"source_id": data.get("source_id", ""),
|
|
"file_path": data.get("file_path", ""),
|
|
}
|
|
return records
|
|
|
|
|
|
def chunk_records(storage: Path) -> dict[str, dict]:
|
|
chunks = json.loads((storage / "kv_store_text_chunks.json").read_text(encoding="utf-8"))
|
|
return {
|
|
chunk_id: {
|
|
"full_doc_id": chunk.get("full_doc_id", ""),
|
|
"content": chunk.get("content", ""),
|
|
"file_path": chunk.get("file_path", ""),
|
|
}
|
|
for chunk_id, chunk in chunks.items()
|
|
}
|
|
|
|
|
|
def build_store(path: Path, records: dict[str, dict], model: SentenceTransformer, batch_size: int) -> None:
|
|
ids = list(records)
|
|
contents = [records[i]["content"] for i in ids]
|
|
print(f"Embedding {len(ids)} records for {path.name} ...")
|
|
started = time.time()
|
|
vectors = model.encode(
|
|
contents,
|
|
batch_size=batch_size,
|
|
normalize_embeddings=True,
|
|
show_progress_bar=True,
|
|
convert_to_numpy=True,
|
|
).astype(np.float32)
|
|
print(f" done in {time.time() - started:.0f}s")
|
|
|
|
now = int(time.time())
|
|
datas = []
|
|
for record_id, vector in zip(ids, vectors):
|
|
encoded = base64.b64encode(zlib.compress(vector.astype(np.float16).tobytes())).decode("utf-8")
|
|
datas.append({
|
|
"__id__": record_id,
|
|
"__created_at__": now,
|
|
**records[record_id],
|
|
"vector": encoded,
|
|
"__vector__": vector,
|
|
})
|
|
|
|
tmp = path.with_suffix(".rebuilt.json")
|
|
if tmp.exists():
|
|
tmp.unlink()
|
|
db = NanoVectorDB(vectors.shape[1], storage_file=str(tmp))
|
|
db.upsert(datas=datas)
|
|
db.save()
|
|
|
|
if path.exists():
|
|
backup = path.with_suffix(".broken.json")
|
|
shutil.move(str(path), str(backup))
|
|
print(f" old file moved to {backup.name}")
|
|
shutil.move(str(tmp), str(path))
|
|
print(f" saved {path}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument("--storage", type=Path, default=ROOT / "lightrag" / "rag_storage_qwen36")
|
|
parser.add_argument("--batch-size", type=int, default=128)
|
|
parser.add_argument(
|
|
"--only", choices=("entities", "relationships", "chunks"), help="Rebuild only one store."
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
model = SentenceTransformer(MODEL_NAME)
|
|
if args.only in (None, "entities", "relationships"):
|
|
graph_file = args.storage / "graph_chunk_entity_relation.graphml"
|
|
print(f"Loading graph {graph_file} ...")
|
|
graph = nx.read_graphml(graph_file)
|
|
print(f" {graph.number_of_nodes()} nodes, {graph.number_of_edges()} edges")
|
|
if args.only in (None, "entities"):
|
|
build_store(args.storage / "vdb_entities.json", entity_records(graph), model, args.batch_size)
|
|
if args.only in (None, "relationships"):
|
|
build_store(args.storage / "vdb_relationships.json", relation_records(graph), model, args.batch_size)
|
|
if args.only in (None, "chunks"):
|
|
build_store(args.storage / "vdb_chunks.json", chunk_records(args.storage), model, args.batch_size)
|
|
print("Finished.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|