Files
meida_backend/migrations/versions/0005_kb_tables.py
T
xsl d5f366fa4c chore(prep): shared groundwork for the Sprint-2 parallel batch
第二批(T6~T11)协调提交:
- 迁移 0005:kb 七表(entity/relation/rule/chunk vector1024+tsv 生成列/
  asset/color/gap),HNSW+GIN 索引,FK 级联
- ORM:七个 Kb* 模型(pgvector Vector/TSVECTOR/Computed)
- 配置:检索与 embedding、华为 JWKS、短信全参数组
- 错误码:40110/40111/42910/42911/61000
- 骨架路由:kb.py + auth_sms.py(已注册);依赖:tencentcloud-sdk-python-sms
  + spike extra(onnxruntime/opencv);gitignore:models/ 与 spike 输出
2026-08-14 17:01:43 +08:00

180 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Knowledge base tables for the Sprint-2 parallel batch (架构文档 §6.2 kb 域).
七张表一次落齐,任务批(T6/T7/T8)不再写迁移:
kb_entity / kb_relation / kb_rule → T6 实体图谱、T7 规则引擎
kb_chunk (vector 1024 + tsv) → T8 混合检索
kb_asset / kb_color → T6/T8
kb_gap → T8 无结果缺口埋点
索引决策:
- kb_chunk.embedding 用 HNSW(cosine)——万级数据下召回延迟远优于 IVFFlat,
且不需要先灌数据再训练聚类(IVFFlat 的 num_lists 依赖数据量)。
- tsv 用 GIN;tsvector 是生成列(PG12+),应用层不手工维护。
Reversible: 是。降级按依赖倒序 drop。
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from pgvector.sqlalchemy import Vector
from sqlalchemy.dialects import postgresql
revision: str = "0005"
down_revision: str | None = "0004"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"kb_entity",
sa.Column("entity_id", sa.BigInteger(), nullable=False),
sa.Column("entity_type", sa.String(length=32), nullable=False),
sa.Column("name", sa.String(length=128), nullable=False),
sa.Column("attrs", postgresql.JSONB(), nullable=False),
sa.Column("status", sa.String(length=16), nullable=False, server_default=sa.text("'draft'")),
sa.Column("version", sa.Integer(), nullable=False, server_default=sa.text("1")),
sa.Column("source", sa.String(length=255), nullable=True),
sa.Column("license", sa.String(length=64), nullable=True),
sa.Column("updated_by", sa.String(length=64), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.PrimaryKeyConstraint("entity_id"),
sa.UniqueConstraint("entity_type", "name", name="uq_kb_entity_type_name"),
)
op.create_index("ix_kb_entity_type_status", "kb_entity", ["entity_type", "status"])
op.create_table(
"kb_relation",
sa.Column("relation_id", sa.BigInteger(), nullable=False),
sa.Column("from_id", sa.BigInteger(), nullable=False),
sa.Column("rel_type", sa.String(length=64), nullable=False),
sa.Column("to_id", sa.BigInteger(), nullable=False),
sa.Column("weight", sa.Numeric(4, 3), nullable=False, server_default=sa.text("1")),
sa.Column("confidence", sa.Numeric(4, 3), nullable=False, server_default=sa.text("1")),
sa.Column("evidence", sa.String(length=255), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.PrimaryKeyConstraint("relation_id"),
sa.UniqueConstraint("from_id", "rel_type", "to_id", name="uq_kb_relation_triple"),
sa.ForeignKeyConstraint(["from_id"], ["kb_entity.entity_id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["to_id"], ["kb_entity.entity_id"], ondelete="CASCADE"),
)
op.create_index("ix_kb_relation_to", "kb_relation", ["to_id"])
op.create_table(
"kb_rule",
sa.Column("rule_id", sa.BigInteger(), nullable=False),
sa.Column("rule_key", sa.String(length=64), nullable=False),
sa.Column("category", sa.String(length=32), nullable=False),
sa.Column("version", sa.Integer(), nullable=False, server_default=sa.text("1")),
sa.Column("status", sa.String(length=16), nullable=False, server_default=sa.text("'draft'")),
sa.Column("priority", sa.Integer(), nullable=False, server_default=sa.text("60")),
sa.Column("conditions", postgresql.JSONB(), nullable=False),
sa.Column("effects", postgresql.JSONB(), nullable=False),
sa.Column("is_hard", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("confidence", sa.Numeric(4, 3), nullable=False, server_default=sa.text("0.9")),
sa.Column("evidence", sa.String(length=255), nullable=True),
sa.Column("author", sa.String(length=64), nullable=True),
sa.Column("reviewer", sa.String(length=64), nullable=True),
sa.Column("published_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.PrimaryKeyConstraint("rule_id"),
sa.UniqueConstraint("rule_key", name="uq_kb_rule_key"),
)
op.create_index("ix_kb_rule_status_priority", "kb_rule", ["status", "priority"])
op.create_table(
"kb_chunk",
sa.Column("chunk_id", sa.BigInteger(), nullable=False),
sa.Column("entity_id", sa.BigInteger(), nullable=False),
sa.Column("chunk_type", sa.String(length=32), nullable=False, server_default=sa.text("'text'")),
sa.Column("text", sa.Text(), nullable=False),
sa.Column("embedding", Vector(1024), nullable=False),
sa.Column("embedding_model", sa.String(length=64), nullable=False),
sa.Column("embedding_version", sa.String(length=16), nullable=False),
sa.Column(
"tsv",
postgresql.TSVECTOR(),
sa.Computed("to_tsvector('simple', text)", persisted=True),
nullable=False,
),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.PrimaryKeyConstraint("chunk_id"),
sa.ForeignKeyConstraint(["entity_id"], ["kb_entity.entity_id"], ondelete="CASCADE"),
)
op.create_index("ix_kb_chunk_entity", "kb_chunk", ["entity_id"])
op.execute(
"CREATE INDEX ix_kb_chunk_embedding_hnsw ON kb_chunk "
"USING hnsw (embedding vector_cosine_ops)"
)
op.execute("CREATE INDEX ix_kb_chunk_tsv_gin ON kb_chunk USING gin (tsv)")
op.create_table(
"kb_asset",
sa.Column("asset_id", sa.BigInteger(), nullable=False),
sa.Column("entity_id", sa.BigInteger(), nullable=False),
sa.Column("asset_type", sa.String(length=32), nullable=False),
sa.Column("cos_key", sa.String(length=255), nullable=False),
sa.Column("spec_check", postgresql.JSONB(), nullable=False),
sa.Column("tryon_ready", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("review_status", sa.String(length=16), nullable=False, server_default=sa.text("'pending'")),
sa.Column("license", sa.String(length=64), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.PrimaryKeyConstraint("asset_id"),
sa.ForeignKeyConstraint(["entity_id"], ["kb_entity.entity_id"], ondelete="CASCADE"),
)
op.create_index("ix_kb_asset_entity", "kb_asset", ["entity_id"])
op.execute(
"CREATE INDEX ix_kb_asset_tryon_ready ON kb_asset (tryon_ready) WHERE tryon_ready"
)
op.create_table(
"kb_color",
sa.Column("color_id", sa.BigInteger(), nullable=False),
sa.Column("season_code", sa.String(length=16), nullable=False),
sa.Column("role", sa.String(length=8), nullable=False),
sa.Column("lab_l", sa.Numeric(6, 2), nullable=False),
sa.Column("lab_a", sa.Numeric(6, 2), nullable=False),
sa.Column("lab_b", sa.Numeric(6, 2), nullable=False),
sa.Column("hex", sa.String(length=7), nullable=False),
sa.Column("name_cn", sa.String(length=64), nullable=False),
sa.PrimaryKeyConstraint("color_id"),
)
op.create_index("ix_kb_color_season", "kb_color", ["season_code"])
op.create_table(
"kb_gap",
sa.Column("gap_id", sa.BigInteger(), nullable=False),
sa.Column("query_text", sa.Text(), nullable=False),
sa.Column("filter_ctx", postgresql.JSONB(), nullable=False),
sa.Column("hit_count", sa.Integer(), nullable=False, server_default=sa.text("1")),
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.PrimaryKeyConstraint("gap_id"),
)
op.create_index("ix_kb_gap_query", "kb_gap", ["query_text"])
def downgrade() -> None:
op.drop_index("ix_kb_gap_query", table_name="kb_gap")
op.drop_table("kb_gap")
op.drop_index("ix_kb_color_season", table_name="kb_color")
op.drop_table("kb_color")
op.execute("DROP INDEX IF EXISTS ix_kb_asset_tryon_ready")
op.drop_index("ix_kb_asset_entity", table_name="kb_asset")
op.drop_table("kb_asset")
op.execute("DROP INDEX IF EXISTS ix_kb_chunk_tsv_gin")
op.execute("DROP INDEX IF EXISTS ix_kb_chunk_embedding_hnsw")
op.drop_index("ix_kb_chunk_entity", table_name="kb_chunk")
op.drop_table("kb_chunk")
op.drop_index("ix_kb_rule_status_priority", table_name="kb_rule")
op.drop_table("kb_rule")
op.drop_index("ix_kb_relation_to", table_name="kb_relation")
op.drop_table("kb_relation")
op.drop_index("ix_kb_entity_type_status", table_name="kb_entity")
op.drop_table("kb_entity")