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

Redis Search 与向量检索:索引、查询、Hybrid 和容量边界

Redis 的基础数据结构解决的是“如何保存和访问值”,而 Redis Search 解决的是“如何按字段内容找到值”。在此基础上,Redis 的向量字段和 KNN 查询又把“按相似度查找”加入了同一个查询引擎。

这几个能力经常被混在一起:

  • 索引决定哪些数据可以被检索,以及如何组织检索结构;
  • 全文检索按词项、字段和文本匹配规则查找;
  • 向量检索按向量距离查找近邻;
  • Hybrid Search把结构化过滤、全文匹配和向量召回组合起来;
  • 容量边界决定索引能否放入内存、查询是否稳定,以及召回率和延迟能否接受。

本文讨论 Redis Query Engine 的公开稳定语义。历史资料中常称其为 RediSearch;命令仍以 FT. 开头。Redis 核心数据结构本身不等于 Redis Search,实际部署需要使用包含 Query Engine 的 Redis Stack、Redis Cloud 或相应的 Redis Enterprise 能力。


一、先建立模型:数据、索引和查询结果不是同一个东西

Redis Search 至少涉及三层对象:

  1. 原始数据:Hash 或 JSON 文档;
  2. 搜索索引:由 FT.CREATE 创建,保存字段的倒排索引、数值索引或向量索引;
  3. 查询结果:由 FT.SEARCH 返回的文档标识、字段值和计算出来的分数。

例如,下面的 Hash 是原始数据:

doc:1001
  title    = Redis 向量检索
  category = database
  body     = Redis 可以同时保存结构化字段和向量字段
  embedding = <FLOAT32 二进制>

索引不会把这个 Hash 变成另一份可以独立读取的业务文档。索引保存的是用于查找的数据结构,命中后仍然需要返回原始文档中的字段。

因此:

  • 删除索引,通常不会自动删除原始 Hash;
  • 删除原始 Hash,索引中的对应项也应随索引更新而消失;
  • 修改被索引字段,会导致索引项更新;
  • 没有索引的字段仍可存储,但不能直接作为 Search 查询字段使用。

1. 索引的范围

FT.CREATE 通过 PREFIX 和数据类型确定索引范围:

FT.CREATE idx:docs
  ON HASH
  PREFIX 1 doc:
  SCHEMA
    title TEXT
    body TEXT
    category TAG
    price NUMERIC
    embedding VECTOR HNSW 6
      TYPE FLOAT32
      DIM 3
      DISTANCE_METRIC COSINE

这表示:

  • 只索引键名以 doc: 开头的 Hash;
  • titlebody 是全文字段;
  • category 是标签字段;
  • price 是数值字段;
  • embedding 是 3 维、FLOAT32 格式、使用 HNSW 的向量字段;
  • 向量相似度使用余弦距离。

索引可以在数据写入之前或之后创建。创建在已有数据之上时,Redis 会对匹配范围内的数据建立索引;创建之后写入或修改匹配文档时,索引也会更新。

PREFIX 不是数据分区的安全边界。一个错误的前缀可能把不应进入同一索引的业务对象全部纳入,造成字段缺失、索引膨胀和查询结果污染。

2. 字段类型决定查询语义

TEXT:全文字段

TEXT 字段使用文本索引。查询通常按词项匹配,而不是简单的字符串包含:

@title:Redis
@body:(向量检索)
@title:(Redis Search)

分词、词项、字段权重和文本评分都属于全文查询语义。不要把 TEXT 当作任意子串搜索;例如要查找完整 URL、版本号或精确状态值,通常更适合使用 TAG 或单独保存规范化字段。

TAG:枚举和精确标签

@category:{database}
@category:{database|search}

TAG 适合状态、租户、类别、语言等有限集合。它不是全文字段,不会对值进行自然语言分词。

标签值中包含查询语法特殊字符时,需要按照 Redis Search 的转义规则处理。生产代码不应直接拼接用户输入,而应使用白名单或可靠的转义函数。

