数据库基础体系 · 第 64/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Weaviate 基础:Collection、Vectorizer、过滤、混合检索和多租户

Weaviate 是一个面向对象数据、全文检索和向量检索的数据库。它的核心使用方式不是先建立一张关系表,再为某一列额外创建向量索引,而是定义一个 Collection,为其中的对象配置属性、倒排索引、向量索引和向量化方式,然后通过相应的查询接口访问这些索引。

本文使用以下术语:

  • Collection:对象的逻辑集合,类似关系数据库中的表,但同时承载属性、倒排索引和向量索引配置。
  • Vectorizer:把文本或其他输入转换为向量的组件。
  • 过滤:在检索过程中根据结构化条件缩小候选对象集合。
  • 混合检索:把关键词检索和向量检索的结果融合。
  • 多租户:在同一个 Collection 内按租户划分数据分片和访问上下文。

示例以 Weaviate Cloud 或启用了相应模块的 Weaviate 部署为边界,并使用 Python 客户端 v4 风格 API。具体的客户端小版本、向量化模块和云服务配置可能不同,正式项目应固定 weaviate-client 版本,并对照该版本的官方文档。


一、Collection 不只是“表名”

1. Collection 的职责

在 Weaviate 中,一个 Collection 通常包含以下部分:

Collection
├── 属性定义 properties
├── 属性索引 inverted index
├── 一个或多个向量索引 vector index
├── 向量化配置 vectorizer
├── 分片配置 sharding
├── 复制配置 replication
└── 可选的多租户配置 multi-tenancy

例如,可以定义一个 Article Collection:

Article
├── title: TEXT
├── body: TEXT
├── category: TEXT
├── published_at: DATE
└── word_count: INT

对于同一个对象,Weaviate 可以同时保存:

  1. 原始属性值,例如标题和正文;
  2. 属性的倒排索引,用于 BM25 和结构化过滤;
  3. 由正文生成的向量,用于近似近邻搜索;
  4. 其他元数据,例如 UUID、租户和时间戳。

因此,向量检索并不替代原始文本。RAG 系统最终需要引用原文、标题、来源和权限信息,这些内容仍应作为对象属性保存。

2. Collection 与旧版 Class 的关系

Weaviate 的早期 API 使用 Class 表示数据集合。较新的概念和客户端 API 使用 Collection。在理解现有资料或旧项目时,可以把:

旧术语:Class
新术语:Collection

视为同一层级的概念,但具体 API、配置字段和客户端写法可能不同。新项目应优先使用当前客户端文档中的 Collection API,而不是混用旧版 REST schema 写法。

3. 属性类型决定检索语义

常见属性类型包括:

  • TEXT:参与文本搜索和文本向量化;
  • INTNUMBER:支持数值比较过滤;
  • DATE:支持时间比较过滤;
  • BOOLEAN:支持布尔过滤;
  • TEXT_ARRAY、数值数组等:支持数组相关查询;
  • GEO_COORDINATES:用于地理位置相关能力;
  • OBJECTOBJECT_ARRAY:表示嵌套结构,但具体索引和查询能力要以部署版本为准。

文本属性还涉及 tokenization。tokenization 决定文本如何拆词,进而影响 BM25 和 EqualLike 等过滤行为。比如,面向自然语言的文本字段通常使用适合分词的配置;邮箱、订单号和 SKU 这类标识符则往往需要避免被拆成不希望的词项。

这不是纯粹的展示配置。改变 tokenization 可能改变已有倒排索引的行为,通常需要重新导入或重建数据来验证效果。


二、Vectorizer:对象如何获得向量

1. Vectorizer 的输入和输出

Vectorizer 的作用可以抽象为:

f:XRdf: X \rightarrow \mathbb{R}^{d}

其中:

  • XX 是输入文本或多模态内容;
  • dd 是向量维度;
  • f(x)f(x) 是输入 xx 的向量。

如果 Collection 的向量化配置指定了 titlebody,插入对象时,Weaviate 会根据这些属性生成向量:

对象属性
  title = "向量数据库简介"
  body  = "向量数据库用于近似近邻搜索..."
        │
        ▼
