为什么选择 Vespa 而非传统向量数据库
在向量搜索领域,大多数人首先想到的是 Pinecone、Milvus 或 Qdrant 这样的专用向量数据库。然而,Yahoo(现 Verizon Media)开源的 Vespa 搜索引擎在实时搜索和推荐系统中已经历了超过十年的生产验证,它不仅能高效处理向量检索,还能同时完成全文搜索、结构化过滤和实时排序计算——这一切在同一个查询中完成,无需额外的系统编排。
Vespa 的核心优势在于其统一查询模型:你可以在一次查询中同时执行向量近邻搜索、BM25 全文检索、结构化属性过滤,以及基于机器学习模型的实时打分排序。这种能力在 RAG(检索增强生成)和推荐系统中尤为关键,因为真实的业务场景往往需要多种信号的综合判断,而非单一的向量相似度。
与专用向量数据库相比,Vespa 的架构差异体现在以下几个维度:
- 查询表达能力:Vespa 使用自己的查询语言 YQL,支持复杂的布尔组合、嵌套查询和排序表达式,而大多数向量数据库仅支持简单的 Top-K 检索加过滤
- 实时计算能力:Vespa 的排序表达式可以在查询时执行任意数学运算和模型推理,而非仅依赖预计算的静态分数
- 内容集群架构:Vespa 采用分布式内容集群设计,数据按文档粒度自动分片和冗余,支持在线扩容和缩容
- 多模态支持:同一文档可以同时包含文本、向量、张量等多种类型的字段,查询时可以混合使用
本文将从 Vespa 的架构设计出发,逐步演示如何构建一个生产级的混合检索系统,涵盖向量索引配置、YQL 查询语法、实时排序表达式、性能调优和部署策略。