NUMERIC:范围条件

@price:[0 100]
@created_at:[1700000000 +inf]

其中 -inf+inf 表示无穷边界。数值字段可以用于范围过滤,但它不会自动提供全文匹配能力。

VECTOR:向量字段

向量字段的定义至少要明确:

  • 算法:FLATHNSW
  • 数据类型:例如 FLOAT32
  • 维度:DIM
  • 距离度量:例如 COSINEL2IP

维度和数据类型是索引契约。索引声明 DIM 3 TYPE FLOAT32,查询向量和写入向量都必须能按该契约解释;维度不一致通常会导致写入或查询错误,而不是“自动补零”。


二、向量检索的数学含义:查的是距离,不一定是“相似度”

设文档向量为:

x=(x1,x2,,xd)x=(x_1,x_2,\ldots,x_d)

查询向量为:

q=(q1,q2,,qd)q=(q_1,q_2,\ldots,q_d)

向量检索的任务是从文档集合中找出距离 D(q,x)D(q,x) 最小的前 KK 个向量。

1. L2 距离

欧氏距离为:

DL2(q,x)=i=1d(qixi)2D_{L2}(q,x)=\sqrt{\sum_{i=1}^{d}(q_i-x_i)^2}

有些实现可以比较平方距离,因为平方根不会改变排序关系。直觉上,两个向量在几何空间中越近,距离越小。

2. 余弦距离

余弦相似度为:

Scos(q,x)=qxqxS_{\cos}(q,x)= \frac{q\cdot x}{\|q\|\|x\|}

Redis 查询结果中的 COSINE 通常表现为余弦距离,其关系可理解为:

Dcos=1ScosD_{\cos}=1-S_{\cos}

因此,数值越小越相似。在归一化向量且方向完全相同的情况下,距离接近 0;方向相反时,距离可能接近 2。

这会造成一个常见错误:

SORTBY distance DESC

如果 distance 是余弦距离,这会把最不相似的结果排在前面。KNN 查询通常应使用升序:

SORTBY distance ASC

3. 内积

内积为:

SIP(q,x)=qxS_{IP}(q,x)=q\cdot x

如果业务把内积作为相似度,则通常是分数越大越相似。距离度量和排序方向必须以索引类型及查询返回值的实际语义为准,不能仅根据字段名 score 猜测。


三、FLAT 和 HNSW:精确性与索引成本的选择

1. FLAT:逐个计算,结果精确

FLAT 的基本过程是:

  1. 读取候选集合中的每个向量;
  2. 计算它与查询向量的距离;
  3. 维护距离最小的前 KK 个结果。

若候选数为 NN,维度为 DD,一次查询的距离计算量大致与:

O(ND)O(ND)

相关。

FLAT 的优势是结果精确、行为容易理解,适合:

  • 数据量较小;
  • 强调精确 KNN;
  • 需要作为 HNSW 召回率基准;
  • 过滤后候选集合已经很小。

它的边界也明确:当每次查询都要扫描大量向量时,延迟会随数据量近似线性增长。

2. HNSW:图上的近似搜索

HNSW 将向量组织成多层近邻图。查询大致经历:

  1. 从高层图入口开始;
  2. 沿着更接近查询向量的邻居移动;
  3. 逐层下降;
  4. 在底层维护候选集合;
  5. 返回距离最小的前 KK 个结果。

它不保证像 FLAT 一样返回数学意义上的精确近邻,而是在较低查询成本下获得较高召回率。

影响 HNSW 行为的参数包括:

  • M:每个节点维护的邻居数量,影响图大小和连接性;
  • EF_CONSTRUCTION:建图时的候选宽度,通常越高,建图成本越大,但图质量可能更好;
  • EF_RUNTIME:查询时的候选宽度,通常越高,查询成本越大,但召回率可能提高。

