_embed_chunks.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. """
  2. Step 1: 加载中文 embedding 模型
  3. Step 2: 为 _chunks.jsonl 全部 22,781 条计算向量
  4. Step 3: 写入 FAISS 索引(含分类过滤元数据)
  5. 输出:
  6. _vectors.np — 22,781 × dim 向量矩阵
  7. _vector_meta.json — 每行 chunk 的 metadata(含 category)
  8. _faiss_index.bin — FAISS 索引文件
  9. """
  10. import json, os, sys
  11. import numpy as np
  12. from pathlib import Path
  13. from collections import defaultdict
  14. BASE = Path(r"C:\code\cfc\docs\菌群知识库")
  15. CHUNKS_FILE = BASE / "_chunks.jsonl"
  16. VEC_NP = BASE / "_vectors.npy"
  17. VEC_META = BASE / "_vector_meta.json"
  18. FAISS_INDEX = BASE / "_faiss_index.bin"
  19. EMBED_MODEL = "BAAI/bge-small-zh-v1.5"
  20. MAX_SEQ_LEN = 256
  21. BATCH_SIZE = 128
  22. print("=== Step 1: 加载中文 embedding 模型 ===")
  23. from sentence_transformers import SentenceTransformer
  24. print(f" 加载模型: {EMBED_MODEL} ...")
  25. model = SentenceTransformer(EMBED_MODEL)
  26. vec_dim = model.get_sentence_embedding_dimension()
  27. print(f" 向量维度: {vec_dim}")
  28. print(" 模型加载完成")
  29. print("\n=== Step 2: 读取 JSONL 并计算向量 ===")
  30. chunks_data = []
  31. meta_list = []
  32. with open(CHUNKS_FILE, "r", encoding="utf-8") as f:
  33. for line in f:
  34. d = json.loads(line)
  35. chunks_data.append(d)
  36. print(f" 共 {len(chunks_data)} 条 chunk")
  37. # 分批计算向量
  38. print(f" 开始计算向量(batch_size={BATCH_SIZE})...")
  39. all_vectors = []
  40. for i in range(0, len(chunks_data), BATCH_SIZE):
  41. batch = chunks_data[i:i+BATCH_SIZE]
  42. texts = [c["content"] for c in batch]
  43. vecs = model.encode(texts, batch_size=BATCH_SIZE, show_progress_bar=(i % 2000 == 0), normalize_embeddings=True)
  44. all_vectors.append(vecs)
  45. if (i // BATCH_SIZE + 1) % 10 == 0:
  46. print(f" processed {min(i+BATCH_SIZE, len(chunks_data))}/{len(chunks_data)} chunks...")
  47. vectors = np.concatenate(all_vectors, axis=0)
  48. vectors = np.ascontiguousarray(vectors, dtype=np.float32)
  49. print(f" 向量矩阵: {vectors.shape}")
  50. # 保存向量
  51. np.save(VEC_NP, vectors)
  52. print(f" 向量已保存: {VEC_NP}")
  53. # 保存 metadata(顺序与向量一致)
  54. for c in chunks_data:
  55. meta_list.append({
  56. "id": c["id"],
  57. "title": c["metadata"]["title"],
  58. "postId": c["metadata"]["postId"],
  59. "category": c["metadata"]["category"],
  60. "source_url": c["metadata"].get("source_url", ""),
  61. "topics": c["metadata"].get("topics", []),
  62. "chunk_index": c["metadata"]["chunk_index"],
  63. "chunk_total": c["metadata"]["chunk_total"],
  64. "filename": c["metadata"]["filename"],
  65. "content_preview": c["content"][:120],
  66. })
  67. with open(VEC_META, "w", encoding="utf-8") as f:
  68. json.dump(meta_list, f, ensure_ascii=False)
  69. print(f" 元数据已保存: {VEC_META}")
  70. # 分类过滤索引(category → chunk_indices)
  71. cat_indices = defaultdict(list)
  72. for idx, m in enumerate(meta_list):
  73. cat_indices[m["category"]].append(idx)
  74. cat_filter = json.dumps({k: v for k, v in cat_indices.items()}, ensure_ascii=False)
  75. with open(BASE / "_cat_filter.json", "w", encoding="utf-8") as f:
  76. json.dump({k: v for k, v in cat_indices.items()}, f, ensure_ascii=False)
  77. print("\n=== Step 3: 写入 FAISS 索引 ===")
  78. import faiss
  79. # 创建 HNSW 索引(带分类过滤支持)
  80. print(f" 创建 FAISS IDMap + HNSW 索引...")
  81. idx_flat = faiss.IndexFlatIP(vec_dim) # 内积(已归一化向量)
  82. idx = faiss.IndexIDMap2(idx_flat)
  83. # 添加向量(IDs = 0..N-1)
  84. idx.add_with_ids(vectors, np.arange(len(vectors)))
  85. print(f" 索引已添加 {idx.ntotal} 条向量")
  86. # 保存 FAISS 索引
  87. faiss.write_index(idx, str(FAISS_INDEX))
  88. print(f" FAISS 索引已保存: {FAISS_INDEX}")
  89. # 统计分类覆盖
  90. print("\n=== 分类统计 ===")
  91. for cat, indices in sorted(cat_indices.items()):
  92. print(f" {cat}: {len(indices)} chunks")
  93. print(f"\n✅ 完成!{len(chunks_data)} chunks → {vectors.shape[1]}d 向量 → FAISS 索引")