Vespa 核心架构解析
分布式内容集群模型
Vespa 的架构围绕应用程序包(Application Package)组织,每个应用包含服务集群定义、文档模型和排序配置。一个典型的 Vespa 集群包含以下核心组件:
- Config Server:管理集群配置,负责将应用包分发到所有节点,支持配置的热更新
- Container Cluster:无状态的查询处理层,负责接收请求、执行查询规划、调用排序表达式并返回结果
- Content Cluster:有状态的数据存储层,文档按分布策略分片到不同节点,每个分片可配置冗余副本
Content Cluster 的数据分布策略是 Vespa 高可用性的基础。每个文档根据其 ID 哈希到特定的分桶(bucket),每个分桶在多个节点上持有副本。当某个节点故障时,集群自动将请求路由到副本节点,确保查询不中断。这种设计使得 Vespa 可以在不停机的情况下完成节点的添加和移除。
文档模型与张量类型
Vespa 的文档模型使用强类型 Schema定义,每个字段必须声明类型。对于向量搜索,关键字段类型是
1 | tensor |
,它支持任意维度的密集张量。以下是一个典型的文档 Schema 定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 schema article {
document article {
field title type string {
indexing: summary | index
index: enable-bm25
}
field content type string {
indexing: summary | index
index: enable-bm25
}
field embedding type tensor<float>(x[768]) {
indexing: summary | attribute | index
index {
hnsw {
max-links-per-node: 16
neighbors-to-explore-at-insert: 200
}
}
}
field category type string {
indexing: summary | attribute
}
field publish_time type long {
indexing: summary | attribute
}
}
fieldset default {
fields: title, content
}
rank-profile semantic {
inputs {
query(query_embedding) tensor<float>(x[768])
}
function cosine_similarity() {
expression: cosSimilarity(query(query_embedding), attribute(embedding))
}
first-phase {
expression: cosine_similarity
}
}
rank-profile hybrid {
inputs {
query(query_embedding) tensor<float>(x[768])
}
function cosine() {
expression: cosSimilarity(query(query_embedding), attribute(embedding))
}
function bm25_score() {
expression: bm25(title) + bm25(content)
}
first-phase {
expression: 0.7 * cosine + 0.3 * bm25_score
}
}
}
这个 Schema 定义了几个关键设计:
-
1embedding
字段使用
1tensor<float>(x[768])类型,表示 768 维浮点向量,并启用了 HNSW 索引
-
1index: enable-bm25
为文本字段启用了 BM25 评分,这是混合检索的基础
-
1rank-profile
定义了两种排序策略:纯语义排序和混合排序
- 混合排序中通过
10.7 * cosine + 0.3 * bm25_score
加权融合语义和词汇信号
值得注意的是,Vespa 的 HNSW 实现支持实时增量更新——你可以在不重建索引的情况下插入、更新和删除文档,而大多数向量数据库的 HNSW 索引在更新时需要重建或使用复杂的版本管理策略。
从零构建 Vespa 混合检索系统
环境准备与服务部署
我们使用 Docker 部署 Vespa 容器,这是最快捷的开发和测试方式:
1
2
3
4
5 # 启动 Vespa 配置服务器和容器节点
docker run --detach --name vespa --hostname vespa-container --publish 8080:8080 --publish 19071:19071 vespaengine/vespa
# 等待配置服务器就绪
docker exec vespa bash -c "curl -s --head http://localhost:19071/ApplicationStatus"
Vespa 的应用包是部署的核心单元,包含 Schema、服务配置和排序配置。创建项目目录结构:
1
2
3
4
5 vespa-app/
├── schemas/
│ └── article.sd
├── services.xml
└── validation-overrides.xml
1 | services.xml |
定义了集群拓扑和资源分配:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 <?xml version="1.0" encoding="utf-8"?>
<services version="1.0">
<container id="default" version="1.0">
<search/>
<document-api/>
<nodes>
<node hostalias="node1"/>
</nodes>
</container>
<content id="content" version="1.0">
<redundancy>2</redundancy>
<documents>
<document type="article" mode="index"/>
</documents>
<nodes>
<node hostalias="node1" distribution-key="0"/>
</nodes>
</content>
</services>
部署应用包到 Vespa 集群:
1
2
3
4 # 打包并部署
zip -r application.zip schemas/ services.xml
curl --header "Content-Type: application/zip" --data-binary @application.zip http://localhost:19071/application/v2/tenant/default/application/default/prepare
curl -X POST http://localhost:19071/application/v2/tenant/default/application/default/activate
文档写入与向量索引构建
使用 Vespa 的 Document V1 API 写入文档,支持同步和异步操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 import requests
import numpy as np
VESPA_ENDPOINT = "http://localhost:8080"
def generate_embedding(text):
# 使用 Sentence-Transformer 生成向量嵌入
# 实际生产中应使用 batch 推理和更高效的模型
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-base-zh-v1.5')
return model.encode(text).tolist()
def index_document(doc_id, title, content, category, publish_time):
# 向 Vespa 索引一篇文档
full_text = f"{title}。{content}"
embedding = generate_embedding(full_text)
document = {
"fields": {
"title": title,
"content": content,
"embedding": embedding,
"category": category,
"publish_time": publish_time
}
}
response = requests.post(
f"{VESPA_ENDPOINT}/document/v1/article/article/docid/{doc_id}",
json=document
)
return response.json()
# 批量索引示例
documents = [
{
"id": "doc-001",
"title": "深度学习在自然语言处理中的最新进展",
"content": "Transformer 架构的提出彻底改变了 NLP 领域...",
"category": "AI",
"publish_time": 1700000000
},
{
"id": "doc-002",
"title": "Kubernetes 集群高可用部署最佳实践",
"content": "在生产环境中部署 Kubernetes 集群需要考虑...",
"category": "DevOps",
"publish_time": 1700001000
}
]
for doc in documents:
result = index_document(
doc["id"], doc["title"], doc["content"],
doc["category"], doc["publish_time"]
)
print(f"Indexed {doc['id']}: {result}")
Vespa 的文档写入是实时生效的——文档一旦写入成功,即可在查询中被检索到,无需等待索引刷新。这种实时性对于新闻推荐、电商搜索等时效性要求高的场景至关重要。