一个 HNSW 查询的 K 不是“只检查 K 个向量”。K 是最终返回结果数,实际搜索候选数由运行时参数和过滤条件等共同影响。

3. 精确召回率如何验证

不能只观察平均延迟判断 HNSW 是否适合。应使用同一批查询分别执行:

  • FLAT 查询,作为精确结果集合;
  • HNSW 查询,作为近似结果集合。

对每个查询计算:

Recall@K=TopKHNSWTopKFLATKRecall@K= \frac{|TopK_{HNSW}\cap TopK_{FLAT}|}{K}

例如,精确结果为:

[A, B, C, D, E]

HNSW 返回:

[A, B, D, F, G]

则:

Recall@5=35=0.6Recall@5=\frac{3}{5}=0.6

这不是“距离分数相似”就算召回正确,而是看精确 Top-K 中有多少文档被找回来。


四、从建索引到查询:一个可运行的 Hash 向量示例

下面示例使用 Python redis-py 的 Search API。服务端必须提供 Redis Query Engine;客户端版本也应与服务端支持的 Search API 相匹配。

安装依赖:

pip install redis numpy

假设 Redis 运行在本机默认端口。

1. 创建文档和索引

import numpy as np
import redis

from redis.commands.search.field import TextField, TagField, VectorField
from redis.commands.search.index_definition import IndexDefinition, IndexType

r = redis.Redis(host="localhost", port=6379, decode_responses=False)

index_name = "idx:docs"

# 为了让示例可重复,先删除旧索引和示例数据。
# DROPINDEX 默认只删除索引,不删除原始 Hash。
try:
    r.ft(index_name).dropindex()
except Exception:
    pass

for key in (b"doc:1", b"doc:2", b"doc:3"):
    r.delete(key)

fields = [
    TextField("title"),
    TagField("category"),
    VectorField(
        "embedding",
        "HNSW",
        {
            "TYPE": "FLOAT32",
            "DIM": 3,
            "DISTANCE_METRIC": "COSINE",
            "M": 16,
            "EF_CONSTRUCTION": 100,
            "EF_RUNTIME": 10,
        },
    ),
]

r.ft(index_name).create_index(
    fields,
    definition=IndexDefinition(
        prefix=["doc:"],
        index_type=IndexType.HASH,
    ),
)

def vec(values):
    return np.asarray(values, dtype=np.float32).tobytes()

r.hset(
    "doc:1",
    mapping={
        "title": "Redis Search 基础",
        "category": "database",
        "embedding": vec([1.0, 0.0, 0.0]),
    },
)

r.hset(
    "doc:2",
    mapping={
        "title": "向量检索入门",
        "category": "search",
        "embedding": vec([0.9, 0.1, 0.0]),
    },
)

r.hset(
    "doc:3",
    mapping={
        "title": "消息流处理",
        "category": "database",
        "embedding": vec([0.0, 1.0, 0.0]),
    },
)

这里的关键点是:

  • embedding 保存的是二进制 FLOAT32 字节串,而不是 Python 列表的字符串表示;
  • 每个向量正好包含 3 个 float32
  • 索引字段名必须和 Hash 字段名一致;
  • category 使用 TAG,因此查询时采用标签语法,而不是全文语法。

2. 执行 KNN 查询

from redis.commands.search.query import Query

query_vector = vec([1.0, 0.0, 0.0])

q = (
    Query("*=>[KNN 2 @embedding $query_vec AS distance]")
    .sort_by("distance", asc=True)
    .return_fields("title", "category", "distance")
    .paging(0, 2)
    .dialect(2)
)

result = r.ft(index_name).search(
    q,
    query_params={"query_vec": query_vector},
)

for doc in result.docs:
    print(doc.id, doc.title, doc.category, doc.distance)

查询表达式可以拆成:

