嵌入式数据库的最大特点是无需独立服务进程,数据直接落在本地文件,适合桌面应用、移动端和边缘设备。SQLite作为关系型嵌入式数据库的代表,用SQL管理结构化数据;Chroma则专注于向量存储和相似度检索。二者结合,可以构建一个既支持精确条件过滤又支持语义搜索的本地数据层。本文通过一个完整实战项目,展示如何用Python整合SQLite和Chroma,实现文档存储、向量索引和联合查询。

一、SQLite与Chroma的定位差异和协作场景
SQLite是一个轻量级关系型数据库,所有表、索引和事务都存储在一个扩展名为.db的文件中,支持完整的SQL语法。它适合保存具有明确结构的数据,例如文章标题、分类、时间戳、用户ID等字段。由于没有网络通信开销,读写速度在本地场景下非常快,并且可以通过事务保证一致性。
Chroma则是一个专为向量检索设计的嵌入式数据库,它将文本转换成高维向量,并用近似最近邻算法快速找到语义上最相近的内容。Chroma本身不擅长处理复杂的关系查询,比如按分类筛选并排序,但它可以轻松回答“哪些文档与这句话意思最接近”这类模糊问题。
在一个典型的知识库或检索增强生成应用中,我们既需要精确的结构化过滤(例如只查找某一分类下的内容),又需要语义级别的相似度匹配。单独使用SQLite时,模糊搜索只能靠LIKE语句,对同义词和上下文理解非常有限;单独使用Chroma时,条件过滤虽然可以通过元数据实现,但复杂的关联查询和事务处理不如SQLite方便。因此让两者协作,SQLite存元数据,Chroma存向量,通过统一的ID关联,可以发挥各自优势。
二、环境准备与初始化代码
在开始编码之前,需要安装两个Python包:sqlite3是Python标准库,无需额外安装;Chroma可以通过pip install chromadb安装。如果使用内置的嵌入模型,Chroma会自动下载相关依赖,也可以指定使用sentence-transformers中的模型。
本项目把数据存放在当前目录下,SQLite文件命名为app.db,Chroma持久化目录命名为chroma_db。初始化阶段需要完成三件事:连接SQLite并创建表、初始化Chroma客户端并创建集合、准备好嵌入函数。下面先创建SQLite表结构。
import sqlite3
conn = sqlite3.connect("app.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
category TEXT,
content TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
这段代码创建了一张documents表,其中id是主键,用来和Chroma中的向量记录对应。title和category属于结构化元数据,content保存原文内容,方便后续展示或二次处理。这里使用文本主键而不是自增整数,是为了让两边ID完全一致,减少映射逻辑。
接下来初始化Chroma。Chroma支持两种客户端:Client用于临时内存模式,PersistentClient用于持久化到磁盘。生产环境应使用后者,避免程序退出后向量丢失。嵌入函数选择了常用的all-MiniLM-L6-v2模型,它在速度和效果之间平衡得较好。
import chromadb
from chromadb.utils import embedding_functions
embedding_func = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="all-MiniLM-L6-v2"
)
client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(
name="docs_collection",
embedding_function=embedding_func
)
上面的get_or_create_collection方法保证集合存在,如果已经创建过则直接复用。集合名称需要全局唯一,实际项目中可以按业务模块划分多个集合。需要注意的是,嵌入函数一旦指定,后续添加和查询时必须使用相同的嵌入函数,否则向量维度不一致会报错。
三、写入数据与联合查询流程
数据的写入流程分为两步:先将结构化信息写入SQLite,再将文本向量写入Chroma。两步之间最好放在同一个应用逻辑中,并使用相同的doc_id。如果其中一步失败,可以通过捕获异常进行补偿或回滚,避免两边数据不一致。
doc_id = "doc_001"
title = "向量数据库入门"
category = "数据库"
content = "本文介绍向量数据库的基本概念和应用场景。"
cursor.execute(
"INSERT INTO documents (id, title, category, content) VALUES (?, ?, ?, ?)",
(doc_id, title, category, content)
)
conn.commit()
collection.add(
ids=[doc_id],
documents=[content],
metadatas=[{"title": title, "category": category}]
)
在Chroma的add方法中,ids、documents和metadatas都接受列表,可以一次性批量写入多条数据。元数据字段会随向量一起存储,查询时除了返回向量距离,也会返回对应的元数据,方便在结果中直接展示标题和分类。
联合查询的典型场景有两种:一是先根据条件在SQLite中筛选出候选ID,再拿这些ID去Chroma里做向量检索;二是先通过向量相似度获得最相关的ID列表,再回SQLite查完整的业务字段。前者适合用户同时给了分类条件和搜索词的情况,后者适合纯语义搜索。下面演示第二种方式。
results = collection.query(
query_texts=["什么是嵌入式数据库"],
n_results=3
)
ids = results["ids"][0]
placeholders = ",".join("?" for _ in ids)
cursor.execute(
f"SELECT id, title, category FROM documents WHERE id IN ({placeholders})",
ids
)
rows = cursor.fetchall()
for row in rows:
print(row)
这段代码先用自然语言问题查询Chroma,得到最相近的3个文档ID,然后构造一个带有多个问号占位符的SQL语句,从SQLite中取出对应的标题和分类。这样用户在界面上看到的就是可读的结构化数据,而不是向量ID或原始文本片段。实际项目中还可以根据相似度分数设置阈值,过滤掉相关度过低的结果。
四、持久化配置与性能调优要点
SQLite和Chroma都支持持久化,但两者的底层存储机制不同。SQLite的数据全部写入单个文件app.db,可以方便地备份和迁移。Chroma的持久化目录chroma_db会包含索引文件、元数据文件和向量数据文件,整体体积通常比SQLite大很多,尤其是使用高维向量时。因此部署时需要为向量目录预留足够的磁盘空间。
性能方面,SQLite在单表数据量达到百万级时依然表现良好,但要注意给常用查询字段建立索引。比如经常按category过滤,就应该在category列上创建索引。Chroma的性能主要取决于嵌入模型的推理速度和近似最近邻索引的效率。对于大规模数据,建议使用批量添加而不是逐条写入,并考虑使用更轻量的嵌入模型来降低计算开销。
另一个常见的调优点是查询数量n_results的设置。如果设置过大,Chroma需要返回更多候选向量,响应时间会增加;设置过小则可能漏掉相关文档。可以根据业务需要先返回一个稍大的候选集,再通过SQLite条件过滤或分数阈值进行二次筛选。此外,如果数据更新频繁,需要定期重建Chroma索引,避免删除操作导致索引碎片化。总之,SQLite与Chroma的结合方案在本地和嵌入式环境中非常实用,但要获得稳定性能,还需要针对具体数据规模和查询模式进行参数调整。