Memory向量记忆系统2-SQLite

上一章讲了基础知识,文本可以转成向量,可以根据query进行召回。那在现实场景中怎么存储,怎么搜索呢?

向量数据库对比

下面是一些常用的向量数据库,我VikingDB用的多一些。

名称 类型 部署方式 规模上限 核心优势 核心劣势 适用场景
FAISS 算法库 本地 / 嵌入 千万级 最快最灵活,算法最全,性能天花板 不是数据库,无持久化,全要自己封装 研究、原型、嵌入式场景
Chroma 轻量开源 本地 / 嵌入 百万级 5 行代码跑通,Python 友好,LangChain 标配 单机,功能少,性能一般 原型验证、小项目、个人开发
Qdrant 开源 自部署 / SaaS 亿级 Rust 写的,单机性能极强,混合检索好 分布式不如 Milvus 成熟,国内社区小 中大型项目,追求单机性能
pgvector PG 扩展 自部署 千万级 不用学新库,SQL 直接查,事务 / 权限全有 性能不如专用向量库,规模受 PG 限制 已有 PG 栈,小中型项目
Weaviate 开源 自部署 / SaaS 亿级 知识图谱 + 向量一体化,GraphQL 灵活 纯向量性能一般,国内用户少 知识图谱 + RAG、复杂语义查询
Milvus 开源 自部署 十亿级 + 分布式最强,功能最全,国内社区活跃 部署复杂,组件多,运维成本高 超大规模、企业级生产环境
Pinecone 闭源 SaaS 全托管 十亿级 + 零运维,自动扩容,稳定有 SLA 贵,闭源,数据在海外有合规风险 不想运维、快速上线、海外项目
VikingDB 云服务 火山引擎 SaaS 万亿级 字节自研,千亿级验证,混合检索强,内置豆包 embedding 绑定火山生态,开源版能力有限 国内企业、大规模场景、火山生态
Elasticsearch 搜索引擎 自部署 / 云 亿级 全文 + 向量混合检索最强,已有 ES 栈零成本接入 8.0 才支持 ANN,纯向量性能一般 已有 ES、需要全文 + 语义混合检索
ClickHouse OLAP 数据库 自部署 亿级 SQL 分析能力极强,向量化引擎快,和数仓集成好 向量功能新(25.8 才 GA),生态不如专用库 已有 CH 数仓、需要分析 + 向量检索
Redis Vector 缓存 / 数据库 自部署 / 云 千万级 速度极快,已有 Redis 栈直接用 规模有限,功能简单 缓存 + 轻量向量、已有 Redis 栈
SQLite + sqlite-vec 嵌入式扩展 十万级 零依赖、超轻量、全平台、WASM 支持 无 ANN 索引,规模大了慢 移动端、嵌入式、本地小工具、浏览器端

SQLite

安装

OpenClaw 选择 SQLite 作为存储引擎,那就试试怎么用吧。配合两个关键扩展

  • sqlite-vec:向量搜索扩展,支持余弦相似度计算,实现高效的 ANN(近似最近邻)检索。
  • FTS5:SQLite 内置的全文搜索引擎,支持 BM25 排序算法。

OpenClaw用SQLite主要是为了Local First。

shell 复制代码
#是否安装了,我mac上自带了
sqlite3 --version
#没有的话就安装一下
brew install sqlite
#安装 sqlite-vec 扩展。mac要在虚拟环境里安装。
pip3 install sqlite-vec 
# 检查有没有fts5
sqlite3 :memory: "CREATE VIRTUAL TABLE t USING fts5(content);"

BM25排序算法

BM25(Best Match 25)是全文搜索里最经典的相关性排序算法,用来算"查询词和文档的相关程度",分数越低越相关(SQLite FTS5 里就是这样)。


📐 BM25 公式

核心公式长这样:

看着复杂,拆开来看就三部分:


🔍 拆解成大白话

第一部分:IDF(逆文档频率)