Vectorizer
        │
        ▼
向量 v ∈ R^d
        │
        ▼
写入向量索引

查询时,near_text 也会执行类似过程:

查询文本 "如何进行近似近邻搜索"
        │
        ▼
同一个 Vectorizer
        │
        ▼
查询向量 q ∈ R^d
        │
        ▼
在向量索引中查找接近 q 的对象

插入和查询必须使用语义兼容的向量化模型。若写入时使用模型 A、查询时使用模型 B,即使两个模型都输出相同维度,也不能假设向量空间相同。

2. 由 Weaviate 生成向量,还是由应用传入向量

常见有两种方式。

方式一:Weaviate 管理向量化

配置一个 Weaviate 支持的向量化模块,例如某个云端 embedding 服务或本地 embedding 服务:

import weaviate
from weaviate.classes.config import Configure, DataType, Property

client = weaviate.connect_to_local()

try:
    articles = client.collections.create(
        name="Article",
        vector_config=Configure.Vectors.text2vec_openai(),
        properties=[
            Property(name="title", data_type=DataType.TEXT),
            Property(name="body", data_type=DataType.TEXT),
            Property(name="category", data_type=DataType.TEXT),
        ],
    )
finally:
    client.close()

这个示例的前提是:

  1. Weaviate 部署启用了对应的向量化集成;
  2. 运行环境提供该模块所需的凭据;
  3. 当前 Python 客户端支持 Configure.Vectors.text2vec_openai() 这一配置形式。

如果模块未启用,Collection 创建或插入时会失败,常见表现是模块不可用、向量化配置无效或请求无法调用 embedding 服务。

方式二:应用自行生成向量

如果 embedding 由应用、专用模型服务或数据管道负责,可以把 Collection 配置为不自动向量化,并在写入和查询时显式提供向量。

这种方式的关键不是“把向量传进去”这么简单,而是应用必须保证:

  • 所有对象使用相同的模型和预处理规则;
  • 查询向量使用相同的模型;
  • 向量维度一致;
  • 模型升级时能够区分新旧向量,或重新生成全部向量。

如果模型从 f1f_1 切换到 f2f_2,则旧对象位于空间 f1(X)f_1(X),新查询位于空间 f2(X)f_2(X)。直接混用的结果通常不是一个明确的异常,而是检索质量悄然下降。

3. 多向量与命名向量

一个对象可能需要多个向量,例如:

  • 标题向量;
  • 正文向量;
  • 图片向量;
  • 不同语义目标对应的向量。

这类配置通常称为 named vectors。它解决的是“同一个对象存在多个向量空间”的问题,但也增加了查询时的明确性要求:查询必须指定使用哪个向量空间,不能把不同模型生成的向量当成一个空间进行比较。

生产环境中,命名向量适合以下场景:

用户查询 ──► title_vector
图片查询 ──► image_vector
长文语义查询 ──► body_vector

它不等于把多个向量简单拼接。拼接会改变维度和距离分布;命名向量则保留多个独立的索引和语义空间。


三、距离、向量索引和近似搜索

1. 距离与相似度

向量检索首先需要定义两个向量之间的距离或相似度。常见度量包括余弦距离、点积和欧氏距离。

以余弦相似度为例:

cos(q,x)=qxqx\operatorname{cos}(q,x) = \frac{q \cdot x}{\|q\|\|x\|}

其中:

  • qq 是查询向量;
  • xx 是对象向量;
  • qxq \cdot x 是点积;
  • q\|q\|x\|x\| 是向量长度。

余弦相似度越大,方向越接近。若向量已经归一化,余弦相似度就等价于点积。

Weaviate 的查询结果通常可以返回距离或附加元数据。注意“距离越小越好”和“相似度越大越好”是两个相反方向的概念。调试混合检索时,必须确认客户端返回的是哪一种量。

2. 为什么需要近似近邻搜索

精确检索需要把查询向量与所有 NN 个对象逐一比较。若每个向量维度为 dd,单次查询的比较成本近似为:

O(Nd)O(Nd)

