build_index.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. #!/usr/bin/env python3
  2. """
  3. 构建 RAG 索引: 文本分块 → JSON 索引文件
  4. 简单但实用的 BM25 风格索引(无需向量数据库)
  5. """
  6. import json
  7. import os
  8. import re
  9. from pathlib import Path
  10. PROCESSED_DIR = Path('/home/67/knowledge/maritime/processed')
  11. INDEX_FILE = Path('/home/67/knowledge/maritime/rag_index.json')
  12. CHUNK_SIZE = 512 # 每块字符数
  13. CHUNK_OVERLAP = 50 # 重叠字符数
  14. def split_chunks(text, source_file, chunk_size=CHUNK_SIZE, overlap=CHUNK_OVERLAP):
  15. """将文本分割为带重叠的块"""
  16. chunks = []
  17. # 按段落优先分割
  18. paragraphs = re.split(r'\n\s*\n', text)
  19. current_chunk = ''
  20. chunk_id = 0
  21. for para in paragraphs:
  22. para = para.strip()
  23. if not para:
  24. continue
  25. if len(current_chunk) + len(para) <= chunk_size:
  26. current_chunk += '\n' + para if current_chunk else para
  27. else:
  28. if current_chunk:
  29. chunks.append({
  30. 'id': f'{source_file}#chunk{chunk_id}',
  31. 'source': source_file,
  32. 'chunk_id': chunk_id,
  33. 'text': current_chunk,
  34. 'length': len(current_chunk),
  35. })
  36. chunk_id += 1
  37. # 重叠: 保留上一块末尾
  38. overlap_text = current_chunk[-overlap:] if len(current_chunk) > overlap else ''
  39. current_chunk = overlap_text + '\n' + para
  40. else:
  41. # 单段落超长,按字符切
  42. for i in range(0, len(para), chunk_size - overlap):
  43. chunk_text = para[i:i+chunk_size]
  44. chunks.append({
  45. 'id': f'{source_file}#chunk{chunk_id}',
  46. 'source': source_file,
  47. 'chunk_id': chunk_id,
  48. 'text': chunk_text,
  49. 'length': len(chunk_text),
  50. })
  51. chunk_id += 1
  52. current_chunk = ''
  53. # 最后一块
  54. if current_chunk.strip():
  55. chunks.append({
  56. 'id': f'{source_file}#chunk{chunk_id}',
  57. 'source': source_file,
  58. 'chunk_id': chunk_id,
  59. 'text': current_chunk,
  60. 'length': len(current_chunk),
  61. })
  62. return chunks
  63. # 构建索引
  64. all_chunks = []
  65. txt_files = sorted(PROCESSED_DIR.glob('*.txt'))
  66. print(f'处理 {len(txt_files)} 个文本文件...')
  67. for i, txt_path in enumerate(txt_files):
  68. text = txt_path.read_text(encoding='utf-8')
  69. if '[扫描版PDF' in text:
  70. continue
  71. chunks = split_chunks(text, txt_path.stem)
  72. all_chunks.extend(chunks)
  73. if (i+1) % 200 == 0:
  74. print(f' 进度: {i+1}/{len(txt_files)}, 累计块数: {len(all_chunks)}')
  75. # 构建关键词倒排索引(简单 BM25 近似)
  76. print(f'构建倒排索引... ({len(all_chunks)} 个文本块)')
  77. keyword_index = {}
  78. for chunk in all_chunks:
  79. # 分词: 中文按字符 bigram + 英文按空格
  80. words = set()
  81. text = chunk['text'].lower()
  82. # 中文 bigram
  83. chinese_chars = re.findall(r'[\u4e00-\u9fff]+', text)
  84. for cc in chinese_chars:
  85. for j in range(len(cc) - 1):
  86. words.add(cc[j:j+2])
  87. if len(cc) >= 3:
  88. for j in range(len(cc) - 2):
  89. words.add(cc[j:j+3])
  90. # 英文单词
  91. eng_words = re.findall(r'[a-z]+', text)
  92. words.update(eng_words)
  93. for w in words:
  94. if w not in keyword_index:
  95. keyword_index[w] = []
  96. keyword_index[w].append(chunk['id'])
  97. # 保存索引
  98. index_data = {
  99. 'total_chunks': len(all_chunks),
  100. 'total_files': len(txt_files),
  101. 'chunk_size': CHUNK_SIZE,
  102. 'chunks': all_chunks,
  103. 'keyword_index_size': len(keyword_index),
  104. }
  105. with open(INDEX_FILE, 'w', encoding='utf-8') as f:
  106. json.dump(index_data, f, ensure_ascii=False)
  107. # 单独保存倒排索引
  108. with open(INDEX_FILE.with_suffix('.keywords.json'), 'w', encoding='utf-8') as f:
  109. json.dump(keyword_index, f, ensure_ascii=False)
  110. print(f'完成!')
  111. print(f' 文本块: {len(all_chunks)}')
  112. print(f' 关键词: {len(keyword_index)}')
  113. print(f' 索引文件: {INDEX_FILE} ({INDEX_FILE.stat().st_size / 1024 / 1024:.1f} MB)')