YQL 查询语法与混合检索
Vespa 的查询语言 YQL(Yahoo Query Language)是其核心表达力所在。以下展示三种典型的查询模式:
1. 纯向量语义搜索
1
2
3
4
5
6
7 # 使用 nearestNeighbor 操作符进行向量检索
{
"yql": "select * from article where {targetHits: 100}nearestNeighbor(embedding, query_embedding)",
"query_embedding": [0.12, -0.34, 0.56, ...],
"ranking": "semantic",
"hits": 10
}
1 | nearestNeighbor |
操作符使用 HNSW 索引进行近似最近邻搜索,
1 | targetHits |
参数控制每个内容节点返回的候选数量。Vespa 会先通过 HNSW 索引找到候选集,再按照 rank-profile 中定义的排序表达式进行精确排序。
2. 结构化过滤 + 向量搜索
1
2
3
4
5
6
7 # 向量搜索结合分类过滤和时间范围
{
"yql": "select * from article where {targetHits: 100}nearestNeighbor(embedding, query_embedding) and category contains 'AI' and publish_time > 1699000000",
"query_embedding": [0.12, -0.34, 0.56, ...],
"ranking": "semantic",
"hits": 10
}
在 Vespa 中,过滤条件在 HNSW 搜索之后、排序之前应用。这意味着
1 | targetHits |
应该设置得足够大以确保过滤后有足够的候选文档。对于高选择性过滤,Vespa 提供了
1 | approximate |
选项来控制过滤精度:
1
2
3
4
5
6
7 # 使用 approximate 过滤提升性能
{
"yql": "select * from article where {targetHits: 500, approximate: true}nearestNeighbor(embedding, query_embedding) and category contains 'AI'",
"query_embedding": [0.12, -0.34, 0.56, ...],
"ranking": "semantic",
"hits": 10
}
3. 真正的混合检索:向量 + BM25 + 过滤
1
2
3
4
5
6
7
8 # 融合语义相似度和词汇匹配的混合查询
{
"yql": "select * from article where ({targetHits: 100}nearestNeighbor(embedding, query_embedding)) or userQuery()",
"query": "深度学习 NLP",
"query_embedding": [0.12, -0.34, 0.56, ...],
"ranking": "hybrid",
"hits": 10
}
这个查询同时执行两路检索:
1 | nearestNeighbor |
从 HNSW 索引中获取语义相似的候选,
1 | userQuery() |
从倒排索引中获取词汇匹配的候选。两路结果合并后,由
1 | hybrid |
排序配置进行统一打分。这种设计避免了在应用层做两路检索再合并的复杂逻辑,所有工作在 Vespa 内部原子化完成。
高级排序表达式与实时计算
Vespa 最强大的特性之一是排序表达式(Rank Profile),它允许在查询时执行任意计算,包括数学运算、条件分支和模型推理。这远超传统搜索引擎的静态评分机制。
多阶段排序管线
生产系统中通常使用两阶段排序以平衡召回率和延迟:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 rank-profile production {
inputs {
query(query_embedding) tensor<float>(x[768])
query(user_click_weight) double # 用户行为权重
query(freshness_weight) double # 时效性权重
}
function cosine() {
expression: cosSimilarity(query(query_embedding), attribute(embedding))
}
function freshness() {
expression: pow(0.95, (now() - attribute(publish_time)) / 86400000.0)
}
function popularity() {
expression: attribute(click_count) / (attribute(impression_count) + 1.0)
}
# 第一阶段:轻量级粗排,从所有候选中快速筛选
first-phase {
expression: cosine + 0.1 * freshness
keep-rank-count: 200
}
# 第二阶段:精确重排,仅对 Top-200 候选执行
second-phase {
expression: 0.5 * cosine + 0.2 * freshness + 0.2 * popularity + 0.1 * query(user_click_weight)
rerank-count: 100
}
}
这种两阶段设计在生产环境中至关重要:
- 第一阶段使用轻量级计算(向量相似度 + 时间衰减),快速从数百万候选中筛选出 Top-200
- 第二阶段对 Top-200 执行更复杂的计算(加入点击率等统计特征),输出最终排序
- 整个管线在 Vespa 内部完成,避免了网络往返和数据序列化开销
张量运算与模型推理
Vespa 的排序表达式支持张量运算,可以在查询时执行神经网络的前向传播。以下示例展示了如何在排序中实现 Cross-Encoder 重排序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 rank-profile cross_encoder_rerank inherits hybrid {
inputs {
query(query_embedding) tensor<float>(x[768])
query(token_ids) tensor<float>(t[32])
}
# 模型权重作为常量张量存储在应用包中
constant cross_encoder_w1 {
tensor<float>(x[768], y[256])
file: models/cross_encoder_w1.tensored
}
function cross_encoder_logits() {
expression: reduce(
matmul(
concat(query(query_embedding), attribute(embedding), t),
constant(cross_encoder_w1)
),
sum, y
)
}
second-phase {
expression: cross_encoder_logits
rerank-count: 50
}
}
这种能力使得 Vespa 可以在查询时执行小型模型的推理,而无需额外的模型服务基础设施。对于需要实时个性化和上下文感知排序的场景,这一特性极具价值。
性能调优与生产部署
HNSW 参数调优
Vespa 的 HNSW 索引实现提供了多个可调参数,影响索引大小、构建速度和查询性能:
| 参数 | 默认值 | 说明 | 调优建议 |
|---|---|---|---|
| max-links-per-node | 16 | HNSW 图中每个节点的最大连接数 | 16-32,值越大召回率越高但内存占用越大 |
| neighbors-to-explore-at-insert | 200 | 插入时搜索的邻居数量 | 100-500,值越大索引质量越高但构建越慢 |
| max-links-per-node-upper | max-links-per-node * 2 | 节点连接数上限(用于增量更新) | 保持默认或设为 max-links-per-node 的 1.5 倍 |
在查询侧,
1 | targetHits |
参数是最关键的性能旋钮:
1
2
3
4
5 # 低延迟场景:减少 targetHits
{targetHits: 50}nearestNeighbor(embedding, query_embedding)
# 高召回场景:增大 targetHits
{targetHits: 500}nearestNeighbor(embedding, query_embedding)
建议通过基准测试确定适合你数据集的 targetHits 值。一般来说,100-200 是一个合理的起点,然后根据召回率/延迟曲线进行调整。
内存与存储规划
Vespa 的内存使用主要由以下几部分构成:
- 向量属性:768 维 float32 向量 = 3KB/文档,100 万文档约 3GB
- HNSW 索引:约为向量数据大小的 1.5-2 倍,100 万文档约 4.5-6GB
- 倒排索引:取决于文本量和分词策略,通常为原始文本的 30-50%
- 文档存储:使用内存映射文件,热数据自动缓存在内存中
对于生产环境,建议每个内容节点配置的 JVM 堆内存不超过物理内存的 50%,剩余空间留给操作系统缓存和 HNSW 索引的内存映射。
Kubernetes 生产部署
Vespa 官方提供了 Helm Chart 和 Operator,支持在 Kubernetes 上部署:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 # 使用 Vespa Operator 部署
apiVersion: vespa.io/v1alpha1
kind: VespaApplication
metadata:
name: search-app
spec:
disk: 100Gi
memory: 32Gi
cpu: 8
nodes: 3
content:
resources:
requests:
memory: 24Gi
cpu: 6
limits:
memory: 32Gi
cpu: 8
container:
resources:
requests:
memory: 8Gi
cpu: 4
limits:
memory: 12Gi
cpu: 6
deployment:
replicas:
content: 3
container: 2
关键运维要点:
- Content 节点需要本地 SSD,避免网络存储的性能瓶颈
- 配置
1redundancy: 2
确保单节点故障时数据不丢失
- 使用
1vespa-logctl
控制日志级别,生产环境建议 WARNING 级别
- 通过
1vespa-get-config
监控集群状态,配合 Prometheus + Grafana 实现可视化监控

