"""答案库长出向量列 —— 安全 FAQ 的语义检索(RAG)底座. Revision ID: 561 Revises: 560 员工说「这个搜索很蠢」,说的是对的,而且原因是结构性的:答案库里的每一条 都是按**客户安全问卷**的口吻写的(「贵公司是否部署数据防泄漏(DLP)方案?」), 员工问的却是「我能用个人电脑写公司代码吗」。两句话没有任何一个公共子串, 所以 LIKE 找不到,二字切词也只能靠巧合命中。 这三列是把「靠字面重合」换成「靠语义邻近」的最小改动,形状与 ``support_kb_ articles``(迁移 306)完全一致,理由也一样: * ``embedding`` 存**可移植的 JSON 浮点数组**,不是 TiDB VECTOR —— 本地 MySQL 跑的那套测试因此一行都不用改,排序用 Python 余弦;库只有几百行 精编条目,全表打分比维护一个向量索引便宜得多。 * ``embedding_model`` 记下产出这条向量的模型 id。换模型时靠它把旧向量挑 出来重嵌;没有这列,一次模型切换会留下一批永远不会被重算、又和新查询 向量不在同一空间里的僵尸向量。 * ``embedded_at`` 是「什么时候嵌的」,给回填任务和管理台看进度。 三列都可空:没有向量 = 语义检索这一路不可用,词法检索照常工作。这条降级 路径不是补丁,是设计 —— 没配 LLM Key 的环境(以及所有测试)必须仍然能搜。 幂等 + 可重跑(按列存在性守卫)。 """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "561" down_revision = "560" branch_labels = None depends_on = None TABLE = "auditor_answer_library_entries" COLUMNS = ( ("embedding", lambda: sa.Column("embedding", sa.JSON(), nullable=True)), ( "embedding_model", lambda: sa.Column("embedding_model", sa.String(length=64), nullable=True), ), ( "embedded_at", lambda: sa.Column("embedded_at", sa.DateTime(timezone=True), nullable=True), ), ) def _column_exists(bind, table: str, column: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t " "AND COLUMN_NAME = :c" ), {"t": table, "c": column}, ).scalar() is not None def upgrade() -> None: bind = op.get_bind() for name, factory in COLUMNS: if not _column_exists(bind, TABLE, name): op.add_column(TABLE, factory()) def downgrade() -> None: bind = op.get_bind() for name, _ in COLUMNS: if _column_exists(bind, TABLE, name): op.drop_column(TABLE, name)