#!/usr/bin/env python3 """ Optimize RAG keyword index: 1. Prune useless keywords (too frequent / too rare) 2. Use pickle for fast loading 3. Store chunk_id as int index instead of string """ import json import pickle import time import re from pathlib import Path from collections import Counter BASE = Path("/data/knowledge/maritime") INDEX_FILE = BASE / "rag_index.json" KEYWORD_FILE = BASE / "rag_index.keywords.json" # Optimized outputs OPT_INDEX = BASE / "rag_index.pkl" OPT_KEYWORDS = BASE / "rag_keywords.pkl" print("=== RAG Index Optimizer ===") # Load original print("Loading chunk index...") t0 = time.time() with open(INDEX_FILE) as f: data = json.load(f) chunks = data["chunks"] print(f" Chunks: {len(chunks)}, loaded in {time.time()-t0:.1f}s") print("Loading keyword index...") t0 = time.time() with open(KEYWORD_FILE) as f: keywords = json.load(f) t1 = time.time() print(f" Keywords: {len(keywords)}, loaded in {t1-t0:.1f}s") # Analyze lengths = [len(v) for v in keywords.values()] print(f"\n--- Before optimization ---") print(f" Total keywords: {len(keywords)}") print(f" Total postings: {sum(lengths)}") print(f" Avg postings/keyword: {sum(lengths)/len(lengths):.1f}") print(f" Max postings: {max(lengths)}") # Build chunk_id -> int index mapping chunk_ids = [c["id"] for c in chunks] id_to_idx = {cid: i for i, cid in enumerate(chunk_ids)} # Prune keywords MIN_FREQ = 2 # Keywords appearing in <2 chunks are noise MAX_FREQ_PCT = 0.3 # Keywords appearing in >30% of chunks are too common (stopwords) max_freq = int(len(chunks) * MAX_FREQ_PCT) pruned = {} removed_rare = 0 removed_common = 0 for kw, posting_list in keywords.items(): n = len(posting_list) if n < MIN_FREQ: removed_rare += 1 continue if n > max_freq: removed_common += 1 continue # Convert string IDs to int indices for compact storage int_postings = [] for cid in posting_list: if cid in id_to_idx: int_postings.append(id_to_idx[cid]) if int_postings: pruned[kw] = int_postings print(f"\n--- After pruning ---") print(f" Removed rare (<{MIN_FREQ}): {removed_rare}") print(f" Removed common (>{max_freq}): {removed_common}") print(f" Remaining keywords: {len(pruned)}") print(f" Total postings: {sum(len(v) for v in pruned.values())}") # Build optimized chunk data (minimal fields) opt_chunks = [] for c in chunks: opt_chunks.append({ "id": c["id"], "source": c["source"], "chunk_id": c["chunk_id"], "text": c["text"], "length": c["length"], }) # Save as pickle print("\nSaving optimized index (pickle)...") t0 = time.time() opt_data = { "total_chunks": len(opt_chunks), "total_files": data.get("total_files", 0), "chunk_size": data.get("chunk_size", 512), "chunks": opt_chunks, "chunk_ids": chunk_ids, # for reverse lookup } with open(OPT_INDEX, "wb") as f: pickle.dump(opt_data, f, protocol=pickle.HIGHEST_PROTOCOL) with open(OPT_KEYWORDS, "wb") as f: pickle.dump(pruned, f, protocol=pickle.HIGHEST_PROTOCOL) t1 = time.time() print(f" Saved in {t1-t0:.1f}s") # File sizes idx_size = OPT_INDEX.stat().st_size / 1024 / 1024 kw_size = OPT_KEYWORDS.stat().st_size / 1024 / 1024 old_kw_size = KEYWORD_FILE.stat().st_size / 1024 / 1024 print(f"\n--- File sizes ---") print(f" Chunk index: {idx_size:.1f} MB (pickle)") print(f" Keyword index: {kw_size:.1f} MB (pickle) vs {old_kw_size:.1f} MB (JSON) = {kw_size/old_kw_size*100:.0f}%") # Verify load time print("\n--- Load time verification ---") t0 = time.time() with open(OPT_INDEX, "rb") as f: d1 = pickle.load(f) t1 = time.time() with open(OPT_KEYWORDS, "rb") as f: d2 = pickle.load(f) t2 = time.time() print(f" Chunk index load: {(t1-t0)*1000:.0f}ms") print(f" Keyword index load: {(t2-t1)*1000:.0f}ms") print(f" Total: {(t2-t0)*1000:.0f}ms") print(f"\n Speedup: {43000/(t2-t0)/1000:.0f}x faster!")