欢迎光临

Vespa 向量搜索引擎深度实战:从实时计算架构到混合检索生产部署指南

为什么选择 Vespa 而非传统向量数据库

在向量搜索领域,大多数人首先想到的是 Pinecone、Milvus 或 Qdrant 这样的专用向量数据库。然而,Yahoo(现 Verizon Media)开源的 Vespa 搜索引擎在实时搜索和推荐系统中已经历了超过十年的生产验证,它不仅能高效处理向量检索,还能同时完成全文搜索、结构化过滤和实时排序计算——这一切在同一个查询中完成,无需额外的系统编排。

Vespa 的核心优势在于其统一查询模型:你可以在一次查询中同时执行向量近邻搜索、BM25 全文检索、结构化属性过滤,以及基于机器学习模型的实时打分排序。这种能力在 RAG(检索增强生成)和推荐系统中尤为关键,因为真实的业务场景往往需要多种信号的综合判断,而非单一的向量相似度。

与专用向量数据库相比,Vespa 的架构差异体现在以下几个维度:

  • 查询表达能力:Vespa 使用自己的查询语言 YQL,支持复杂的布尔组合、嵌套查询和排序表达式,而大多数向量数据库仅支持简单的 Top-K 检索加过滤
  • 实时计算能力:Vespa 的排序表达式可以在查询时执行任意数学运算和模型推理,而非仅依赖预计算的静态分数
  • 内容集群架构:Vespa 采用分布式内容集群设计,数据按文档粒度自动分片和冗余,支持在线扩容和缩容
  • 多模态支持:同一文档可以同时包含文本、向量、张量等多种类型的字段,查询时可以混合使用

本文将从 Vespa 的架构设计出发,逐步演示如何构建一个生产级的混合检索系统,涵盖向量索引配置、YQL 查询语法、实时排序表达式、性能调优和部署策略。

Vespa Architecture Overview

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 定义了几个关键设计:

  • 1
    embedding

    字段使用

    1
    tensor<float>(x[768])

    类型,表示 768 维浮点向量,并启用了 HNSW 索引

  • 1
    index: enable-bm25

    为文本字段启用了 BM25 评分,这是混合检索的基础

  • 1
    rank-profile

    定义了两种排序策略:纯语义排序和混合排序

  • 混合排序中通过
    1
    0.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 的文档写入是实时生效的——文档一旦写入成功,即可在查询中被检索到,无需等待索引刷新。这种实时性对于新闻推荐、电商搜索等时效性要求高的场景至关重要。

Data Flow Architecture

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,避免网络存储的性能瓶颈
  • 配置
    1
    redundancy: 2

    确保单节点故障时数据不丢失

  • 使用
    1
    vespa-logctl

    控制日志级别,生产环境建议 WARNING 级别

  • 通过
    1
    vespa-get-config

    监控集群状态,配合 Prometheus + Grafana 实现可视化监控

Monitoring Dashboard

与 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 索引支持实时更新,但高频更新可能导致索引碎片化。建议:

  • 对于批量导入场景,使用
    1
    mode="index"

    并在导入后执行

    1
    compact

    操作

  • 对于增量更新场景,使用
    1
    mode="index"

    的默认行为,Vespa 会自动处理增量更新

  • 避免在同一文档上频繁更新向量字段,这会导致 HNSW 图的局部退化
  • 对于需要频繁更新的场景,考虑使用
    1
    fast-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 系统提供统一的检索基础设施。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Vespa 向量搜索引擎深度实战:从实时计算架构到混合检索生产部署指南
分享到: 更多 (0)