Redis&ES------Retriever的抽象实现
Eino 的 Retriever 抽象就是为了屏蔽底层检索实现,Redis 和 Elasticsearch(ES)都可以作为 Retriever 的实现,只不过支持方式有所区别。
Eino 中 Retriever 的定位
Eino 将检索能力统一抽象成了一个接口,大致可以理解成:
go
type Retriever interface {
Retrieve(ctx context.Context, query string, opts ...Option) ([]*schema.Document, error)
}
无论底层是:
- 向量数据库(Milvus、Qdrant、Chroma)
- 搜索引擎(Elasticsearch)
- Redis
- 自定义数据库
最终都只需要实现这个接口即可。
这样在 Chain 或 Agent 中,只需要依赖 Retriever,而不用关心底层是什么。
1. Elasticsearch
ES 非常适合作为 Retriever。
可以有几种方式:
① BM25 检索
User Query
│
▼
Elasticsearch
(match / multi_match)
│
▼
Documents
这种就是传统全文检索。
例如:
"如何部署docker"
↓
match:
content:"如何部署docker"
② 向量检索
ES 8.x 已支持 Dense Vector。
流程:
Embedding
↓
query_vector
↓
KNN Search
↓
TopK Documents
Retriever 内部:
Embedding(query)
↓
ES KNN
↓
返回 Document[]
③ Hybrid Search
很多 RAG 都采用:
BM25
+
Vector Search
+
RRF
Retriever 完全可以封装这套逻辑。
2. Redis
Redis 也完全可以做 Retriever。
但这里要区分是哪种 Redis。
普通 Redis
例如:
GET
HGET
SCAN
它本身没有真正意义上的语义检索。
如果只是:
key -> document
那更像 KV Store,不属于典型 Retriever。
Redis Stack(RediSearch)
如果安装了:
Redis Stack
或者:
RediSearch
就支持:
- 全文检索
- Vector Search
- Hybrid Search
例如:
FT.SEARCH
KNN
VECTOR
那么它完全可以作为 Retriever。
流程:
Embedding
↓
Redis Vector Index
↓
TopK
↓
Document[]
3. 在 Eino 中如何使用
Eino 不要求 Retriever 一定来自官方。
例如可以自己实现:
ESRetriever
RedisRetriever
MilvusRetriever
PGVectorRetriever
QdrantRetriever
它们都满足:
Retrieve()
↓
[]Document
Chain 完全不用修改。
例如:
KnowledgeRetriever
│
┌────────┴─────────┐
│ │
ESRetriever MilvusRetriever
│ │
ES Milvus
Agent 根本不知道底层是谁。
4. 官方是否内置了 Redis 和 ES?
截至目前,Eino 官方生态主要内置和维护的是对常见向量数据库、Embedding、LLM 等组件的适配 。Elasticsearch 和 Redis 并不是官方长期维护的核心 Retriever 实现,更多时候需要你自行实现适配,或者基于社区扩展来接入。
由于 Eino 的 Retriever 接口非常简单,实现一个 ESRetriever 或 RedisRetriever 的成本并不高,只需要把底层查询结果转换成 schema.Document 即可。
总结
| 检索后端 | 能否作为 Retriever | 是否适合 RAG |
|---|---|---|
| Elasticsearch(BM25) | ✅ | ✅ |
| Elasticsearch(Vector) | ✅ | ✅ |
| Elasticsearch(Hybrid) | ✅ | ⭐⭐⭐⭐⭐ |
| Redis(普通 KV) | ⚠️ 可以封装,但不属于真正语义检索 | ❌ |
| Redis Stack / RediSearch | ✅ | ✅ |
| Milvus | ✅ | ⭐⭐⭐⭐⭐ |
| Qdrant | ✅ | ⭐⭐⭐⭐⭐ |
如果你是在做企业级 RAG 或智能体项目,我更推荐采用"ES + 向量库"的双检索架构(Hybrid Retrieval):ES 负责关键词/BM25 检索,Milvus 或 Qdrant 负责语义向量检索,再通过 RRF、加权融合或重排序(Reranker)合并结果。这也是目前许多生产系统采用的方案。
也就是说,要区分抽象(Interface)**和**具体实现(Implementation)。
第一层:Retriever 是抽象
Eino 中真正依赖的是 Retriever 接口,例如:
go
type Retriever interface {
Retrieve(ctx context.Context, query string, opts ...Option) ([]*schema.Document, error)
}
对于 Chain、Agent 来说,它们只认识:
Retriever
并不知道下面到底是谁。
例如:
Retriever
▲
┌──────────────┼──────────────┐
│ │ │
MilvusRetriever ESRetriever RedisRetriever
│ │ │
Milvus ES Redis Stack
Agent 调用的永远都是
Retrieve(...)
所以 Retriever 抽象可以对应任意数据库。
第二层:MilvusRetriever 是具体实现
MilvusRetriever 的内部代码一般会直接调用 Milvus SDK,例如:
go
client.Search(...)
它知道:
- collection
- vector field
- metric type
- topK
例如:
User Query
│
Embedding
│
MilvusRetriever
│
Milvus SDK
│
Milvus Server
因此:
MilvusRetriever 不可能直接去查 ES。
因为:
ES 的 API 是
POST /_search
Milvus 是
Search()
参数完全不同。
第三层:如果数据库换了怎么办?
假设今天不用 Milvus,而换成:
- Qdrant
- Weaviate
- PGVector
- Chroma
那么应该:
Retriever
▲
┌───────────┴───────────┐
│ │
MilvusRetriever QdrantRetriever
│ │
Milvus Qdrant
只需要:
go
type QdrantRetriever struct {
client *qdrant.Client
}
func (r *QdrantRetriever) Retrieve(...) {
...
}
即可。
整个 Agent 一行代码都不用改。
为什么要这样设计?
这就是经典的依赖倒置(Dependency Inversion)。
例如:
以前:
Agent
│
▼
Milvus
Agent 被绑死了。
现在:
Agent
│
▼
Retriever
│
├── Milvus
├── ES
├── Redis
├── PGVector
└── Qdrant
Agent 根本不知道下面是谁。
以后替换数据库:
Milvus
↓
Qdrant
Agent 完全不用改。
Eino 为什么要这样抽象?
因为一个 Retriever 并不一定对应一个数据库,它甚至可以组合多个数据源。
例如:
HybridRetriever
│
┌────┴────┐
│ │
ES Milvus
│ │
BM25 Vector Search
└────┬────┘
▼
Rank Fusion
▼
Documents
对于 Agent 来说,它依然只是:
go
docs, err := retriever.Retrieve(ctx, query)
它不知道内部实际上调用了:
- ES
- Milvus
- Reranker
甚至可以再加:
- Redis Cache
- Knowledge Graph
- MySQL
都可以。
一句话总结
Retriever:Eino 定义的抽象接口,可以适配任意检索后端(Milvus、Qdrant、ES、Redis Stack、PGVector 等)。MilvusRetriever:Retriever的一个具体实现,只负责调用 Milvus,因此不能直接用于其他向量数据库。- 如果更换向量数据库,应实现对应的
QdrantRetriever、PGVectorRetriever等,只要都实现Retriever接口,Agent 和 Chain 无需修改。
这也是 Eino 将组件分为 Loader → Transformer → Indexer → Retriever → Reranker 的核心思想:上层依赖统一抽象,下层可以自由替换具体实现。