对象数量增大后,这种方式会变慢。因此,向量数据库通常使用近似近邻索引。Weaviate 常见的向量索引实现是 HNSW。

HNSW 可以理解为多层图:

高层:少量节点,快速跨越远距离
中层:更多节点,逐步靠近目标区域
底层:完整或近完整的邻接图

一次查询的大致过程是:

  1. 从高层入口开始;
  2. 访问邻居;
  3. 如果邻居更接近查询向量,则移动到邻居;
  4. 在当前层无法继续改善时,下降到下一层;
  5. 在底层维护候选集;
  6. 返回近似最近邻。

ef、连接数等索引参数会影响召回率、内存和延迟。这里的“近似”意味着系统可能不返回真实的全局最近邻。提高搜索宽度通常有助于召回,但会增加查询成本。

3. 向量索引和倒排索引是两套索引

对于同一个 Collection:

正文属性 ──► 倒排索引 ──► BM25、文本过滤
对象向量 ──► 向量索引 ──► near_vector、near_text

这两类索引不能互相替代:

  • 倒排索引擅长精确词项、词频和结构化条件;
  • 向量索引擅长语义相近但词面不同的内容。

例如:

文档:如何降低数据库写放大
查询:减少存储系统的额外写入

词面可能差异很大,但语义相近,向量检索有优势。相反,查询:

错误码 0x80070005

通常更适合关键词或精确过滤,而不是依赖语义相似度。


四、过滤:先定义候选范围,再进行检索

1. 过滤表达式

过滤是对对象属性施加逻辑条件。例如:

category = "database"
AND word_count > 1000

可以写成:

from weaviate.classes.query import Filter

filters = (
    Filter.by_property("category").equal("database")
    & Filter.by_property("word_count").greater_than(1000)
)

常见操作包括:

  • equal
  • not_equal
  • greater_than
  • greater_or_equal
  • less_than
  • less_or_equal
  • like
  • contains_any
  • contains_all
  • is_null

逻辑组合通常使用:

  • & 表示 AND;
  • | 表示 OR;
  • ~ 或对应的否定 API 表示 NOT,具体写法应以客户端版本为准。

完整查询示例:

import weaviate
from weaviate.classes.query import Filter

client = weaviate.connect_to_local()

try:
    articles = client.collections.get("Article")

    filters = (
        Filter.by_property("category").equal("database")
        & Filter.by_property("word_count").greater_than(1000)
    )

    result = articles.query.near_text(
        query="如何设计向量检索系统",
        filters=filters,
        limit=5,
        return_metadata=["distance"],
    )

    for obj in result.objects:
        print(obj.properties)
        print(obj.metadata.distance)
finally:
    client.close()

这个请求的语义是:

  1. 把查询文本向量化;
  2. 在满足 category = databaseword_count > 1000 的对象中进行向量检索;
  3. 返回最多五个结果;
  4. 同时返回向量距离。

过滤条件不是返回后由 Python 再筛选。若把过滤放在应用层,可能发生:

数据库先返回 5 个语义结果
应用过滤后只剩 1 个

这会导致结果数量不足,也可能让系统错误地认为“数据库没有更多相关结果”。

2. EqualLike 和“包含”不是同一回事

一个常见误解是把 Equal 当作自然语言搜索。

假设属性为:

title = "Weaviate 向量数据库基础"

那么:

  • Equal("Weaviate 向量数据库基础") 表示属性值或其索引语义上的等值匹配;
  • Like("*向量数据库*") 表示通配模式匹配;
  • BM25 查询表示按词项相关性排序;
  • 向量查询表示按向量空间中的距离排序。

它们的目标不同。字段的 tokenization 还会影响文本等值和通配行为,因此不能只看 Python 字符串是否相等来推断数据库中的过滤结果。

3. 过滤如何影响向量检索

理想语义是:

TopK({xP(x)},distance(q,x))\operatorname{TopK} \left( \{x \mid P(x)\}, \operatorname{distance}(q,x) \right)

其中:

  • P(x)P(x) 是过滤谓词;
  • 只有满足 P(x)P(x) 的对象才属于候选集合;
  • TopK 再从候选集合中选择距离最近的对象。