* => [KNN 2 @embedding $query_vec AS distance]
  • *:初始过滤条件为全部文档;
  • KNN 2:返回 2 个最近邻;
  • @embedding:使用向量字段;
  • $query_vec:查询参数中的二进制向量;
  • AS distance:把距离暴露为结果字段;
  • DIALECT 2:使用支持该 KNN 语法的查询方言;
  • SORTBY distance ASC:距离越小越优先。

结果中的 distance 是计算结果,不是原始 Hash 中的字段。调用方可以返回它,也可以将其转换成应用层自己的相似度表示,但转换时必须保留原始度量的方向含义。

3. 增加标签过滤

q = (
    Query("(@category:{database})=>[KNN 2 @embedding $query_vec AS distance]")
    .sort_by("distance", asc=True)
    .return_fields("title", "category", "distance")
    .paging(0, 2)
    .dialect(2)
)

result = r.ft(index_name).search(
    q,
    query_params={"query_vec": query_vector},
)

这里的 @category:{database} 是过滤条件,KNN 只在符合类别的范围内工作。从语义上看,结果必须同时满足:

xFx\in F

并且在过滤集合 FF 中按距离选出前 KK 个:

TopK({xxF},D(q,x))TopK(\{x\mid x\in F\}, D(q,x))

这与“先取全库 Top-K,再在客户端过滤”不同。后者可能只剩下很少结果,甚至一个也没有。

4. 查询返回零结果时如何判断问题

下面几种情况会导致零结果或错误:

  1. PREFIX 与实际键名不匹配;
  2. 文档不是索引声明的数据类型;
  3. Hash 中字段名与索引字段不一致;
  4. TAG 查询没有正确转义;
  5. 查询向量维度或二进制格式不正确;
  6. 过滤条件确实排除了所有文档;
  7. 索引仍处于构建或更新阶段;
  8. 查询方言不支持当前查询表达式。

可以先执行:

FT.INFO idx:docs
FT.SEARCH idx:docs "*" LIMIT 0 10

FT.INFO 用于查看索引的字段和运行状态;FT.SEARCH ... "*" 用于验证索引是否能看到基本文档。诊断时应先去掉向量和过滤条件,再逐层加回,而不是一开始修改多个参数。


五、全文、向量和结构化条件如何组成 Hybrid Search

“Hybrid Search”不是一个唯一的算法名称。在 Redis 中,至少有三种不同层次的组合。

1. 向量检索加结构化过滤

这是最直接的一类 Hybrid:

(@category:{database} @tenant:{t1})
  =>[KNN 10 @embedding $vec AS distance]

它的含义是:

  1. 先满足租户和类别等约束;
  2. 在约束后的候选中执行向量近邻搜索;
  3. 按距离返回前 10 个结果。

这类查询适合“语义相似,但必须属于指定租户或商品类别”的场景。

注意,过滤条件可能显著改变候选集合大小。即使全库 HNSW 的召回率很高,过滤后的有效候选很少时,结果数量和延迟也可能出现不同表现。生产验证必须使用真实过滤分布,而不是只测无过滤全库查询。

2. 全文条件加向量召回

例如:

(@title:Redis | @body:Redis)
  =>[KNN 10 @embedding $vec AS distance]

它表达的是“文本条件成立的文档中,再按向量距离取近邻”。这不是自动把 BM25 分数和向量距离加权相加,而是把全文条件作为候选约束。

如果全文条件过于严格,语义上相关但没有匹配词项的文档会被排除;如果全文条件过于宽松,向量搜索仍可能面对很大的候选集合。

3. 两路召回,再做融合和重排

更常见的搜索系统做法是分别获得两路结果:

  • 文本检索得到 TextTopN
  • 向量检索得到 VectorTopN
  • 合并候选;
  • 去重;
  • 进行融合或重排;
  • 最后返回前 KK 个结果。

这样做的原因是两种分数通常不可直接比较:

  • BM25 分数的范围受词频、文档长度和查询词影响;
  • 余弦距离通常越小越好;
  • 内积可能越大越好;
  • 不同查询之间分数分布也不稳定。

