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 对齐。也可 l2innerproduct
method.engine faiss 推荐。Meta 开源,支持 HNSW/IVF/PQ/SQ。备选 nmsliblucene
parameters.m 16 每个节点最大连接数。范围 2-100。百万级取 16-32
parameters.ef_construction 128 构建时的搜索宽度。范围 100-2000。越大索引越准但构建越慢

m 16→32:内存 +80%,QPS +25-40%。ef_construction 128→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 全版本通用)

knnmatch 同时放进 boolshould 子句,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 支持 hotwarmcolddelete 四个阶段。
  • 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 阶段:执行 delete action,OpenSearch 自动删除该索引。retry 配置失败重试 3 次,指数退避。

什么时候用 ISM,什么时候用 _delete_by_query

方案 适用场景 原理
ISM 按整个索引粒度清理,例如按月份拆分的索引(user_memory_2025-01)到时间后整库删除 删除整个索引,零 I/O 开销,秒级完成
_delete_by_query 单索引内按条件精细删除,例如同索引中删掉 importance < 0.5timestamp 超一年的部分文档 逐文档匹配删除,开销大但粒度细

我们的场景(单索引 user_long_term_memory,需要条件判断 importancetimestamp)更适合 _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 轮对话后,后台任务:

  1. 拼接 Redis 对话记录 → 发 LLM 提取 categorytextimportance
  2. 对每条提取的记忆计算向量,与 OpenSearch 同 category 已有记忆做余弦相似度对比
  3. 相似度 > 0.85 跳过,< 0.85 执行 delete_by_query + index 更新
  4. 保留最近 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。

踩坑提醒

  1. 别维护两套检索系统。Milvus 做向量 + OpenSearch 做全文 = 数据同步 + 结果融合 + 双份运维。优先一站式方案。
  2. 维度是第一坑model.encode("test").shape 确认输出,与 dimension 对齐。创建后不可改。
  3. 混合检索 >> 纯向量检索。纯向量 recall@10 天花板约 85-92%,加 BM25 后 92-97%。OpenSearch/ES/Weaviate 的混合检索是真正的工程利器。
  4. 量化省内存。OpenSearch Faiss SQ(4x)、Qdrant BQ(32x)、pgvector halfvec(2x),精度损失在 RAG 中可忽略。
  5. 千万级以上用 IVF。HNSW 内存膨胀快,IVF 更经济。
  6. 先 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: trueknn_vector 字段,零新增组件。它的混合检索、全文检索和元数据过滤,是纯向量数据库天然不具备的能力。反过来,Faiss 引擎的多索引类型(HNSW + IVF)和量化压缩(PQ/SQ),又是 ES 没有的。

架构选型的智慧:知道什么"够用",比知道什么"最强"更重要。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