实现上,数据库需要协调倒排索引与向量索引。不同版本和索引配置可能采用不同的预过滤、候选扩展或图遍历策略。工程上不应假设“过滤永远零成本”,尤其是以下情况:

  • 过滤条件极其稀疏;
  • 过滤条件选择性很高;
  • 数据分布不均匀;
  • limit 较大;
  • 分片数量较多。

如果过滤后只有很少对象,但向量索引的候选搜索仍然需要扩大范围,延迟和召回都可能发生变化。应使用真实数据评测,而不是只在无过滤数据集上测向量查询性能。

4. 过滤字段与权限边界

过滤可以表达:

tenant_id = "tenant-a"
AND document_status = "published"
AND user_group CONTAINS "finance"

但过滤本身不自动构成安全边界。若应用忘记添加权限过滤,数据库不会凭空知道当前用户只能访问哪些对象。

更安全的做法是:

  1. 租户隔离使用 Weaviate 的多租户上下文;
  2. 用户、组织或文档级授权由应用认证和授权层决定;
  3. 必须执行的权限条件在服务端统一注入;
  4. 不允许客户端直接提交任意过滤表达式来绕过权限逻辑。

五、混合检索:关键词和向量如何合并

1. 为什么需要混合检索

关键词检索和向量检索的失败模式不同。

关键词检索通常擅长:

  • 专有名词;
  • 产品型号;
  • 错误码;
  • 精确术语;
  • 词频和字段权重。

向量检索通常擅长:

  • 同义表达;
  • 语义改写;
  • 用户问题与文档表述不同;
  • 不包含完全相同关键词的相关内容。

因此,对查询:

Kafka consumer rebalance 失败

关键词搜索可以准确抓住 Kafkaconsumerrebalance。对查询:

为什么消息消费组会重复分配分区

向量检索可能更容易找到关于 consumer group rebalance 的说明。

2. 混合检索的基本过程

混合检索不是把文本直接拼接成一个向量,而是分别执行两条检索路径:

查询文本
  ├──► BM25 / 倒排索引 ──► 关键词结果
  └──► Vectorizer ───────► 向量结果
                         │
                         ▼
                    分数融合
                         │
                         ▼
                    最终排序

设某对象的 BM25 分数为 b(x)b(x),向量检索分数为 v(x)v(x)。由于两者的数值范围和含义不同,不能直接相加。需要先归一化:

b^(x)=b(x)bminbmaxbmin\hat b(x) = \frac{b(x)-b_{\min}}{b_{\max}-b_{\min}}

v^(x)=v(x)vminvmaxvmin\hat v(x) = \frac{v(x)-v_{\min}}{v_{\max}-v_{\min}}

然后使用权重 α\alpha 融合:

s(x)=(1α)b^(x)+αv^(x)s(x) = (1-\alpha)\hat b(x) + \alpha\hat v(x)

其中:

  • α=0\alpha=0 更偏向关键词;
  • α=1\alpha=1 更偏向向量;
  • α=0.5\alpha=0.5 表示两者权重相同。

Weaviate 的混合检索支持 BM25 与向量检索结果融合,并允许通过 alpha 调整倾向。具体分数融合方式和默认行为属于版本相关语义,应以当前版本文档为准;在较新的实现中,常见的是基于相对分数的融合,而不是简单使用原始 BM25 分数和距离。

3. 混合检索示例

import weaviate
from weaviate.classes.query import Filter

client = weaviate.connect_to_local()

try:
    articles = client.collections.get("Article")

    filters = Filter.by_property("category").equal("database")

    result = articles.query.hybrid(
        query="如何降低向量检索的误召回",
        alpha=0.65,
        filters=filters,
        limit=5,
        return_metadata=["score"],
    )

    for obj in result.objects:
        print(obj.properties["title"])
        print("hybrid score:", obj.metadata.score)
finally:
    client.close()

