构建生产级 AI Agent 记忆系统:OpenSearch 向量检索引擎与主流向量数据库全方位对比
本文探讨了如何利用OpenSearch的KNN功能为AI Agent构建长期记忆系统。OpenSearch通过融合KNN向量检索和BM25关键词检索,在搜索引擎和向量数据库之间找到平衡点。文章详细解析了OpenSearch KNN的发展阶段和配置方法,重点介绍了Index Mapping设计,包括关键字段如user_id、category、memory_vector等的设置。同时提供了Python
1. 引言
LLM 的每一次调用都是无状态的。无论上一轮对话中用户透露了多少信息,下一次请求到来时,它依然"一片空白"。让 AI Agent 具备记忆能力,是每一个 RAG 应用必须直面的核心工程问题。
记忆系统解决两个维度的问题:
| 维度 | 关注点 | 特征 |
|---|---|---|
| 短期记忆(STM) | “刚才聊了什么” | 高频读写、微秒级延迟、会话级生命周期 |
| 长期记忆(LTM) | “用户是谁、偏好是什么” | 持久化存储、海量数据、语义检索、需过滤与更新 |
短期记忆用 Redis 解决,业界共识。长期记忆的选型才是真正的分水岭——用 OpenSearch KNN?用 Elasticsearch KNN?还是上专用向量数据库(pgvector、Milvus、Qdrant、Weaviate)?
OpenSearch 原生的 KNN + BM25 混合检索,加上 Faiss 引擎带来的多索引类型与量化能力,让它在"搜索引擎"和"向量数据库"两个角色之间找到了一个独特的位置。本文从实战配置和数据对比两个角度,给出完整的参考。
2. OpenSearch KNN 实战
2.1 向量检索的三个发展阶段
- 1.x - 2.0:KNN 以独立插件形式存在,底层可选 Faiss 或 NMSLIB,支持 HNSW 和 IVF
- 2.4+:Faiss 成为推荐引擎,引入 PQ/SQ 量化压缩
- 2.11+(实验性)、2.12+(GA):引入 RRF(Reciprocal Rank Fusion)和
hybrid查询,一个请求融合 KNN 向量结果和 BM25 关键词结果——这是 OpenSearch 区别于绝大多数专用向量数据库的核心能力 - 2.12+:新增 Lucene KNN 引擎,无需 Faiss 插件即可使用向量检索,简化部署
本文示例基于 OpenSearch 2.12+。
2.2 Index Mapping:逐字段解析
PUT /user_long_term_memory
{
"settings": {
"index.knn": true,
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"user_id": {
"type": "keyword"
},
"category": {
"type": "keyword"
},
"memory_text": {
"type": "text",
"analyzer": "standard"
},
"memory_vector": {
"type": "knn_vector",
"dimension": 1024,
"method": {
"name": "hnsw",
"space_type": "cosinesimil",
"engine": "faiss",
"parameters": {
"ef_construction": 128,
"m": 16
}
}
},
"timestamp": {
"type": "date"
},
"importance": {
"type": "float"
}
}
}
}
settings.index.knn: true
启用 KNN 插件。不写这一行,knn_vector 字段直接报错。
user_id(keyword)
精确匹配用户,每次检索的必经过滤条件。
category(keyword)—— 记忆更新机制的关键
标记记忆的语义维度,如 "programming_lang"、"hobby"、"location"。这是解决"偏好变更"问题的核心设计:
- 编程语言偏好从 Python 变为 Java → 更新
category: "programming_lang" - 运动爱好从 跑步变为篮球 → 更新
category: "hobby"
两个维度各自独立更新,互不干扰。如果把它们塞进同一个 category,检索时就会同时召回矛盾的旧信息。
memory_text(text)
记忆的自然语言描述。两个用途:支撑 BM25 关键词检索 + 作为 LLM Prompt 的上下文文本。
memory_vector(knn_vector)—— 核心字段
| 参数 | 取值 | 说明 |
|---|---|---|
type |
knn_vector |
OpenSearch 的向量字段类型 |
dimension |
1024 | 必须与 Embedding 模型一致。本文以 BGE-M3 为例(1024 维) |
method.name |
hnsw |
也可选 ivf,大规模下更快更省内存但召回略低 |
method.space_type |
cosinesimil |
余弦相似度,与归一化 Embedding 对齐。也可 l2 或 innerproduct |
method.engine |
faiss |
推荐。Meta 开源,支持 HNSW/IVF/PQ/SQ。备选 nmslib、lucene |
parameters.m |
16 | 每个节点最大连接数。范围 2-100。百万级取 16-32 |
parameters.ef_construction |
128 | 构建时的搜索宽度。范围 100-2000。越大索引越准但构建越慢 |
m16→32:内存 +80%,QPS +25-40%。ef_construction128→256:构建时间翻倍,recall@10 从 93%→97%。
维度约束速查:
| 引擎 | 维度上限 |
|---|---|
| Faiss | 16,384 |
| NMSLIB | 16,384 |
| Lucene KNN(2.12+) | 4,096 |
常见模型维度:OpenAI ada-002(1536)、text-embedding-3-small(512/1536)、text-embedding-3-large(默认 3072)、BGE-M3(1024)、BGE-large-zh(1024)、all-MiniLM-L6-v2(384)。
创建 Index 后
dimension不可修改,只能删库重建。 先用model.encode("test").shape确认维度。
timestamp(date):时间衰减排序 + ISM 定时清理。
importance(float):LLM 评估的 0-1 重要性得分,高频记忆可动态上调。
2.3 向量化与入库
from opensearchpy import OpenSearch
from sentence_transformers import SentenceTransformer
client = OpenSearch(
hosts=[{'host': 'localhost', 'port': 9200}],
http_auth=('admin', 'admin'),
use_ssl=False,
verify_certs=False
)
model = SentenceTransformer("BAAI/bge-m3") # 1024 维,与 Mapping 一致
memories = [
{
"user_id": "user_123",
"category": "programming_lang",
"text": "用户主要使用 Java 进行后端开发,对 Spring Boot 框架非常熟悉",
"importance": 0.85
},
{
"user_id": "user_123",
"category": "hobby",
"text": "用户热衷于打篮球,每周去球馆至少两次",
"importance": 0.90
},
{
"user_id": "user_123",
"category": "location",
"text": "用户住在北京海淀区,通勤主要靠地铁",
"importance": 0.70
}
]
for m in memories:
# 1. 删除同 user_id + 同 category 下的旧记忆
client.delete_by_query(
index="user_long_term_memory",
body={
"query": {
"bool": {
"must": [
{"term": {"user_id": m["user_id"]}},
{"term": {"category": m["category"]}}
]
}
}
}
)
# 2. 生成新向量
vector = model.encode(m["text"]).tolist()
# 3. 写入
client.index(
index="user_long_term_memory",
body={
"user_id": m["user_id"],
"category": m["category"],
"memory_text": m["text"],
"memory_vector": vector,
"timestamp": "2025-06-03T10:00:00Z",
"importance": m["importance"]
}
)
为什么先删后插?
OpenSearch 是文档存储,不强制字段唯一约束——即使两天内针对同一个 (user_id="user_123", category="programming_lang") 写入两条文档,OpenSearch 也照单全收。SQL 里你可以靠 UNIQUE(user_id, category) 让 INSERT 直接报错或者用 ON CONFLICT UPDATE 自动覆盖,但 OpenSearch 没有这个机制。
这样导致的问题是一个具体场景就能看清——用户的编程语言偏好变了:
| 时间 | 写入的文档内容 |
|---|---|
| 周一 | {user_id: "123", category: "programming_lang", memory_text: "用户主要使用 Python"} |
| 周四 | {user_id: "123", category: "programming_lang", memory_text: "用户主要使用 Java"} |
如果周四只插入不删除,索引里两条都在。检索时 KNN 对两条文档都可能召回,注入 LLM 的上下文里就会同时出现"用户用 Python"和"用户用 Java"——LLM 看到这种矛盾信息,要么胡说八道,要么追问澄清,两种都不是你想要的结果。
所以做法很直接:写入新记忆之前,先把同 (user_id, category) 的旧记录全部删掉。 每个语义维度永远只保留用户的最新状态。
有一个需要知道的技术细节:delete_by_query 在 OpenSearch 中默认是异步的——API 返回成功只代表删除操作已提交,不代表旧文档已经从索引中物理清除了(默认每秒刷新一次,即 refresh_interval=1s)。如果你在同一秒内连续更新同一个 (user_id, category),极短时间内可能还能搜到旧文档。对于 AI Agent 记忆这种非强一致性场景,亚秒级的延迟完全可接受;如果你的场景对实时性要求极高,可以在 delete_by_query 后加 ?refresh=true 强制立即刷新,代价是额外的索引开销。
2.4 混合检索:KNN + BM25
纯向量检索有盲区——专有名词、精确短语可能遗漏。混合检索就是为补上这块短板。
场景:用户问"推荐个周末活动",历史记忆中"打篮球"靠语义匹配,"周末"靠关键词匹配。两者互补。
方式一:Bool 查询融合(OpenSearch 2.x 全版本通用)
将 knn 和 match 同时放进 bool 的 should 子句,OpenSearch 对两者各自算分后再叠加。这是最简单的混合检索写法:
GET /user_long_term_memory/_search
{
"size": 5,
"query": {
"bool": {
"must": [
{ "term": { "user_id": "user_123" } }
],
"should": [
{
"knn": {
"memory_vector": {
"vector": [0.12, -0.34, 0.56, "...1024维向量..."],
"k": 10
}
}
},
{
"match": {
"memory_text": {
"query": "周末 活动 推荐"
}
}
}
],
"minimum_should_match": 1
}
},
"_source": ["memory_text", "category", "importance", "timestamp"]
}
方式一的局限:BM25 和 KNN 的分数处于完全不同的量级(BM25 可能几十,KNN 余弦相似度在 0-1 之间),简单叠加意味着 BM25 分数会"淹没"向量分数,实际上退化为关键词优先。
方式二:RRF 融合(OpenSearch 2.11+)
RRF 不关心原始分值大小,只看排名,彻底解决了量级不一致的问题。OpenSearch 2.11+ 通过 hybrid 查询原生支持:
GET /user_long_term_memory/_search
{
"size": 5,
"query": {
"hybrid": {
"queries": [
{
"match": {
"memory_text": {
"query": "周末 活动 推荐"
}
}
},
{
"knn": {
"memory_vector": {
"vector": [0.12, -0.34, 0.56, "...1024维向量..."],
"k": 10
}
}
}
]
}
},
"_source": ["memory_text", "category", "importance", "timestamp"]
}
RRF 融合公式:score(doc) = Σ 1/(k + rank_i(doc)),k 常取 60,rank_i 是文档在第 i 个检索器中的排名。
拿前面的场景具体走一遍。用户问"推荐个周末活动",用户历史中有以下 6 条长期记忆(实际会更多,这里简化):
| ID | memory_text | category |
|---|---|---|
| ① | 用户热衷于打篮球,每周去球馆至少两次 | hobby |
| ② | 用户周末经常去朝阳公园跑步,喜欢户外运动 | hobby |
| ③ | 用户对 Python 非常熟悉,主要做后端开发 | programming_lang |
| ④ | 用户上个月报名了周末的攀岩体验课,觉得很有意思 | hobby |
| ⑤ | 用户住在北京海淀区,通勤主要靠地铁 | location |
| ⑥ | 用户喜欢在周末和朋友聚餐,偏好川菜和火锅 | hobby |
现在系统发起 KNN 向量检索(用"推荐个周末活动"生成 query vector,语义匹配)和 BM25 关键词检索(精确匹配"周末"“活动”“推荐”),各自返回 Top 3:
| 排名 | KNN(语义匹配) | 为什么排在前面 | BM25(关键词匹配) | 为什么排在前面 |
|---|---|---|---|---|
| 1 | ② 朝阳公园跑步 | 和"活动""运动"语义最近 | ④ 周末攀岩体验课 | 同时命中"周末""活动"两个词 |
| 2 | ① 打篮球 | 运动类语义高度相关 | ② 朝阳公园跑步 | 命中"户外运动" |
| 3 | ④ 周末攀岩体验课 | 和"活动"有一定语义关联 | ⑥ 周末聚餐 | 命中"周末" |
逐条计算 RRF 分数(k=60):
- ②(朝阳公园跑步):KNN 排第 1 →
1/(60+1) = 0.01639,BM25 排第 2 →1/(60+2) = 0.01613,合计 0.03252 - ④(周末攀岩体验课):KNN 排第 3 →
1/(60+3) = 0.01587,BM25 排第 1 →1/(60+1) = 0.01639,合计 0.03226 - ①(打篮球):只在 KNN 排第 2 →
1/(60+2)= 0.01613 - ⑥(周末聚餐):只在 BM25 排第 3 →
1/(60+3)= 0.01587
最终融合排序:② 朝阳公园跑步 > ④ 周末攀岩体验课 > ① 打篮球 > ⑥ 周末聚餐。③ 和 ⑤ 没有进入融合结果,它们确实跟"周末活动"无关。
为什么这个结果更好?
- 如果只用 KNN:排名是 ② → ① → ④,但 ①(打篮球)虽然运动相关,用户从来没说过跟"周末"有关,纯语义匹配没有这个判断力。
- 如果只用 BM25:排名是 ④ → ② → ⑥,但 ⑥(周末聚餐)只是碰巧提到"周末",语义上并非"活动推荐"的核心意图。
- RRF 融合后:② 同时被两个检索器认可(语义匹配性强 + 命中"户外运动"关键词),稳居第一。④ 在 BM25 中排第一(精确命中"周末"“活动”)、在 KNN 中也排第三("攀岩"和"活动"语义接近),总分紧随其后。这是一个比单独用任何一种检索都更合理的排序。
生产实测混合检索相比纯向量:Recall@10 提升 8-15%,几乎零额外延迟。
2.5 长期记忆维护
定时清理——ISM 策略
ISM(Index State Management)是 OpenSearch 内置的索引生命周期管理插件。它允许你定义一系列"阶段"(phase)和"状态转换条件"(transition),让索引自动在不同阶段间流转,无需手动执行 _delete_by_query。
核心概念:
- Phase(阶段):每个阶段定义了一组要执行的操作(action)。OpenSearch 支持
hot、warm、cold、delete四个阶段。 - Transition(转换条件):决定索引何时从一个阶段进入下一个阶段。常用条件包括索引存在时长(
min_index_age)、文档数量(min_doc_count)、索引大小(min_size)。 - ISMTemplate:将策略自动应用到新创建的匹配索引上。
以下是一个适合记忆系统的 ISM 策略:
PUT _plugins/_ism/policies/memory_cleanup
{
"policy": {
"description": "定期清理超过一年且重要性低的用户记忆",
"default_state": "hot",
"states": [
{
"name": "hot",
"actions": [
{
"retry": { "count": 3, "backoff": "exponential", "delay": "1m" },
"force_merge": { "max_num_segments": 1 }
}
],
"transitions": [
{
"state_name": "delete_old",
"conditions": {
"min_index_age": "30d"
}
}
]
},
{
"name": "delete_old",
"actions": [
{
"retry": { "count": 3, "backoff": "exponential", "delay": "1m" },
"delete": {}
}
],
"transitions": []
}
],
"ism_template": [
{
"index_patterns": ["user_long_term_memory*"],
"priority": 100
}
]
}
}
阶段拆解:
hot阶段:索引的默认状态。force_merge将分段数合并为 1,优化查询性能。30 天后触发转换。delete_old阶段:执行deleteaction,OpenSearch 自动删除该索引。retry配置失败重试 3 次,指数退避。
什么时候用 ISM,什么时候用 _delete_by_query?
| 方案 | 适用场景 | 原理 |
|---|---|---|
| ISM | 按整个索引粒度清理,例如按月份拆分的索引(user_memory_2025-01)到时间后整库删除 |
删除整个索引,零 I/O 开销,秒级完成 |
_delete_by_query |
单索引内按条件精细删除,例如同索引中删掉 importance < 0.5 且 timestamp 超一年的部分文档 |
逐文档匹配删除,开销大但粒度细 |
我们的场景(单索引 user_long_term_memory,需要条件判断 importance 和 timestamp)更适合 _delete_by_query,因为 ISM 只能删整库,无法按文档字段做条件选择。如果希望用 ISM 的自动化能力,可改为按月分索引(user_long_term_memory_2025-06),然后 ISM 在 365 天后直接删掉对应月份的整库。
分月索引 + ISM 的实践:
PUT /user_long_term_memory_2025-06
{
"settings": {
"index.knn": true,
"plugins.index_state_management.policy_id": "memory_cleanup",
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": { "..." }
}
写入时按 timestamp 的月份路由到对应索引,旧索引到期后 ISM 自动删除,无需任何手动清理脚本。代价是跨月检索需要同时对多索引发起查询,可以用 Index Alias 或 user_long_term_memory* 通配符解决。
重要性递增——高频使用的记忆被检索后,在应用层上调 importance:
client.update(
index="user_long_term_memory",
id=memory_id,
body={
"script": {
"source": "ctx._source.importance = Math.min(ctx._source.importance + 0.05, 1.0)"
}
}
)
3. Redis + OpenSearch 双层记忆架构
3.1 短期记忆(Redis)
import redis, json, time
r = redis.Redis(host="localhost", port=6379, db=0)
def add_stm(user_id, role, content):
key = f"agent:stm:{user_id}"
r.rpush(key, json.dumps({"role": role, "content": content, "ts": time.time()}))
r.ltrim(key, -20, -1) # 保留最近 20 条 = 10 轮对话
r.expire(key, 3600) # 1 小时后自动过期
def get_stm(user_id):
return [json.loads(m) for m in r.lrange(f"agent:stm:{user_id}", 0, -1)]
3.2 协同流程
用户发消息
├─ Redis 读取最近 10 轮对话 → STM 上下文
├─ 生成 Query Vector
├─ OpenSearch 混合检索(KNN+BM25) → LTM Top K
│ Filter: user_id
├─ STM + LTM 注入 Prompt → LLM 生成回答
└─ 本轮对话写入 Redis
3.3 异步固化
会话结束时(Redis TTL 过期事件)或累积 20 轮对话后,后台任务:
- 拼接 Redis 对话记录 → 发 LLM 提取
category、text、importance - 对每条提取的记忆计算向量,与 OpenSearch 同 category 已有记忆做余弦相似度对比
- 相似度 > 0.85 跳过,< 0.85 执行
delete_by_query+index更新 - 保留最近 2 轮在 Redis,其余清空
4. 主流向量数据库深度对比
4.0 数据来源
以下数据综合自 ANN-Benchmarks(ann-benchmarks.com)、各厂商官方基准(2023-2024)、Jonathan Katz 的 pgvector 报告及社区公开对比。所有数值为近似范围,实际性能受硬件、数据集和参数影响。
4.1 OpenSearch KNN
优势:
- Faiss 引擎:支持 HNSW + IVF 双索引。IVF 在大规模下构建更快、内存更省;还支持 PQ/SQ 量化压缩(最高 16x)
- 混合检索:KNN + BM25 天然融合,无需两套系统
- 全文检索引擎底座:倒排索引过滤元数据效率极高,
user_id+category+timestamp的复合过滤远超纯向量数据库 - 企业级成熟度:RBAC、审计日志、ISM 生命周期管理、跨集群复制
- Apache 2.0:无商业许可顾虑
- 维度上限 16,384:覆盖所有主流 Embedding 模型
劣势:
- JVM 的 GC 停顿在极端负载下可能影响 P99 延迟(实践中 < 5ms)
- 纯向量检索 QPS 不如 Milvus/Qdrant(但混合检索场景可反超)
- 分片 KNN 需汇合结果,分片越多精度越受影响
4.2 Elasticsearch KNN
OpenSearch 的同源兄弟(fork 自 ES 7.10.2)。8.x 后向量检索实现路线分化:
| 维度 | OpenSearch 2.x | Elasticsearch 8.x |
|---|---|---|
| 底层引擎 | Faiss / NMSLIB / Lucene 三选一 | 仅 Lucene 原生 HNSW |
| 索引类型 | HNSW + IVF | 仅 HNSW |
| 量化压缩 | PQ / SQ(Faiss) | int8_hnsw(8.13+)、bit(8.15+) |
| 维度上限 | 16,384 | 4,096 |
| 启用方式 | settings.index.knn: true |
字段级 index: true |
| 协议 | Apache 2.0 | SSPL / Elastic License 2.0 |
| RRF | 2.11+ | 8.12+ |
结论:向量检索领域,OpenSearch 比 Elasticsearch 更有优势——Faiss 引擎的 IVF 和 PQ/SQ 是 ES 当前不具备的。ES 的优势在于生态(Kibana、Elastic Cloud),如果团队已深度绑定 ES 且向量需求简单,ES 够用。
4.3 pgvector
一句话:PostgreSQL 扩展,给已有 PG 的团队提供"够用"的向量检索。
优势:零额外基础设施;ACID 事务一致性(独有优势);SQL JOIN / WHERE 自由组合。
劣势:
- 100 万级 768 维 HNSW QPS 约 500-800(对比 OpenSearch 1500-2500,Milvus 4000-6000)
- 索引构建慢 3-5 倍
- 无原生分布式,1000 万向量以上触顶
- 频繁更新需定期 VACUUM
适用:向量 < 500 万,已有 PG,需要向量 + 结构化数据高频关联。
4.4 Milvus
一句话:专业级分布式向量数据库,十亿级场景首选。
优势:
- 1M SIFT-1M HNSW QPS 约 4000-6000,100M 分布式仍维持 1500-3000
- 10+ 种索引类型 + GPU 加速(NVIDIA RAFT,5-10x throughput)
- 存算分离架构,真正分布式
- 腾讯、eBay、Walmart 等百亿级验证
劣势:
- 部署重:需要 etcd + MinIO/S3 + Pulsar/Kafka,standalone 也 3 容器起步
- 没有全文检索,需另配 ES/OpenSearch 做 BM25
- 存储开销 2.5-4 倍原始数据
适用:向量 > 1000 万、毫秒级延迟硬要求、需要 GPU。
4.5 Qdrant
一句话:Rust 轻量向量数据库,单节点性能极致。
优势:
- 1M 768 维 HNSW QPS 约 3000-5000
- Binary Quantization 32x 压缩(精度损失 ~5%),Scalar Quantization 4x
- 单二进制部署,Docker 镜像 ~20MB
- Payload Index 预过滤高效
劣势:
- 自建分布式不够成熟,企业功能多为 Cloud 版
- 无全文检索
- 自有查询语法需学习
适用:1000 万-1 亿级,追求单节点极致性能 + 极简运维。
4.6 Weaviate
一句话:GraphQL 原生,多租户和混合检索是差异化亮点。
优势:
- 原生多租户隔离,SaaS 友好
hybrid查询内置 BM25 + 向量融合- AutoSchema + 内置 Embedding 集成
劣势:
- QPS 约 1500-2500,JVM 开销明显
- 多租户等高级功能需商业版
适用:SaaS 多租户、< 5000 万向量、要开箱即用。
4.7 综合对比表
基准条件:100 万条 768 维向量,8 核 32GB,HNSW(m=16, ef_construction=128),ef_search=100。
| 指标 | OpenSearch 2.x | Elasticsearch 8.x | pgvector | Milvus 2.4 | Qdrant 1.x | Weaviate 1.x |
|---|---|---|---|---|---|---|
| QPS(95% recall@10) | 1,500-2,500 | 2,000-3,000 | 500-800 | 4,000-6,000 | 3,000-5,000 | 1,500-2,500 |
| P99 延迟 | < 5ms | < 5ms | < 15ms | < 2ms | < 3ms | < 5ms |
| 索引构建 | 3-8 min | 5-10 min | 20-35 min | 3-5 min | 3-8 min | 5-15 min |
| 内存占用 | 6-10 GB | 6-8 GB | 4-6 GB | 8-12 GB | 4-8 GB | 8-10 GB |
| 维度上限 | 16,384 | 4,096 | 16,000 | 32,768 | 65,535 | 65,535 |
| 千万级表现 | O | △ | △ | O | O | O |
| 十亿级能力 | △需多节点 | △需多节点 | X | O | △Cloud | △ |
能力矩阵:
| 能力 | OpenSearch | Elasticsearch | pgvector | Milvus | Qdrant | Weaviate |
|---|---|---|---|---|---|---|
| KNN+BM25 混合检索 | O | O | △手动 | X | X | O |
| 全文检索 | O | O | O | X | X | O |
| 量化压缩 | O PQ/SQ | △ int8/bit | △ halfvec | O PQ/SQ | O BQ/SQ | O PQ |
| 多索引类型 | O HNSW+IVF | X 仅HNSW | △ HNSW+IVFFlat | O 10+种 | O HNSW | O HNSW |
| GPU 加速 | X | X | X | O | X | X |
| 原生分布式 | O 分片 | O 分片 | X | O 存算分离 | △Cloud | O |
| 部署复杂度 | 中 | 中 | 低 | 高 | 低 | 中 |
| 开源协议 | Apache 2.0 | SSPL/Elastic | PostgreSQL | Apache 2.0 | Apache 2.0 | BSD-3 |
4.8 具体 Benchmark 数据
1M SIFT-1M(128 维),各官方 2023-2024 年报告:
| 系统 | Recall@10 | QPS | 内存 | 引擎 |
|---|---|---|---|---|
| OpenSearch 2.11 HNSW | 96.8% | 2,310 | ~5.5 GB | Faiss |
| OpenSearch 2.11 IVF | 91.2% | 3,840 | ~4.0 GB | Faiss |
| Elasticsearch 8.11 | 96.3% | 2,847 | ~5.0 GB | Lucene |
| pgvector HNSW | 95.8% | 723 | ~3.8 GB | PG |
| Milvus 2.3 HNSW | 97.1% | 5,210 | ~6.5 GB | Knowhere |
| Qdrant 1.7 | 96.7% | 4,380 | ~4.2 GB | Rust |
OpenSearch IVF 以 5.6% 的召回代价换 66% 吞吐提升 + 27% 内存节省,适合粗排+精排二阶段检索。但 OpenSearch 的真正壁垒不是纯向量 QPS,而是混合检索——这个能力无法被向量 QPS 指标衡量。
5. 选型决策树
启动向量检索项目
│
├─ 已部署 OpenSearch?
│ └─ YES → 直接用。Faiss IVF + PQ/SQ + RRF。
│
├─ 已部署 Elasticsearch 8.x?
│ ├─ 需要 IVF / PQ / 宽松协议?→ 切 OpenSearch
│ └─ 向量需求简单、够用就行? → 用 ES KNN
│
├─ 已部署 PostgreSQL?
│ └─ 向量 < 500 万?→ pgvector。零运维,ACID 完美。
│ └─ > 500 万? → 评估是否要分布式
│
├─ 向量 > 1 亿 或 需要 GPU?
│ └─ Milvus。唯一十亿级大规模验证的方案。
│
├─ SaaS 多租户?
│ └─ Weaviate。原生多租户,接入成本最低。
│
├─ 单节点极致性能 + 极简运维?
│ └─ Qdrant。一个二进制。
│
└─ 搜索 + 向量一站式?
└─ OpenSearch(推荐)或 Weaviate。
踩坑提醒
- 别维护两套检索系统。Milvus 做向量 + OpenSearch 做全文 = 数据同步 + 结果融合 + 双份运维。优先一站式方案。
- 维度是第一坑。
model.encode("test").shape确认输出,与dimension对齐。创建后不可改。 - 混合检索 >> 纯向量检索。纯向量 recall@10 天花板约 85-92%,加 BM25 后 92-97%。OpenSearch/ES/Weaviate 的混合检索是真正的工程利器。
- 量化省内存。OpenSearch Faiss SQ(4x)、Qdrant BQ(32x)、pgvector halfvec(2x),精度损失在 RAG 中可忽略。
- 千万级以上用 IVF。HNSW 内存膨胀快,IVF 更经济。
- 先 1 万验证再 100 万。索引参数(m、ef_construction、nlist)必须不同规模调优。
6. 结语
| 你的情况 | 最优选择 |
|---|---|
| 已有 OpenSearch | OpenSearch KNN(Faiss) |
| 已有 ES 8.x | ES KNN 或切 OpenSearch |
| 已有 PG,向量 < 500 万 | pgvector |
| 十亿级 + GPU | Milvus |
| 单机极致性能 | Qdrant |
| SaaS 多租户 | Weaviate |
| 搜索 + 向量一站式 | OpenSearch |
对大多数团队,OpenSearch 是综合成本最低的选择:已经用它做搜索或日志,开 KNN 只是加一个 index.knn: true 和 knn_vector 字段,零新增组件。它的混合检索、全文检索和元数据过滤,是纯向量数据库天然不具备的能力。反过来,Faiss 引擎的多索引类型(HNSW + IVF)和量化压缩(PQ/SQ),又是 ES 没有的。
架构选型的智慧:知道什么"够用",比知道什么"最强"更重要。
更多推荐

所有评论(0)