| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- """
- DashScope 文本嵌入 + PG 入库脚本
- 将药典条目 chunk → 向量化 → 写入 pgvector
- """
- import json
- import asyncio
- import os
- import sys
- from pathlib import Path
- import httpx
- from sqlalchemy.ext.asyncio import create_async_engine
- from sqlalchemy import text
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "backend"))
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
- os.chdir(Path(__file__).resolve().parent.parent)
- def load_env():
- env_file = Path(__file__).resolve().parent.parent / ".env"
- if env_file.exists():
- for line in open(env_file):
- line = line.strip()
- if line and not line.startswith("#") and "=" in line:
- key, _, val = line.partition("=")
- os.environ.setdefault(key.strip(), val.strip())
- load_env()
- API_KEY = os.environ.get("QWEN_API_KEY", "")
- EMBEDDING_URL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding"
- EMBEDDING_MODEL = "text-embedding-v3"
- # 从环境变量读取数据库连接信息
- PG_HOST = os.environ.get("POSTGRES_HOST", "localhost")
- PG_PORT = os.environ.get("POSTGRES_PORT", "5432")
- PG_DB = os.environ.get("POSTGRES_DB", "pharmacopoeia")
- PG_USER = os.environ.get("POSTGRES_USER", "postgres")
- PG_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "postgres")
- DB_URL = f"postgresql+asyncpg://{PG_USER}:{PG_PASSWORD}@{PG_HOST}:{PG_PORT}/{PG_DB}"
- async def get_embeddings(texts: list[str], text_type: str = "document") -> list[list[float]]:
- async with httpx.AsyncClient(timeout=60) as client:
- resp = await client.post(
- EMBEDDING_URL,
- headers={
- "Content-Type": "application/json",
- "Authorization": f"Bearer {API_KEY}",
- },
- json={
- "model": EMBEDDING_MODEL,
- "input": {"texts": texts},
- "parameters": {"text_type": text_type},
- },
- )
- data = resp.json()
- if data.get("code") != "" and data.get("code") is not None:
- raise RuntimeError(f"Embedding API error: {data.get('message', data)}")
- # text-embedding-v3 返回结构: output.embeddings[].embedding
- return [item["embedding"] for item in data["output"]["embeddings"]]
- async def ingest_drugs(json_path: str):
- with open(json_path, "r", encoding="utf-8") as f:
- drugs = json.load(f)
- engine = create_async_engine(DB_URL)
- chunks_to_embed = []
- chunk_rows = []
- print(f"📦 加载 {len(drugs)} 个药品条目")
- for drug in drugs:
- source = f"{drug['source']['version']} {drug['source']['volume']} P{drug['source']['page']}"
- for section_key, section_text in drug["sections"].items():
- if not section_text or len(section_text.strip()) < 10:
- continue
- content = f"【{drug['name']} - {section_key}】\n{section_text}\n\n来源:{source}"
- chunks_to_embed.append(content)
- chunk_rows.append({
- "drug_id": drug["drug_id"],
- "section": section_key,
- "content": content,
- "source": source,
- "drug_name": drug["name"],
- "category": drug["category"],
- })
- print(f"✂️ 切出 {len(chunks_to_embed)} 个 chunk,正在向量化...")
- # 批量向量化(每批 10 个)
- all_vectors = []
- for i in range(0, len(chunks_to_embed), 10):
- batch = chunks_to_embed[i:i+10]
- vectors = await get_embeddings(batch, text_type="document")
- all_vectors.extend(vectors)
- n = min(i + 10, len(chunks_to_embed))
- print(f" 向量化进度: {n}/{len(chunks_to_embed)}")
- print(f"✅ 向量化完成,维度: {len(all_vectors[0]) if all_vectors else 'N/A'}")
- # 写入 PG
- count = 0
- async with engine.begin() as conn:
- for idx, (row, vec) in enumerate(zip(chunk_rows, all_vectors)):
- await conn.execute(
- text("""
- INSERT INTO drug_chunks (drug_id, section, content, source, chunk_index, vec)
- VALUES (:drug_id, :section, :content, :source, :chunk_index, :vec)
- """),
- {
- "drug_id": row["drug_id"],
- "section": row["section"],
- "content": row["content"],
- "source": row["source"],
- "chunk_index": idx,
- "vec": f"[{','.join(str(v) for v in vec)}]",
- },
- )
- count += 1
- # 同时写入 drugs 表
- async with engine.begin() as conn:
- for drug in drugs:
- await conn.execute(
- text("""
- INSERT INTO drugs (drug_id, name, name_en, pinyin, category, subcategory, sections, source_version, source_volume, source_page, is_active)
- VALUES (:drug_id, :name, :name_en, :pinyin, :category, :subcategory, :sections, :source_version, :source_volume, :source_page, TRUE)
- ON CONFLICT (drug_id) DO UPDATE SET
- sections = EXCLUDED.sections,
- updated_at = NOW()
- """),
- {
- "drug_id": drug["drug_id"],
- "name": drug["name"],
- "name_en": drug.get("name_en"),
- "pinyin": drug.get("pinyin"),
- "category": drug.get("category"),
- "subcategory": drug.get("subcategory"),
- "sections": json.dumps(drug.get("sections", {}), ensure_ascii=False),
- "source_version": drug["source"]["version"],
- "source_volume": drug["source"]["volume"],
- "source_page": drug["source"]["page"],
- },
- )
- await engine.dispose()
- print(f"\n🎉 入库完成!药品 {len(drugs)} 个,chunk {count} 条")
- if __name__ == "__main__":
- if len(sys.argv) > 1:
- json_path = sys.argv[1]
- else:
- json_path = os.path.join(os.path.dirname(__file__), "data", "sample_drugs.json")
- if not os.path.exists(json_path):
- print(f"❌ 找不到数据文件: {json_path}")
- sys.exit(1)
- asyncio.run(ingest_drugs(json_path))
|