每个参数的语义是:

  • query:同时作为 BM25 查询文本和向量化输入;
  • alpha=0.65:整体上更偏向向量结果;
  • filters:只在符合条件的对象中进行混合检索;
  • limit=5:返回最终融合后的前五个对象;
  • score:返回融合分数,不能直接解释为概率。

4. 一个完整的融合算例

假设 BM25 和向量检索分别返回以下对象:

对象 BM25 原始分数 向量相似度
A 12 0.71
B 8 0.90
C 2 0.85

BM25 范围为 [2,12][2,12],向量相似度范围为 [0.71,0.90][0.71,0.90]

归一化后:

对象 BM25 归一化 向量归一化
A 1.00 0.00
B 0.60 1.00
C 0.00 0.74

α=0.6\alpha=0.6

s(A)=0.4×1.00+0.6×0.00=0.40s(A)=0.4 \times 1.00 + 0.6 \times 0.00=0.40

s(B)=0.4×0.60+0.6×1.00=0.84s(B)=0.4 \times 0.60 + 0.6 \times 1.00=0.84

s(C)=0.4×0.00+0.6×0.74=0.444s(C)=0.4 \times 0.00 + 0.6 \times 0.74=0.444

最终排序为:

B > C > A

如果改为 α=0.2\alpha=0.2

s(A)=0.8s(A)=0.8

s(B)=0.8×0.60+0.2×1.00=0.68s(B)=0.8 \times 0.60 + 0.2 \times 1.00=0.68

s(C)=0.2×0.74=0.148s(C)=0.2 \times 0.74=0.148

排序变成:

A > B > C

这说明 alpha 不是“召回率开关”,而是改变两种检索证据在最终排序中的相对影响。

5. 混合检索的边界

混合检索不能解决以下问题:

  • 文档根本没有被正确切分;
  • embedding 模型不适合领域语料;
  • BM25 字段配置错误;
  • 权限过滤缺失;
  • 文档内容过期;
  • 最终结果缺少引用位置。

RAG 中常见的数据流是:

用户问题
  └──► 混合检索
          └──► 过滤、去重、重排
                  └──► 返回文档片段和来源
                          └──► 生成答案与引用

Weaviate 负责检索数据层;重排模型、上下文拼接、引用格式和答案生成通常属于应用或上层检索服务。混合检索结果的 score 不能直接当作事实置信度,也不能替代引用验证。


六、多租户:Collection 内的逻辑隔离和分片

1. 多租户解决什么问题

假设 SaaS 应用有多个客户:

tenant-a:客户 A 的文档
tenant-b:客户 B 的文档
tenant-c:客户 C 的文档

如果所有文档都放在一个普通 Collection 中,应用必须为每个查询附加:

tenant_id = 当前租户

这种方式容易因代码遗漏导致跨租户泄露。

启用 Weaviate 多租户后,租户成为 Collection 数据访问上下文的一部分:

Article / tenant-a
Article / tenant-b
Article / tenant-c

查询必须指定租户。数据库据此选择对应租户的数据分片,而不是在所有租户的数据上搜索后再依赖应用过滤。

多租户提供的是数据库层面的租户数据隔离和分片语义,不等于完整的身份认证与授权系统。应用仍需验证调用者是否有权访问指定租户。

2. 创建多租户 Collection

Python 客户端示例:

import weaviate
from weaviate.classes.config import (
    Configure,
    DataType,
    Property,
)

client = weaviate.connect_to_local()

try:
    documents = client.collections.create(
        name="Document",
        vector_config=Configure.Vectors.text2vec_openai(),
        multi_tenancy_config=Configure.multi_tenancy(
            enabled=True,
        ),
        properties=[
            Property(name="title", data_type=DataType.TEXT),
            Property(name="body", data_type=DataType.TEXT),
            Property(name="source_url", data_type=DataType.TEXT),
        ],
    )

    documents.tenants.create(
        tenants=[
            {"name": "tenant-a"},
            {"name": "tenant-b"},
        ]
    )
finally:
    client.close()

这段代码包含两个不同的生命周期操作:

  1. 创建 Collection;
  2. 创建租户。

