ingest.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. DB_URL = "postgresql+asyncpg://postgres:change-me@localhost:5432/pharmacopoeia"
  29. async def get_embeddings(texts: list[str], text_type: str = "document") -> list[list[float]]:
  30. async with httpx.AsyncClient(timeout=60) as client:
  31. resp = await client.post(
  32. EMBEDDING_URL,
  33. headers={
  34. "Content-Type": "application/json",
  35. "Authorization": f"Bearer {API_KEY}",
  36. },
  37. json={
  38. "model": EMBEDDING_MODEL,
  39. "input": {"texts": texts},
  40. "parameters": {"text_type": text_type},
  41. },
  42. )
  43. data = resp.json()
  44. if data.get("code") != "" and data.get("code") is not None:
  45. raise RuntimeError(f"Embedding API error: {data.get('message', data)}")
  46. # text-embedding-v3 返回结构: output.embeddings[].embedding
  47. return [item["embedding"] for item in data["output"]["embeddings"]]
  48. async def ingest_drugs(json_path: str):
  49. with open(json_path, "r", encoding="utf-8") as f:
  50. drugs = json.load(f)
  51. engine = create_async_engine(DB_URL)
  52. chunks_to_embed = []
  53. chunk_rows = []
  54. print(f"📦 加载 {len(drugs)} 个药品条目")
  55. for drug in drugs:
  56. source = f"{drug['source']['version']} {drug['source']['volume']} P{drug['source']['page']}"
  57. for section_key, section_text in drug["sections"].items():
  58. if not section_text or len(section_text.strip()) < 10:
  59. continue
  60. content = f"【{drug['name']} - {section_key}】\n{section_text}\n\n来源:{source}"
  61. chunks_to_embed.append(content)
  62. chunk_rows.append({
  63. "drug_id": drug["drug_id"],
  64. "section": section_key,
  65. "content": content,
  66. "source": source,
  67. "drug_name": drug["name"],
  68. "category": drug["category"],
  69. })
  70. print(f"✂️ 切出 {len(chunks_to_embed)} 个 chunk,正在向量化...")
  71. # 批量向量化(每批 10 个)
  72. all_vectors = []
  73. for i in range(0, len(chunks_to_embed), 10):
  74. batch = chunks_to_embed[i:i+10]
  75. vectors = await get_embeddings(batch, text_type="document")
  76. all_vectors.extend(vectors)
  77. n = min(i + 10, len(chunks_to_embed))
  78. print(f" 向量化进度: {n}/{len(chunks_to_embed)}")
  79. print(f"✅ 向量化完成,维度: {len(all_vectors[0]) if all_vectors else 'N/A'}")
  80. # 写入 PG
  81. count = 0
  82. async with engine.begin() as conn:
  83. for idx, (row, vec) in enumerate(zip(chunk_rows, all_vectors)):
  84. await conn.execute(
  85. text("""
  86. INSERT INTO drug_chunks (drug_id, section, content, source, chunk_index, embedding)
  87. VALUES (:drug_id, :section, :content, :source, :chunk_index, :embedding)
  88. """),
  89. {
  90. "drug_id": row["drug_id"],
  91. "section": row["section"],
  92. "content": row["content"],
  93. "source": row["source"],
  94. "chunk_index": idx,
  95. "embedding": json.dumps(vec),
  96. },
  97. )
  98. count += 1
  99. # 同时写入 drugs 表
  100. async with engine.begin() as conn:
  101. for drug in drugs:
  102. await conn.execute(
  103. text("""
  104. INSERT INTO drugs (drug_id, name, name_en, pinyin, category, subcategory, sections, source_version, source_volume, source_page, is_active)
  105. VALUES (:drug_id, :name, :name_en, :pinyin, :category, :subcategory, :sections, :source_version, :source_volume, :source_page, TRUE)
  106. ON CONFLICT (drug_id) DO UPDATE SET
  107. sections = EXCLUDED.sections,
  108. updated_at = NOW()
  109. """),
  110. {
  111. "drug_id": drug["drug_id"],
  112. "name": drug["name"],
  113. "name_en": drug.get("name_en"),
  114. "pinyin": drug.get("pinyin"),
  115. "category": drug.get("category"),
  116. "subcategory": drug.get("subcategory"),
  117. "sections": json.dumps(drug.get("sections", {}), ensure_ascii=False),
  118. "source_version": drug["source"]["version"],
  119. "source_volume": drug["source"]["volume"],
  120. "source_page": drug["source"]["page"],
  121. },
  122. )
  123. await engine.dispose()
  124. print(f"\n🎉 入库完成!药品 {len(drugs)} 个,chunk {count} 条")
  125. if __name__ == "__main__":
  126. sample_path = os.path.join(os.path.dirname(__file__), "data", "sample_drugs.json")
  127. if not os.path.exists(sample_path):
  128. print(f"❌ 找不到数据文件: {sample_path}")
  129. sys.exit(1)
  130. asyncio.run(ingest_drugs(sample_path))