一种不依赖分数尺度的融合方法是 Reciprocal Rank Fusion,简称 RRF:

RRF(d)=lL1k+rankl(d)RRF(d)=\sum_{l\in L}\frac{1}{k+rank_l(d)}

其中:

  • dd 是文档;
  • LL 是不同召回列表;
  • rankl(d)rank_l(d) 是文档在列表 ll 中的名次;
  • kk 是平滑常数,常用一个固定正整数,例如 60。

假设:

文本结果:A, C, B, D
向量结果:B, A, E, D

k=60k=60

  • A:文本第 1,向量第 2

161+1620.03252\frac1{61}+\frac1{62}\approx0.03252

  • B:文本第 3,向量第 1

163+1610.03226\frac1{63}+\frac1{61}\approx0.03226

  • D:文本第 4,向量第 4

164+164=0.03125\frac1{64}+\frac1{64}=0.03125

A 和 B 会排在 D 前面。RRF 不需要把 BM25 分数和余弦距离强行归一化,适合先验证两路召回的互补性。

如果需要更复杂的结果质量,可以在融合后进行重排。重排输入通常包括标题、摘要、权限过滤结果和向量距离,而不是把所有原文直接交给重排模型。无论使用 Redis 内部查询还是应用层融合,都必须在融合前完成租户、权限和文档状态过滤,不能把权限判断推迟到结果展示阶段。


六、Hash 与 JSON 的选择

Redis Search 可以索引 Hash,也可以索引 JSON。

Hash 的特点

Hash 适合:

  • 字段结构稳定;
  • 文档较扁平;
  • 业务主要按固定字段查询;
  • 希望用简单的 HSET 更新。

索引定义直接使用字段名:

SCHEMA title TEXT category TAG embedding VECTOR ...

JSON 的特点

JSON 适合:

  • 文档存在嵌套对象或数组;
  • 需要保存较完整的文档结构;
  • 需要通过 JSONPath 将嵌套值映射为索引字段。

示意定义:

FT.CREATE idx:json
  ON JSON
  PREFIX 1 item:
  SCHEMA
    $.title AS title TEXT
    $.category AS category TAG
    $.embedding AS embedding VECTOR HNSW 6
      TYPE FLOAT32 DIM 3 DISTANCE_METRIC COSINE

AS 后的别名是查询字段名。查询使用 @title@category@embedding,而不是直接把 JSONPath 写入常规字段查询中。

JSON 的便利也伴随更新成本:一次嵌套路径更新可能引起相关索引项重建;大型 JSON 文档还会增加原始数据和索引维护开销。向量字段应尽量保持明确、稳定的路径,避免同一文档中存在多个含义不同但维度相同的向量。


七、事务、并发和故障路径

1. 单条 Redis 命令的原子执行

Redis 命令在服务端按顺序执行。一个 HSET 命令不会被另一个客户端命令插入中间。对于包含索引字段的写入,业务上应把原始字段更新视为会触发索引更新的一个操作。

但是,“命令原子执行”不等于“多个命令自动组成事务”。

下面的操作可能产生中间状态:

HSET doc:1 title "新标题"
HSET doc:1 embedding <new-vector>

如果两个命令之间有查询,查询可能看到第一条更新后的状态。需要多个字段一起改变时,可以使用 MULTI/EXEC

MULTI
HSET doc:1 title "新标题"
HSET doc:1 embedding <new-vector>
EXEC

MULTI/EXEC 以事务批次顺序执行,但 Redis 事务没有传统数据库式的回滚机制;命令错误不会把已经执行的前序命令恢复。客户端还应处理网络断开、超时和重试带来的不确定性。

2. 向量写入的失败表现

常见错误包括:

  • 向量字节长度与 DIM * sizeof(TYPE) 不匹配;
  • 使用了 float64,但索引声明为 FLOAT32
  • 写入了文本形式的数组,而不是二进制浮点字节;
  • 查询参数名称与 $query_vec 不一致;
  • 查询时没有使用支持 KNN 语法的方言。