创建 Collection 不等于自动创建所有租户。租户通常需要显式管理。某些版本支持自动租户创建,但生产环境应谨慎使用,因为拼写错误或未经校验的租户名称可能导致产生意外分片。

3. 在租户上下文中写入和查询

import weaviate

client = weaviate.connect_to_local()

try:
    tenant_a = client.collections.use(
        "Document",
        tenant="tenant-a",
    )

    tenant_a.data.insert(
        properties={
            "title": "租户 A 的向量检索说明",
            "body": "这条文档只属于租户 A。",
            "source_url": "https://example.com/a/vector-search",
        }
    )

    result = tenant_a.query.hybrid(
        query="向量检索",
        alpha=0.5,
        limit=3,
    )

    for obj in result.objects:
        print(obj.properties)
finally:
    client.close()

查询上下文已经绑定到 tenant-a,因此结果只来自这个租户。

如果使用不存在的租户,常见结果是请求失败,而不是自动返回空集合。错误可能发生在:

  • 租户不存在;
  • 租户处于不可读状态;
  • Collection 未启用多租户,却传入了租户;
  • Collection 启用了多租户,却没有传入租户。

这些错误应在应用层明确处理,不能把它们统一转换成“没有搜索结果”,否则会掩盖配置错误。

4. 多租户下的向量化流程

多租户不会改变向量化模型本身。数据流仍然是:

tenant-a 的对象
  └──► 同一个 Vectorizer
          └──► tenant-a 分片内的向量索引

tenant-b 的对象
  └──► 同一个 Vectorizer
          └──► tenant-b 分片内的向量索引

同一个 Collection 的租户通常共享 Collection 级别的 schema 和向量化配置。若不同租户需要完全不同的 embedding 模型、属性结构或距离度量,单纯多租户并不适合;应考虑不同 Collection,甚至不同数据库或部署。

5. 租户状态与数据生命周期

租户不只是一个字符串。实际部署中通常还需要管理租户状态,例如:

ACTIVE       可读写
INACTIVE     暂停访问
OFFLOADED    数据从在线资源中卸载,恢复前不可正常查询

具体状态名称和可用操作取决于 Weaviate 版本与部署形态。典型生命周期是:

创建租户
  └──► 写入数据
          └──► 正常查询
                  ├──► 暂停
                  ├──► 卸载以降低资源占用
                  └──► 恢复后继续查询

不能把“租户没有返回结果”和“租户被卸载”混为一谈。后者通常是状态错误或资源未恢复,应查看租户状态、服务日志和指标。

6. 多租户的分片取舍

多租户通常适合:

  • 大量客户共享同一套 schema;
  • 每个租户的数据需要逻辑隔离;
  • 希望按租户扩缩容、迁移或管理生命周期;
  • 查询天然带有租户边界。

但租户数量和数据规模会影响运维成本。若为每个极小客户都创建一个分片,可能带来:

  • 分片元数据增多;
  • 资源利用率下降;
  • 租户状态管理复杂;
  • 备份和恢复粒度变细。

反过来,如果把所有租户放在普通 Collection 中,仅靠 tenant_id 过滤,虽然分片更简单,但隔离依赖应用代码,错误代价更高。


七、写入、批处理和事务边界

1. 单对象写入不是跨对象事务

Weaviate 的对象写入接口不应被理解为关系数据库中的任意多对象事务。以下操作:

插入对象 A
插入对象 B
更新对象 C
删除对象 D

通常不是一个可以自动整体提交或整体回滚的 ACID 事务。中途网络断开时,可能出现:

A 已写入
B 已写入
C 尚未更新
D 尚未删除

因此,批量摄取必须具备:

  • 幂等对象 ID;
  • 可重试;
  • 失败记录;
  • 断点续传;
  • 写入后校验;
  • 必要时的补偿删除或更新。

批处理提高吞吐,但不等于事务。批请求部分失败时,应用必须检查每个对象的结果,而不能只判断 HTTP 请求是否成功。

2. 一个可靠的摄取状态机

可以把文档摄取抽象成:

SOURCE_READ
   │
   ▼
CHUNKED
   │
   ▼
EMBEDDED
   │
   ▼
