目录
- 一、任务概述
- [二、数据库升级:从 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 反序列化)
- [1. 修改 `models.py`:使用 `ARRAY(Float)` 类型](#1. 修改
- [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 数据库服务。
-
更新包列表并安装:
bashsudo apt update sudo apt install postgresql postgresql-contrib -
启动服务并设置开机自启:
bashsudo systemctl start postgresql sudo systemctl enable postgresql -
配置用户和数据库 :
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用户身份:bashexit -
更新环境变量 :
最后,为了让代码连接到这个本地数据库,我们需要设置
DATABASE_URL环境变量。
在当前终端执行(或添加到~/.bashrc):bashexport 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,建议重新生成迁移文件以避免兼容性问题。
-
清理旧的迁移记录 :
删除项目目录下的
alembic/versions文件夹内的所有.py文件。删除旧的
database.db文件(如果存在)。 -
生成新的迁移脚本:
bashalembic revision --autogenerate -m "init_postgres_array"然后在生成的py文件头部添加代码:
bashimport sqlmodel -
应用迁移到 PostgreSQL:
bashalembic upgrade head此时,Alembic 会在
card_db中创建表,且特征列的类型为double precision[]。 -
初始化数据 :
运行脚本,将特征向量计算并以数组形式写入 PostgreSQL。
bashpython init_db.py
2.4.3 全链路测试
一切就绪,让我们验证这个更先进的数据库架构。
-
启动后端:
bashuvicorn main:app --host 0.0.0.0 --port 8001 -
启动客户端并测试 :
打开 Qt 客户端,选择一个国外证件(例如"韩国驾照"),点击识别。
观察点:
-
后端日志:不应出现 pickle 相关的错误。
-
识别速度:理论上与之前持平,但省去了序列化步骤,CPU 开销略有降低。
-
数据库检查 :使用
psql或 DBeaver 查看数据库:
参照前面的方法切换到当前的 postgres=# 提示符下,输入以下命令并回车:
\c card_db,然后使用\dt命令可以查看所有的表。
最后输入下面的命令:sqlSELECT feature_front_white FROM certificatetemplate LIMIT 1;应该能看到清晰的浮点数数组
{0.123, -0.456, ...},而不是乱码般的二进制数据。
至此,我们成功完成了后端架构向生产级数据库 PostgreSQL 的迁移,并实现了特征向量的原生存储。