这些问题应在应用层写入前校验。以 FLOAT32、3 维为例,向量有效载荷应为:

3×4=12 bytes3\times4=12\text{ bytes}

这只是数据载荷长度,不包含 Hash、JSON、键名、索引结构和内存分配器开销。

3. 索引删除的风险

FT.DROPINDEX idx:docs

默认删除索引结构,但保留原始文档。带有删除文档选项的形式会连同索引关联的数据一起删除,使用前必须确认命令语义和数据类型。

在生产环境中,删除索引前应至少验证:

FT.INFO idx:docs
FT.SEARCH idx:docs "*" LIMIT 0 1

并确认已有备份或可重建方案。删除后重新建 HNSW 可能消耗大量 CPU、内存和时间;如果服务仍承接写入或查询,还要评估重建期间的资源竞争。


八、容量边界:真正占内存的不只是向量数组

1. 向量原始载荷的下界

如果有 NN 条向量、每条 DD 维、每个元素占 bb 字节,则仅向量载荷约为:

Memoryraw=N×D×bMemory_{raw}=N\times D\times b

例如:

  • N=10,000,000N=10,000,000
  • D=768D=768
  • FLOAT32b=4b=4

则:

10,000,000×768×4=30,720,000,000 bytes10,000,000\times768\times4 =30,720,000,000\text{ bytes}

约为 30.72 GB 十进制,或约 28.6 GiB。

这还没有计算:

  • Hash 或 JSON 原始文档;
  • Redis key 和对象开销;
  • HNSW 图的邻接关系;
  • TEXT 倒排索引;
  • TAG 和 NUMERIC 索引;
  • 压缩、内存碎片和分配器开销;
  • 副本和故障转移副本;
  • 集群节点的额外管理开销。

因此,用“向量维度乘数据量”估算出来的只是下界,不能直接作为实例规格。

2. FLAT 和 HNSW 的容量差异

FLAT 主要增加向量存储和索引管理开销,查询工作量随候选数线性增长。

HNSW 还需要保存图结构。图结构的实际内存取决于实现、参数、层级分布和分配器,不能用一个固定的“每条向量增加多少字节”在所有版本和配置上准确推导。M 越大,通常图连接越多,内存和建图成本也越高。

容量评估应采用实测:

  1. 用目标维度和数据类型写入代表性数据;
  2. 建立与生产相同的字段索引;
  3. 使用 INFO MEMORYMEMORY USAGEFT.INFO 观察;
  4. 分别测试空文档、完整文档、单向量和多字段索引;
  5. 将副本、故障切换和重建余量纳入预算。

3. 过滤并不总是降低成本

直觉上,加入 @tenant:{t1} 后候选变少,查询应该更快。但实际成本还取决于:

  • 过滤字段的选择性;
  • HNSW 图上是否容易访问到有效候选;
  • 为达到 K 个有效结果需要扩展多少候选;
  • 过滤是否在正确的查询阶段生效;
  • 分片部署时数据和过滤条件如何分布。

如果过滤条件几乎匹配全库,它不会带来明显收益;如果过滤条件极窄,可能找不到足够的有效近邻,或者需要更多搜索工作才能补足结果数。

4. K、分页和结果规模

向量查询的 K 是召回数量,不应直接设置成最终页面大小。例如页面只展示 20 条,但 Hybrid 检索可能需要:

  • 文本召回 100 条;
  • 向量召回 100 条;
  • 合并去重后重排;
  • 最终展示 20 条。

如果直接把两路 K 都设成 20,融合阶段几乎没有候选余量,容易造成某一路结果被另一条路完全覆盖。

另一方面,盲目增大 K 会增加排序、网络传输和应用层融合成本。RETURN 字段也应只返回真正需要的内容;大型 JSON 原文不应在每次召回时全部传输。


九、查询方言和版本边界