WRITTEN
   │
   ▼
VERIFIED

失败路径包括:

embedding 超时 ──► 重试或 DEAD_LETTER
写入失败       ──► 按对象 ID 重试
校验失败       ──► 标记异常并重新生成向量
模型版本变化   ──► 建立新 Collection 或重建向量

如果使用 Weaviate 的远程 Vectorizer,写入时还依赖外部 embedding 服务的可用性。网络、配额、凭据和模型限流都可能成为写入路径的一部分。

3. Schema 变更和模型变更不是同一类操作

增加一个普通属性,通常是 schema 变更。改变以下内容则可能影响已有数据的检索语义:

  • 向量化模型;
  • 向量化的源属性;
  • tokenization;
  • 距离度量;
  • 命名向量配置;
  • 分片或复制策略。

尤其是 embedding 模型变更。一个安全的迁移流程通常是:

建立新 Collection
  └──► 使用新模型重新摄取
          └──► 离线评测
                  └──► 双读或切换流量
                          └──► 保留旧 Collection

直接覆盖旧向量会让新旧数据处于不同的向量空间,导致问题难以诊断。


八、生产环境中的故障表现与诊断路径

1. 查询无结果

可能原因并不只有“没有相关文档”:

  1. 过滤条件拼写错误;
  2. 文本属性的 tokenization 与预期不同;
  3. 查询使用了错误的租户;
  4. 租户处于不可查询状态;
  5. Collection 没有正确配置向量化;
  6. 使用了错误的命名向量;
  7. 写入成功但异步索引尚未完成;
  8. 过滤条件过于严格;
  9. 查询文本使用了与写入不同的模型。

诊断顺序应先验证边界,再验证召回:

1. 能否读取 Collection 配置?
2. 当前租户是否存在且可用?
3. 不加过滤能否 fetch 到对象?
4. 用精确对象 ID 能否读取?
5. 只做 BM25 能否返回?
6. 只做向量检索能否返回?
7. 最后再检查 hybrid 的 alpha 和过滤条件。

不要一开始就调整 alpha。如果对象根本没有写入,调整融合权重不会产生任何作用。

2. 向量检索结果看起来不相关

优先检查:

  • 写入属性是否为空;
  • Vectorizer 是否配置了预期的属性;
  • 写入时是否发生了 embedding 调用失败;
  • 查询和写入是否使用同一个模型;
  • 文档切分是否过长或过短;
  • 是否把标题、正文、代码和元数据混在了同一字段;
  • 是否使用了错误的 named vector;
  • 距离度量是否与模型预期一致。

一个常见反例是:向量化只配置了 title,但工程师以为正文也参与了向量生成。结果是正文内容对 near_text 几乎没有影响,而 BM25 可能仍然可以搜索正文,最终造成“关键词结果正常、向量结果异常”的表象。

3. 混合检索分数不能跨请求直接比较

相对分数融合通常依赖当前请求返回结果中的最小值和最大值。因此,某个请求中的 0.8 不一定比另一个请求中的 0.6 更“可信”。混合分数主要用于同一次查询内部排序,不应未经校准就当作跨查询阈值。

如果业务需要“低于某个相关性阈值就不回答”,应使用专门的评测数据校准阈值,并结合:

  • BM25 分数;
  • 向量距离;
  • 混合排序名次;
  • 重排分数;
  • 是否存在足够的引用证据。

4. 多租户串数据的典型原因

多租户泄露通常不是 HNSW 本身把数据混在一起,而是应用访问上下文错误,例如:

# 错误风险:使用了未绑定租户的 Collection
documents = client.collections.get("Document")
documents.query.hybrid(query="...", limit=10)

在启用多租户的 Collection 中,必须显式选择租户:

documents = client.collections.use(
    "Document",
    tenant=current_tenant,
)

同时,current_tenant 不能直接信任请求体中的任意字符串。它应来自经过认证的会话、令牌或服务端授权结果。


九、如何选择 Collection、向量化和检索方式

1. 选择普通 Collection 还是多租户 Collection

可以按以下条件判断:

