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 输出
This commit is contained in:
xsl
2026-08-14 17:01:43 +08:00
parent ddec3973ad
commit d5f366fa4c
11 changed files with 656 additions and 4 deletions
+179
View File
@@ -0,0 +1,179 @@
"""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")