Redis Search 的查询语法存在方言概念。向量 KNN 查询、参数绑定和部分查询组合需要使用对应的查询方言;示例中显式使用:

DIALECT 2

客户端 API 中通常对应:

Query(...).dialect(2)

不要假设“服务端升级后所有客户端默认方言都会自动改变”。升级 Redis Query Engine 时,应同时验证:

  • FT.CREATE 的向量字段语法;
  • FT.SEARCH 的 KNN 表达式;
  • PARAMS 二进制参数传递;
  • JSON 路径字段映射;
  • HNSW 参数名称;
  • 集群模式下的 Search 命令支持;
  • 客户端库对新语法的封装。

某些向量算法、压缩方式或专用索引能力可能只在特定 Redis 产品、版本或发行版中提供。不能因为其他向量数据库存在某个参数,就推断 Redis 的同名参数具有相同语义。上线前应以目标部署版本的命令文档和 FT.INFO 实际输出为准。


十、生产诊断:从“没有结果”到“结果不对”

情况一:索引查不到任何文档

按以下顺序检查:

FT.INFO idx:docs
FT.SEARCH idx:docs "*" LIMIT 0 10

然后确认:

  • 键名是否匹配 PREFIX
  • 文档类型是否为 Hash 或 JSON;
  • 字段是否存在;
  • 索引创建时是否已完成;
  • 是否把 Hash 字段名误写成 JSONPath;
  • 查询中是否错误使用了 TAGTEXTNUMERIC 语法。

情况二:向量查询报维度或参数错误

检查:

  • 写入向量和查询向量是否都是 FLOAT32
  • 字节长度是否等于 DIM * 4
  • 参数名是否一致;
  • 是否通过 PARAMS 或客户端 query_params 传递;
  • 是否设置了正确方言。

情况三:结果看起来“不相似”

先不要立即调整 HNSW 参数:

  1. 用 FLAT 索引对同一数据执行精确查询;
  2. 检查向量生成模型、归一化方式和版本;
  3. 检查距离度量是否与 embedding 模型约定一致;
  4. 确认排序是距离升序还是相似度降序;
  5. 再用 FLAT 与 HNSW 计算 Recall@K
  6. 最后才评估 EF_RUNTIMEM 等参数。

如果 FLAT 本身结果不符合预期,问题通常不在 HNSW,而在向量生成、字段映射或距离定义。

情况四:查询延迟周期性升高

应区分:

  • 查询计算时间;
  • 网络传输时间;
  • 索引构建或更新时间;
  • 后台持久化、复制或故障转移带来的资源竞争;
  • 应用层重排时间。

重点观察内存是否接近上限、HNSW 参数是否过大、过滤条件是否失去选择性,以及是否返回了过多字段。只提高查询超时时间通常不能解决容量或候选规模问题。


十一、如何在几个边界之间做取舍

可以用下面的决策逻辑,而不是先选择一个固定参数:

  • 需要精确结果、数据量小或候选过滤后很少:优先考虑 FLAT;
  • 需要较大规模、可接受近似结果:考虑 HNSW;
  • 需要强权限和租户隔离:把过滤条件放入 Search 查询,而不是只在客户端过滤;
  • 需要全文和语义互补:采用两路召回并在应用层融合;
  • 需要稳定容量:按原始数据、字段索引、向量索引、副本和重建余量共同估算;
  • 需要可验证质量:用 FLAT 建立 Recall@K 基线,并在真实查询分布上比较;
  • 需要长期升级:固定服务端、客户端、查询方言和索引定义的兼容测试。

Redis Search 的价值不在于把所有搜索问题都变成一个命令,而在于它能让原始文档、结构化过滤、全文倒排和向量近邻在同一个数据服务中协同工作。真正决定系统质量的,是明确每种索引的语义、知道 Hybrid 是过滤还是融合,并把近似检索的召回率、内存和故障恢复成本测量出来。


系列导航与关联阅读

官方资料

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