意思越稀有的词,权重越高。

  • "的"、"是"这种到处都有的词 → IDF 低,几乎不加分
  • "量子力学"这种很少见的词 → IDF 高,匹配上了加很多分
第二部分:词频(TF)的饱和曲线

意思词出现次数越多越相关,但有上限,不会无限涨。

  • 出现 1 次 → 加很多分
  • 出现 5 次 → 比 1 次分高,但不是 5 倍
  • 出现 100 次 → 和出现 20 次差不多,饱和了

对比 TF-IDF:TF-IDF 是线性的,出现 100 次就是 100 倍分,不合理。BM25 是曲线,越往上越平。

第三部分:文档长度归一化

意思长文档要惩罚。

  • 文档越长,词出现的概率天然就高,不公平
  • 同样出现 5 次"苹果",在 100 字的短文里比在 10000 字的长文里更有意义
  • 文档越长,分母越大,整体分数越低(惩罚)

⚙️ 两个关键参数

参数 常用值 作用
k1 1.2 ~ 2.0 控制词频饱和速度。k1 越小,越快饱和
b 0.75 控制文档长度惩罚强度。b=0 不惩罚,b=1 全惩罚

SQLite FTS5 默认值:k1=1.2, b=0.75,是行业标准值,一般不用改。


📊 举个例子

假设查询是 "苹果",有三个文档:

文档 长度 "苹果"出现次数 BM25 分数 排名
A(短文) 100字 3次 1.2 🥇 最相关
B(长文) 5000字 10次 2.5 🥈
C(中等) 500字 1次 3.8 🥉

注意:SQLite FTS5 的 bm25() 返回的是距离,越小越相关。上面的分数是反过来的概念。

为什么 A 排第一?

  • 虽然出现次数少,但文档短,"浓度"高
  • BM25 认为短文档里出现 3 次,比长文档里出现 10 次更能说明相关

🆚 BM25 vs TF-IDF

维度 TF-IDF BM25
词频关系 线性(出现10次=10倍分) 饱和曲线(有上限)
文档长度 不考虑 / 简单归一化 专门的长度归一化公式
参数 k1、b 可调
效果 基础可用 更精准,工业界标准

一句话:BM25 是 TF-IDF 的进化版,考虑了词频饱和和文档长度,排序更合理。


💡 在 SQLite FTS5 里怎么用

sql 复制代码
-- 默认用法,按 bm25 排序(分数越小越相关)
SELECT title, bm25(docs_fts) as score
FROM docs_fts
WHERE docs_fts MATCH '苹果'
ORDER BY score;

-- 自定义参数(k1=1.5, b=0.8),一般不用改
SELECT title, bm25(docs_fts, 1.5, 0.8) as score
FROM docs_fts
WHERE docs_fts MATCH '苹果'
ORDER BY score;

🎯 总结

BM25 的核心思想就三句话:

  1. 稀有词权重高(IDF)
  2. 词频有上限(饱和曲线)
  3. 长文档要惩罚(长度归一化)

它是 20 多年前发明的算法,但至今仍是全文搜索的工业标准,Elasticsearch、SQLite FTS5、Lucene 等默认都是 BM25。

实战

python 复制代码
import sqlite3
import sqlite_vec
import struct
import numpy as np

db = sqlite3.connect("hybrid_test.db")
db.enable_load_extension(True)
sqlite_vec.load(db)
db.enable_load_extension(False)

# 建表
db.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5(
    title,
    content,
    content_rowid='id'
)
""")

db.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS docs_vec USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[8]  -- 用8维演示,实际用1536维
)
""")

# ========== 写入测试数据 ==========
# 注意:FTS5 默认按空格分词,中文要手动分词后用空格隔开
# 这里先用英文+数字演示,确保能搜到
docs = [
    {"id": 1, "title": "apple fruit", "content": "apple is a red fruit sweet"},
    {"id": 2, "title": "banana fruit", "content": "banana is yellow fruit tasty"},
    {"id": 3, "title": "car vegetable", "content": "car is not a vegetable"},
]

