项目实践11—全球证件智能识别系统(切换为PostgreSQL数据库)

目录

  • 一、任务概述
  • [二、数据库升级:从 SQLite 到 PostgreSQL](#二、数据库升级:从 SQLite 到 PostgreSQL)
    • [2.1 为什么要换 PostgreSQL?](#2.1 为什么要换 PostgreSQL?)
    • [2.2 在 Ubuntu 上安装 PostgreSQL](#2.2 在 Ubuntu 上安装 PostgreSQL)
    • [2.3 安装适配FastAPI的PostgreSQL驱动库](#2.3 安装适配FastAPI的PostgreSQL驱动库)
    • [2.4 深度改造:PostgreSQL 环境搭建与向量存储优化](#2.4 深度改造:PostgreSQL 环境搭建与向量存储优化)
      • [2.4.1 代码重构:原生向量存储](#2.4.1 代码重构:原生向量存储)
        • [1. 修改 `models.py`:使用 `ARRAY(Float)` 类型](#1. 修改 models.py:使用 ARRAY(Float) 类型)
        • [2. 修改 `feature_extractor.py`:返回 List[float]](#2. 修改 feature_extractor.py:返回 List[float])
        • [3. 修改 `init_db.py`:适配新类型](#3. 修改 init_db.py:适配新类型)
        • [4. 修改 `main.py`:移除 pickle 反序列化](#4. 修改 main.py:移除 pickle 反序列化)
      • [2.4.2 重置迁移与数据初始化](#2.4.2 重置迁移与数据初始化)
      • [2.4.3 全链路测试](#2.4.3 全链路测试)

一、任务概述

在前序的十篇博客中,我们一步步从零构建了一个包含完整前后端的"全球证件智能识别系统"。目前,我们的后端服务运行在开发环境中,使用SQLite这种轻量级的文件数据库,本篇博客将完成数据库的迁移,完成从SQLite到PostgreSQL的过度。

二、数据库升级:从 SQLite 到 PostgreSQL

2.1 为什么要换 PostgreSQL?

虽然SQLite对原型开发很友好,但PostgreSQL在生产环境具有压倒性优势:

  • 高并发支持:采用多进程架构,支持大量客户端同时读写。
  • 数据类型丰富:原生支持数组(Array)和 JSONB,非常适合存储我们的特征向量(无需 pickle 序列化为 bytes,可以直接存为浮点数数组)和结构化 OCR 结果。
  • 生态强大 :配合 pgvector 插件,可以直接在数据库层面进行向量相似度搜索(虽然本项目目前在内存中计算,但未来扩展性极佳)。

2.2 在 Ubuntu 上安装 PostgreSQL

首先,我们需要在开发机(Ubuntu 22.04)上安装 PostgreSQL 数据库服务。

  1. 更新包列表并安装

    bash 复制代码
    sudo apt update
    sudo apt install postgresql postgresql-contrib
  2. 启动服务并设置开机自启

    bash 复制代码
    sudo systemctl start postgresql
    sudo systemctl enable postgresql
  3. 配置用户和数据库

    PostgreSQL 安装后默认会创建一个名为 postgres 的系统用户。我们需要切换到该用户并进入数据库控制台。

    bash 复制代码
    # 切换到 postgres 用户
    sudo -i -u postgres
    
    # 进入数据库控制台
    psql

    postgres=# 提示符下,执行 SQL 命令来设置密码并创建数据库:

    sql 复制代码
    -- 1. 修改默认用户 postgres 的密码 (请将 'mysecretpassword' 替换为你的强密码)
    ALTER USER postgres PASSWORD 'mysecretpassword';
    
    -- 2. 创建项目专用数据库
    CREATE DATABASE card_db;
    
    -- 3. 退出控制台
    \q

    退出 postgres 用户身份:

    bash 复制代码
    exit
  4. 更新环境变量

    最后,为了让代码连接到这个本地数据库,我们需要设置 DATABASE_URL 环境变量。
    在当前终端执行(或添加到 ~/.bashrc):

    bash 复制代码
    export DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/card_db"

2.3 安装适配FastAPI的PostgreSQL驱动库

在 FastAPI 项目中连接 PostgreSQL,需要安装 psycopg2 驱动。

bash 复制代码
pip install psycopg2-binary

接下来需要修改 database.py,使其能够根据环境变量动态切换连接地址。这样既保留了本地开发的灵活性(连本地库),又适应了 Docker 部署(连容器库)。

代码清单:database.py

python 复制代码
import os
from sqlmodel import create_engine, Session

# 从环境变量中读取数据库连接 URL
# 如果环境变量未设置,默认使用 PostgreSQL 的本地连接字符串(开发环境)
# 格式: postgresql://user:password@host:port/dbname
DATABASE_URL = os.getenv(
    "DATABASE_URL", 
    "postgresql://postgres:mysecretpassword@localhost:5432/card_db"
)

# 创建数据库引擎
# 注意:PostgreSQL 不需要 check_same_thread 参数,那是 SQLite 特有的
engine = create_engine(DATABASE_URL, echo=False)

def get_session():
    """
    FastAPI 依赖注入函数,用于获取数据库会话
    """
    with Session(engine) as session:
        yield session

最后,修改Alembic 的配置文件 alembic.ini ,该文件通常包含硬编码的数据库 URL。为了让它也能读取环境变量,我们需要修改 alembic/env.py 文件。

代码清单:alembic/env.py

找到 run_migrations_online 函数,修改 connectable 的获取方式:

python 复制代码
# ... (原有导入)
import os
from database import DATABASE_URL # <-- 导入我们在database.py中定义的URL

# ...

def run_migrations_online() -> None:
    """Run migrations in 'online' mode."""
    
    # --- 修改开始:使用代码中配置的 URL 覆盖 alembic.ini 中的配置 ---
    configuration = config.get_section(config.config_ini_section)
    configuration["sqlalchemy.url"] = DATABASE_URL
    
    connectable = engine_from_config(
        configuration,
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    # --- 修改结束 ---

    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()

这样,无论在什么环境下运行 alembic upgrade head,它都会使用 database.py 中逻辑确定的数据库地址。

2.4 深度改造:PostgreSQL 环境搭建与向量存储优化

在将系统容器化之前,我们需要在本地开发环境(Ubuntu)中基于PostgreSQL,对代码进行一次深度重构:弃用 Python 的 pickle 序列化方式,改用 PostgreSQL 原生的 ARRAY 类型来存储图像特征向量。这将显著提升数据的透明度,并减少编解码开销。

2.4.1 代码重构:原生向量存储

这是本次改造的核心。我们将修改数据模型、特征提取器和检索逻辑,彻底移除 pickle,直接以浮点数数组(Float Array)的形式处理特征向量。

1. 修改 models.py:使用 ARRAY(Float) 类型

我们需要引入 SQLAlchemy 的 PostgreSQL 方言来定义数组列。

代码清单:models.py

python 复制代码
from typing import List, Optional
from sqlmodel import Field, Relationship, SQLModel
# 引入 PostgreSQL 特有的数组类型和 Float 类型
from sqlalchemy import Column, Float
from sqlalchemy.dialects.postgresql import ARRAY

class Country(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    code: str = Field(unique=True)
    certificate_templates: List["CertificateTemplate"] = Relationship(back_populates="country")

class CertificateTemplate(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    description: str = Field()

    # 图像数据保持为 bytes
    image_front_white: bytes = Field(description="正面白光样证图")
    image_front_uv: bytes = Field(description="正面紫外样证图")
    image_back_white: bytes = Field(description="反面白光样证图")
    image_back_uv: bytes = Field(description="反面紫外样证图")

    # --- 核心修改:特征向量改为浮点数数组 ---
    # sa_column=Column(ARRAY(Float)) 告诉数据库这是一个浮点数数组列
    # Python 侧对应的数据类型是 List[float]
    feature_front_white: List[float] = Field(sa_column=Column(ARRAY(Float)))
    feature_front_uv: List[float] = Field(sa_column=Column(ARRAY(Float)))
    feature_back_white: List[float] = Field(sa_column=Column(ARRAY(Float)))
    feature_back_uv: List[float] = Field(sa_column=Column(ARRAY(Float)))

    country_id: Optional[int] = Field(default=None, foreign_key="country.id")
    country: Optional[Country] = Relationship(back_populates="certificate_templates")
2. 修改 feature_extractor.py:返回 List[float]

移除 pickle 序列化,直接返回 Python 列表。

代码清单:feature_extractor.py (仅展示修改的方法)

python 复制代码
# ... (保留 Imports, 注意移除 pickle) ...
# import pickle  <-- 删除此行

class ImageFeatureExtractor:
    # ... (__init__ 保持不变) ...

    def extract_features(self, image_bytes: bytes) -> list[float]:
        """
        接收图像二进制数据,返回浮点数特征列表。
        """
        # "去色"处理等逻辑保持不变
        image = Image.open(io.BytesIO(image_bytes)).convert("L").convert("RGB")
        input_tensor = self.preprocess(image)
        input_batch = input_tensor.unsqueeze(0).to(self.device)

        with torch.no_grad():
            output_features = self.model(input_batch)

        feature_np = output_features.cpu().numpy().flatten()
        
        # --- 修改:直接转换为 Python List 返回 ---
        return feature_np.tolist()
3. 修改 init_db.py:适配新类型

由于 init_db.py 只是负责数据搬运,且 extractor 返回类型已变更为 List[float]models.py 也接受 List[float],因此该文件几乎不需要修改逻辑

注意 :请确保 init_db.py 顶部的导入中不再包含 pickle ,且在 extract_features 返回空值时(例如文件不存在),应赋予默认空列表 [] 而不是 b''

代码片段修正建议:

python 复制代码
# ...
print("      - 正在提取特征向量...")
# 如果图像存在则提取,否则返回空列表 []
feature_front_white = extractor.extract_features(front_white_bytes) if front_white_bytes else []
# ... (其他字段同理)
4. 修改 main.py:移除 pickle 反序列化

在检索逻辑中,从数据库取出的数据已经是 List,我们只需要将其转回 Numpy Array 即可进行余弦相似度计算。

代码清单:main.py (修改 recognize_document 部分)

python 复制代码
# ... (保留 Imports, 注意移除 pickle) ...
# import pickle <-- 删除此行

# ...

@app.post("/api/recognize", ...)
async def recognize_document(...):
    # ...
    
    # 1. 提取待查询图像的特征 (extractor 现在返回 List[float])
    # 我们需要将其转换为 numpy array 以进行数学计算
    query_feature_front = np.array(extractor.extract_features(front_white_bytes))
    query_feature_back = np.array(extractor.extract_features(back_white_bytes))

    # ... (数据库检索逻辑不变) ...

    for template in templates:
        # --- 修改:直接从对象属性获取 List,并转为 Numpy Array ---
        # 数据库已经帮我们把 ARRAY(Float) 转回了 Python List
        template_feature_front = np.array(template.feature_front_white)
        template_feature_back = np.array(template.feature_back_white)

        # 相似度计算逻辑保持不变 (cosine_similarity 接收 numpy array)
        sim_front = cosine_similarity(query_feature_front, template_feature_front)
        sim_back = cosine_similarity(query_feature_back, template_feature_back)
        
        # ... (后续逻辑不变)
    # 5. 二次校验:紫外荧光图像相似度检查
    uv_message = "未发现异常"
    if best_match_template.feature_front_uv and best_match_template.feature_back_uv:
        # 仅在样证模板包含完整的紫外特征时进行比对
        print("正在进行紫外荧光特征二次校验...")
        query_uv_front_bytes = base64.b64decode(request.image_front_uv)
        query_uv_back_bytes = base64.b64decode(request.image_back_uv)
        
        if query_uv_front_bytes and query_uv_back_bytes:
            # 提取待查询图像的紫外特征
            # --- 修改:直接转为 Numpy Array ---
            query_feature_uv_front = np.array(extractor.extract_features(query_uv_front_bytes))
            query_feature_uv_back = np.array(extractor.extract_features(query_uv_back_bytes))
            
            # 反序列化样证的紫外特征
            # --- 修改:直接转为 Numpy Array ---
            template_feature_uv_front = np.array(best_match_template.feature_front_uv)
            template_feature_uv_back = np.array(best_match_template.feature_back_uv)

            # 计算相似度
            sim_uv_front = cosine_similarity(query_feature_uv_front, template_feature_uv_front)
            sim_uv_back = cosine_similarity(query_feature_uv_back, template_feature_uv_back)
            avg_uv_similarity = (sim_uv_front + sim_uv_back) / 2
            
            print(f"紫外图像平均相似度: {avg_uv_similarity:.4f}")
            if avg_uv_similarity <= 0.80:  # 境外证真伪相似度阈值
                uv_message = "该证存疑,请仔细核查"     
        else:
            avg_uv_similarity = 0.85

2.4.2 重置迁移与数据初始化

由于我们彻底改变了底层数据类型(从 Bytes 变为 Array),且数据库引擎从 SQLite 换成了 PostgreSQL,建议重新生成迁移文件以避免兼容性问题。

  1. 清理旧的迁移记录

    删除项目目录下的 alembic/versions 文件夹内的所有 .py 文件。

    删除旧的 database.db 文件(如果存在)。

  2. 生成新的迁移脚本

    bash 复制代码
    alembic revision --autogenerate -m "init_postgres_array"

    然后在生成的py文件头部添加代码:

    bash 复制代码
    import sqlmodel
  3. 应用迁移到 PostgreSQL

    bash 复制代码
    alembic upgrade head

    此时,Alembic 会在 card_db 中创建表,且特征列的类型为 double precision[]

  4. 初始化数据

    运行脚本,将特征向量计算并以数组形式写入 PostgreSQL。

    bash 复制代码
    python init_db.py

2.4.3 全链路测试

一切就绪,让我们验证这个更先进的数据库架构。

  1. 启动后端

    bash 复制代码
    uvicorn main:app --host 0.0.0.0 --port 8001
  2. 启动客户端并测试

    打开 Qt 客户端,选择一个国外证件(例如"韩国驾照"),点击识别。

观察点

  • 后端日志:不应出现 pickle 相关的错误。

  • 识别速度:理论上与之前持平,但省去了序列化步骤,CPU 开销略有降低。

  • 数据库检查 :使用 psql 或 DBeaver 查看数据库:
    参照前面的方法切换到当前的 postgres=# 提示符下,输入以下命令并回车:
    \c card_db,然后使用\dt命令可以查看所有的表。
    最后输入下面的命令:

    sql 复制代码
    SELECT feature_front_white FROM certificatetemplate LIMIT 1;

    应该能看到清晰的浮点数数组 {0.123, -0.456, ...},而不是乱码般的二进制数据。

至此,我们成功完成了后端架构向生产级数据库 PostgreSQL 的迁移,并实现了特征向量的原生存储。


相关推荐
Heyxy4 小时前
RobustMerge—— 无训练的 PEFT 模型融合方法,从低秩分解视角揭示方向鲁棒性对 PEFT 融合的作用
人工智能·深度学习·机器学习·大模型
Sherlock Ma4 小时前
AI大模型面试题集锦:(1)基础入门题
人工智能·pytorch·自然语言处理·大模型·跳槽·机器翻译·改行学it
不会计算机的g_c__b4 小时前
HuggingGPT深度解析:当ChatGPT遇上HuggingFace,打造AI世界的“万能工具箱”
人工智能·chatgpt
+电报dapp1294 小时前
波场链DAPP智能合约系统开发:解锁Web3.0时代的价值新范式
大数据·人工智能·web3·去中心化·区块链·智能合约·信任链
Freshman小白4 小时前
《人工智能与创新》网课答案2025
人工智能·学习·答案·网课答案
阿湯哥4 小时前
当前主流AI Agent框架深度分析报告
人工智能
陈喜标bill4 小时前
S2B2C私域会员电商如何重构企业经营逻辑
大数据·人工智能·重构
donecoding4 小时前
掌握 :focus-within,让你的AI对话输入体验更上一层楼!
前端·人工智能
newrank_kk4 小时前
AI 搜索时代新战场:智汇GEO 如何重构品牌 AI 形象管理规则
人工智能·重构