与 RAG 系统的集成实践
Vespa 在 RAG 系统中扮演检索引擎的角色,其混合检索能力可以显著提升检索质量。以下是一个完整的 RAG 集成示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 import requests
from openai import OpenAI
VESPA_ENDPOINT = "http://localhost:8080"
client = OpenAI()
def hybrid_search(query, top_k=10):
# 执行 Vespa 混合检索
query_embedding = get_embedding(query)
vespa_query = {
"yql": "select * from article where "
"({targetHits: 100}nearestNeighbor(embedding, query_embedding)) "
"or userQuery()",
"query": query,
"query_embedding": query_embedding,
"ranking": "hybrid",
"hits": top_k,
"timeout": "500ms"
}
response = requests.post(
f"{VESPA_ENDPOINT}/search/",
json=vespa_query
)
results = response.json()
documents = []
for hit in results.get("root", {}).get("children", []):
fields = hit["fields"]
documents.append({
"title": fields.get("title", ""),
"content": fields.get("content", ""),
"relevance": hit.get("relevance", 0),
"source": "vespa"
})
return documents
def rag_pipeline(query):
# 完整的 RAG 流水线
# 1. 混合检索
docs = hybrid_search(query, top_k=5)
# 2. 构建上下文
context = "
".join([
f"【{d['title']}】(相关度: {d['relevance']:.3f})
{d['content']}"
for d in docs
])
# 3. LLM 生成
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "基于以下参考资料回答问题,如果参考资料中没有相关信息,请明确说明。"},
{"role": "user", "content": f"参考资料:
{context}
问题:{query}"}
],
temperature=0.1
)
return response.choices[0].message.content
这个集成方案的关键优势:
- 单次查询获取多路结果:语义和词汇匹配结果在 Vespa 内部合并,无需在应用层做两路查询和去重
- 实时相关性评分:hybrid 排序配置中的加权融合在查询时计算,可以动态调整权重而无需重建索引
- 结构化过滤:在检索时即可按分类、时间等维度过滤,避免无关结果进入 LLM 上下文
常见问题与最佳实践
向量维度选择
Vespa 支持任意维度的向量,但不同维度在索引大小和查询性能上有显著差异。常见的嵌入模型及其维度:
| 模型 | 维度 | 适用场景 | 单向量大小 |
|---|---|---|---|
| OpenAI text-embedding-3-small | 1536 | 通用语义搜索 | 6KB |
| BGE-base-zh-v1.5 | 768 | 中文语义搜索 | 3KB |
| BGE-small-zh-v1.5 | 512 | 低延迟中文搜索 | 2KB |
| Cohere embed-v3 | 1024 | 多语言搜索 | 4KB |
对于大规模数据集(千万级以上),建议使用 512 维或更低的模型以减少内存占用和 HNSW 索引大小。Vespa 也支持通过
1 | reduce |
操作在查询时进行降维,但这会损失精度。
索引更新策略
Vespa 的 HNSW 索引支持实时更新,但高频更新可能导致索引碎片化。建议:
- 对于批量导入场景,使用
1mode="index"
并在导入后执行
1compact操作
- 对于增量更新场景,使用
1mode="index"
的默认行为,Vespa 会自动处理增量更新
- 避免在同一文档上频繁更新向量字段,这会导致 HNSW 图的局部退化
- 对于需要频繁更新的场景,考虑使用
1fast-search
属性替代 HNSW 索引,在精确搜索和实时性之间取得平衡
查询超时与降级
在生产环境中,合理的超时设置是系统稳定性的关键:
1
2
3
4
5
6
7 # 设置查询超时
{
"yql": "select * from article where ...",
"timeout": "300ms",
"ranking": "hybrid",
"hits": 10
}
Vespa 在超时后会返回已收集的部分结果,而非空结果。这确保了即使在负载高峰期,用户也能获得部分有效的搜索结果。建议在应用层监控超时率和结果完整性,当超时率超过阈值时自动降级到更简单的排序策略。
总结与展望
Vespa 作为一个成熟的实时搜索平台,在向量搜索领域提供了独特的价值主张:它不是又一个专用向量数据库,而是一个能够统一处理向量检索、全文搜索、结构化过滤和实时排序的全栈搜索引擎。这种统一性在复杂业务场景中尤为重要——当你的搜索需求超越简单的向量 Top-K 时,Vespa 的查询表达力和计算能力可以显著降低系统复杂度。
对于正在评估向量搜索方案的团队,建议从以下维度考虑是否选择 Vespa:
- 如果你的搜索需求纯向量检索,且数据量在百万级以内,Milvus 或 Qdrant 可能更轻量
- 如果你需要混合检索 + 实时排序 + 复杂过滤,Vespa 是目前最完整的解决方案
- 如果你需要大规模(亿级以上)分布式部署,Vespa 的内容集群架构在水平扩展上更具优势
- 如果你的团队有搜索引擎开发经验,Vespa 的学习曲线会更平缓
未来,随着多模态检索需求的增长,Vespa 的张量运算能力将变得更加重要——图像、视频、音频的嵌入向量可以在同一个文档中与文本字段协同工作,为多模态 RAG 系统提供统一的检索基础设施。
汤不热吧