租户是否共享相同 schema 和模型?
    ├── 否:考虑不同 Collection
    └── 是
        │
        每个租户是否需要数据库级访问上下文?
            ├── 是:启用多租户
            └── 否:普通 Collection + 严格权限过滤

如果只是给文档增加一个 organization_id 字段,并不自动获得多租户隔离。它只是一个属性过滤条件。

2. 选择 Vectorizer 管理方式

场景 适合方式
希望快速构建系统 由 Weaviate 调用集成的 Vectorizer
已有统一 embedding 服务 应用生成向量后写入
需要多个语义空间 Named vectors
模型经常变更 版本化 Collection 或显式记录模型版本
需要严格复现 固定模型、预处理、维度和距离度量

3. 选择检索方式

需求 主要方式
精确术语、错误码、型号 BM25
同义表达和语义相似 向量检索
同时兼顾词面和语义 Hybrid
只搜索某类对象 Filter
按客户隔离数据 Multi-tenancy
对候选结果进一步精排 Hybrid/向量召回后接重排

一个实际的 RAG 查询往往不是单次 near_text

1. 根据租户选择 Collection 上下文
2. 注入权限过滤
3. 执行 hybrid 召回
4. 对结果去重、截断和重排
5. 返回正文、标题、来源 URL、文档 ID 和片段位置
6. 生成答案
7. 验证答案中的引用是否来自返回对象

Weaviate 负责其中的数据检索与索引部分;权限、重排、引用和生成仍需要应用层明确实现。


十、一个最小的端到端验证顺序

在正式接入 RAG 前,可以按下面顺序验证系统。假设已经创建 Article Collection,并配置好了可用的 Vectorizer。

import weaviate
from weaviate.classes.query import Filter

client = weaviate.connect_to_local()

try:
    articles = client.collections.get("Article")

    # 1. 写入一条最小对象
    obj_id = articles.data.insert(
        properties={
            "title": "向量数据库中的混合检索",
            "body": "混合检索同时使用关键词检索和向量检索。",
            "category": "database",
            "word_count": 1200,
        }
    )
    print("inserted:", obj_id)

    # 2. 按 ID 验证原始对象可读
    fetched = articles.query.fetch_object_by_id(obj_id)
    print("fetched:", fetched.properties if fetched else None)

    # 3. 验证关键词路径
    keyword_result = articles.query.bm25(
        query="混合检索",
        limit=3,
    )
    print("bm25 count:", len(keyword_result.objects))

    # 4. 验证向量路径
    vector_result = articles.query.near_text(
        query="同时使用关键词和语义搜索",
        limit=3,
        return_metadata=["distance"],
    )
    print("vector count:", len(vector_result.objects))

    # 5. 验证过滤
    filtered_result = articles.query.hybrid(
        query="检索",
        alpha=0.5,
        filters=(
            Filter.by_property("category").equal("database")
            & Filter.by_property("word_count").greater_than(1000)
        ),
        limit=3,
        return_metadata=["score"],
    )
    print("filtered hybrid count:", len(filtered_result.objects))

finally:
    client.close()

每一步的目的不同:

  1. 插入:验证 schema、写入接口和向量化路径;
  2. 按 ID 读取:排除检索排序问题;
  3. BM25:验证倒排索引和文本属性;
  4. 向量检索:验证 embedding 和向量索引;
  5. 混合加过滤:验证多个组件之间的组合行为。

如果第 2 步失败,应先修复写入或连接问题;如果第 2 步成功而第 3 步失败,应检查文本属性和倒排索引;如果第 3 步成功而第 4 步失败,应检查 Vectorizer、模块凭据和向量配置;只有这些基础路径都正常后,才有意义评估 alpha、切分策略和重排模型。

Collection 定义了数据边界,Vectorizer 定义了语义空间,倒排索引和向量索引提供两种互补的召回路径,过滤负责缩小合法候选集,混合检索负责融合词面和语义证据,多租户则把租户上下文提升为数据库访问边界。理解这些组件之间的数据流和故障边界,才能把 Weaviate 从“能返回相似文本”的演示组件,正确地用作 RAG 的检索数据层。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。