"""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")