for doc in docs:
    # 写入全文索引
    db.execute("""
        INSERT INTO docs_fts(rowid, title, content) VALUES (?, ?, ?)
    """, (doc["id"], doc["title"], doc["content"]))
    
    # 写入向量(随机生成8维向量)
    vec = np.random.rand(8).astype(np.float32)
    db.execute("""
        INSERT INTO docs_vec(id, embedding) VALUES (?, ?)
    """, (doc["id"], struct.pack("8f", *vec)))

db.commit()

# ========== 测试1:全文搜索 ==========
print("=== 全文搜索 'fruit' ===")
fts_results = db.execute("""
    SELECT rowid, title, content, bm25(docs_fts) as score
    FROM docs_fts 
    WHERE docs_fts MATCH 'fruit' 
    ORDER BY score
    LIMIT 10
""").fetchall()

for r in fts_results:
    print(f"  ID:{r[0]} | {r[1]} | score:{r[3]:.4f}")

print(f"  共 {len(fts_results)} 条结果\n")

# ========== 测试2:向量搜索 ==========
print("=== 向量搜索(随机query) ===")
query_vec = np.random.rand(8).astype(np.float32)
vec_results = db.execute("""
    SELECT id, distance as score 
    FROM docs_vec 
    WHERE embedding MATCH ? 
    ORDER BY distance 
    LIMIT 10
""", (struct.pack("8f", *query_vec),)).fetchall()

for r in vec_results:
    print(f"  ID:{r[0]} | distance:{r[1]:.4f}")

print(f"  共 {len(vec_results)} 条结果\n")

db.close()
print("✅ 测试完成!")

执行结果

shell 复制代码
(sqlite) ➜  sqlite python3 test.py
=== 全文搜索 'fruit' ===
  ID:2 | banana fruit | score:-0.0000
  ID:1 | apple fruit | score:-0.0000
  共 2 条结果

=== 向量搜索(随机query) ===
  ID:3 | distance:0.7758
  ID:1 | distance:1.1480
  ID:2 | distance:1.5675
  共 3 条结果

✅ 测试完成!

命令行执行

shell 复制代码
# 第一步:终端里打开数据库,如果不存在会自动创建
$ sqlite3 docs_fts.db

# 第二步:进入 sqlite 后,看有哪些表
sqlite> .tables
docs_fts  docs_vec

# 第三步:看表结构
sqlite> .schema docs_fts
CREATE VIRTUAL TABLE docs_fts USING fts5(content, content_rowid='id');

# 第四步:查数据
sqlite> .headers on
sqlite> .mode column
sqlite> SELECT * FROM docs_fts LIMIT 5;
相关推荐
阳光是sunny1 小时前
LangGraph高级教程:Multi Schema多状态管理详解
前端·人工智能·后端
神奇霸王龙2 小时前
Gemini CLI 中转站配置使用教程
人工智能·ai·ai作画·aigc·ai编程·gemini·goolge
阳光是sunny2 小时前
LangGraph实战教程:状态管理与`graph.invoke`入参深度解析
前端·人工智能·后端
神奇小汤圆2 小时前
深入解析 Flink Kafka Connector:原理、配置与最佳实践
前端·后端
神奇小汤圆3 小时前
在 Nacos 点了下线,为什么流量还是打到了停机的机器上?
后端
KaneLogger3 小时前
花了2天写了个全平台的技能管理工具
aigc·agent·ai编程
阳光是sunny3 小时前
LangGraph实战教程:一文搞懂图的状态(State)管理
前端·人工智能·后端
atbigapp.com3 小时前
一次踩坑如何变成团队记忆:智忆 Capture → Review → Recall 实战
aigc·ai编程
whyfail3 小时前
前端学 Spring Boot(5):从“你是谁”到“服务真的上线了”
前端·spring boot·后端