ingest.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. """
  2. DashScope 文本嵌入 + PG 入库脚本
  3. 将药典条目 chunk → 向量化 → 写入 pgvector
  4. """
  5. import json
  6. import asyncio
  7. import os
  8. import sys
  9. from pathlib import Path
  10. import httpx
  11. from sqlalchemy.ext.asyncio import create_async_engine
  12. from sqlalchemy import text
  13. sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "backend"))
  14. sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
  15. os.chdir(Path(__file__).resolve().parent.parent)
  16. def load_env():
  17. env_file = Path(__file__).resolve().parent.parent / ".env"
  18. if env_file.exists():
  19. for line in open(env_file):
  20. line = line.strip()
  21. if line and not line.startswith("#") and "=" in line:
  22. key, _, val = line.partition("=")
  23. os.environ.setdefault(key.strip(), val.strip())
  24. load_env()
  25. API_KEY = os.environ.get("QWEN_API_KEY", "")
  26. EMBEDDING_URL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding"
  27. EMBEDDING_MODEL = "text-embedding-v3"
  28. # 从环境变量读取数据库连接信息
  29. PG_HOST = os.environ.get("POSTGRES_HOST", "localhost")
  30. PG_PORT = os.environ.get("POSTGRES_PORT", "5432")
  31. PG_DB = os.environ.get("POSTGRES_DB", "pharmacopoeia")
  32. PG_USER = os.environ.get("POSTGRES_USER", "postgres")
  33. PG_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "postgres")
  34. DB_URL = f"postgresql+asyncpg://{PG_USER}:{PG_PASSWORD}@{PG_HOST}:{PG_PORT}/{PG_DB}"
  35. async def get_embeddings(texts: list[str], text_type: str = "document") -> list[list[float]]:
  36. async with httpx.AsyncClient(timeout=60) as client:
  37. resp = await client.post(
  38. EMBEDDING_URL,
  39. headers={
  40. "Content-Type": "application/json",
  41. "Authorization": f"Bearer {API_KEY}",
  42. },
  43. json={
  44. "model": EMBEDDING_MODEL,
  45. "input": {"texts": texts},
  46. "parameters": {"text_type": text_type},
  47. },
  48. )
  49. data = resp.json()
  50. if data.get("code") != "" and data.get("code") is not None:
  51. raise RuntimeError(f"Embedding API error: {data.get('message', data)}")
  52. # text-embedding-v3 返回结构: output.embeddings[].embedding
  53. return [item["embedding"] for item in data["output"]["embeddings"]]
  54. async def ingest_drugs(json_path: str):
  55. with open(json_path, "r", encoding="utf-8") as f:
  56. drugs = json.load(f)
  57. engine = create_async_engine(DB_URL)
  58. chunks_to_embed = []
  59. chunk_rows = []
  60. print(f"📦 加载 {len(drugs)} 个药品条目")
  61. for drug in drugs:
  62. source = f"{drug['source']['version']} {drug['source']['volume']} P{drug['source']['page']}"
  63. for section_key, section_text in drug["sections"].items():
  64. if not section_text or len(section_text.strip()) < 10:
  65. continue
  66. content = f"【{drug['name']} - {section_key}】\n{section_text}\n\n来源:{source}"
  67. chunks_to_embed.append(content)
  68. chunk_rows.append({
  69. "drug_id": drug["drug_id"],
  70. "section": section_key,
  71. "content": content,
  72. "source": source,
  73. "drug_name": drug["name"],
  74. "category": drug["category"],
  75. })
  76. print(f"✂️ 切出 {len(chunks_to_embed)} 个 chunk,正在向量化...")
  77. # 批量向量化(每批 10 个)
  78. all_vectors = []
  79. for i in range(0, len(chunks_to_embed), 10):
  80. batch = chunks_to_embed[i:i+10]
  81. vectors = await get_embeddings(batch, text_type="document")
  82. all_vectors.extend(vectors)
  83. n = min(i + 10, len(chunks_to_embed))
  84. print(f" 向量化进度: {n}/{len(chunks_to_embed)}")
  85. print(f"✅ 向量化完成,维度: {len(all_vectors[0]) if all_vectors else 'N/A'}")
  86. # 写入 PG
  87. count = 0
  88. async with engine.begin() as conn:
  89. for idx, (row, vec) in enumerate(zip(chunk_rows, all_vectors)):
  90. await conn.execute(
  91. text("""
  92. INSERT INTO drug_chunks (drug_id, section, content, source, chunk_index, embedding, vec)
  93. VALUES (:drug_id, :section, :content, :source, :chunk_index, :embedding, :vec)
  94. """),
  95. {
  96. "drug_id": row["drug_id"],
  97. "section": row["section"],
  98. "content": row["content"],
  99. "source": row["source"],
  100. "chunk_index": idx,
  101. "embedding": json.dumps(vec),
  102. "vec": f"[{','.join(str(v) for v in vec)}]",
  103. },
  104. )
  105. count += 1
  106. # 同时写入 drugs 表
  107. async with engine.begin() as conn:
  108. for drug in drugs:
  109. await conn.execute(
  110. text("""
  111. INSERT INTO drugs (drug_id, name, name_en, pinyin, category, subcategory, sections, source_version, source_volume, source_page, is_active)
  112. VALUES (:drug_id, :name, :name_en, :pinyin, :category, :subcategory, :sections, :source_version, :source_volume, :source_page, TRUE)
  113. ON CONFLICT (drug_id) DO UPDATE SET
  114. sections = EXCLUDED.sections,
  115. updated_at = NOW()
  116. """),
  117. {
  118. "drug_id": drug["drug_id"],
  119. "name": drug["name"],
  120. "name_en": drug.get("name_en"),
  121. "pinyin": drug.get("pinyin"),
  122. "category": drug.get("category"),
  123. "subcategory": drug.get("subcategory"),
  124. "sections": json.dumps(drug.get("sections", {}), ensure_ascii=False),
  125. "source_version": drug["source"]["version"],
  126. "source_volume": drug["source"]["volume"],
  127. "source_page": drug["source"]["page"],
  128. },
  129. )
  130. await engine.dispose()
  131. print(f"\n🎉 入库完成!药品 {len(drugs)} 个,chunk {count} 条")
  132. if __name__ == "__main__":
  133. if len(sys.argv) > 1:
  134. json_path = sys.argv[1]
  135. else:
  136. json_path = os.path.join(os.path.dirname(__file__), "data", "sample_drugs.json")
  137. if not os.path.exists(json_path):
  138. print(f"❌ 找不到数据文件: {json_path}")
  139. sys.exit(1)
  140. asyncio.run(ingest_drugs(json_path))