| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- #!/usr/bin/env python3
- """
- RAG search engine v2: uses optimized pickle index
- Loads in ~6s instead of ~50s, uses 96% less disk
- """
- import json
- import os
- import re
- import math
- import pickle
- import struct
- from collections import Counter
- from pathlib import Path
- def _resolve_config():
- search_paths = [
- os.environ.get('AGENT_CONFIG', ''),
- Path(__file__).parent.parent / 'agent-config.yml',
- Path('/app/agentexample/qaagent67/agent-config.yml'),
- ]
- for p in search_paths:
- p = Path(p)
- if p.exists():
- import yaml
- with open(p) as f:
- cfg = yaml.safe_load(f)
- knowledge = cfg.get('knowledge', {})
- base = Path(knowledge.get('baseDir', '/data/knowledge/maritime'))
- return {'base_dir': base}
- base = Path(os.environ.get('KNOWLEDGE_DIR', '/data/knowledge/maritime'))
- return {'base_dir': base}
- _config = None
- _index_cache = {}
- def _get_config():
- global _config
- if _config is None:
- _config = _resolve_config()
- return _config
- def _load_index():
- if 'chunks' not in _index_cache:
- cfg = _get_config()
- base = cfg['base_dir']
- # Try optimized pickle first, fall back to JSON
- pkl_index = base / 'rag_index.pkl'
- pkl_kw = base / 'rag_keywords.pkl'
- if pkl_index.exists() and pkl_kw.exists():
- with open(pkl_index, 'rb') as f:
- data = pickle.load(f)
- with open(pkl_kw, 'rb') as f:
- kw_raw = pickle.load(f)
- chunks = data['chunks'] # list of (source, chunk_id, text) tuples
- use_bytes = data.get('use_bytes', False)
- # Build lookup
- chunk_lookup = {}
- for i, (source, chunk_id, text) in enumerate(chunks):
- cid = f"{source}#chunk{chunk_id}"
- chunk_lookup[i] = {
- 'id': cid, 'source': source,
- 'chunk_id': chunk_id, 'text': text, 'length': len(text),
- }
- # Convert bytes postings back to int lists on demand
- _index_cache['chunks'] = chunk_lookup
- _index_cache['total'] = len(chunks)
- _index_cache['keywords'] = kw_raw
- _index_cache['use_bytes'] = use_bytes
- else:
- # Fallback to JSON
- json_index = base / 'rag_index.json'
- json_kw = base / 'rag_index.keywords.json'
- with open(json_index, 'r', encoding='utf-8') as f:
- data = json.load(f)
- _index_cache['chunks'] = {c['id']: c for c in data['chunks']}
- _index_cache['total'] = data['total_chunks']
- with open(json_kw, 'r', encoding='utf-8') as f:
- _index_cache['keywords'] = json.load(f)
- _index_cache['use_bytes'] = False
- return _index_cache
- def _decode_postings(raw, use_bytes):
- """Decode posting list from bytes or list"""
- if use_bytes and isinstance(raw, bytes):
- n = len(raw) // 2
- return list(struct.unpack(f"<{n}H", raw))
- return raw
- def tokenize(text):
- text = text.lower()
- words = []
- chinese = re.findall(r'[\u4e00-\u9fff]+', text)
- for cc in chinese:
- for i in range(len(cc) - 1):
- words.append(cc[i:i+2])
- if len(cc) >= 3:
- for i in range(len(cc) - 2):
- words.append(cc[i:i+3])
- words.extend(re.findall(r'[a-z]+', text))
- return words
- def search(query, top_k=5):
- idx = _load_index()
- keywords = idx['keywords']
- chunks = idx['chunks']
- total = idx['total']
- use_bytes = idx.get('use_bytes', False)
- query_tokens = tokenize(query)
- if not query_tokens:
- return []
- scores = Counter()
- for token in query_tokens:
- if token not in keywords:
- continue
- posting_raw = keywords[token]
- posting_list = _decode_postings(posting_raw, use_bytes)
- idf = math.log((total - len(posting_list) + 0.5) / (len(posting_list) + 0.5) + 1)
- for idx_num in posting_list:
- scores[idx_num] += idf
- top = scores.most_common(top_k)
- results = []
- for idx_num, score in top:
- chunk = chunks.get(idx_num)
- if chunk:
- results.append({
- 'source': chunk['source'],
- 'chunk_id': chunk['chunk_id'],
- 'score': round(score, 3),
- 'text': chunk['text'],
- })
- return results
- if __name__ == '__main__':
- import sys, time
- cfg = _get_config()
- print(f'Config: base={cfg["base_dir"]}')
- t0 = time.time()
- _load_index()
- t1 = time.time()
- print(f'Index loaded in {t1-t0:.1f}s')
- query = sys.argv[1] if len(sys.argv) > 1 else '船舶进出港报告'
- t0 = time.time()
- results = search(query, top_k=3)
- t1 = time.time()
- print(f'Search "{query}" in {(t1-t0)*1000:.0f}ms')
- for i, r in enumerate(results):
- print(f'\n[{i+1}] {r["source"][:50]} (score={r["score"]})')
- print(f' {r["text"][:150]}...')
|