TiDB 向量搜索实战(2026):VECTOR 索引、混合检索与 RAG 落地
你为什么需要看这篇文章
LLM 时代,向量检索已经从「可选的实验性功能」变成了「每个应用的基础设施」。但把专门的向量数据库(Milvus、Pinecone、Weaviate)和业务数据库分开维护,会带来三重痛苦:
- 双写:插入一条业务记录,需要同时写业务库和向量库,数据一致性用两阶段提交/try-catch 也未必可靠。
- 数据同步延迟:业务数据更新后,向量库的索引重建和同步有窗口延迟,导致检索结果过时。
- 运维成本倍增:两套系统意味着双倍的部署、监控、备份和容量规划。
TiDB 从 7.5 版本开始,把向量功能原生集成到分布式 SQL 数据库中。你不用再同步两套系统——在 TiDB 里你就是同时拥有 OLTP、OLAP 和向量检索能力。
本文覆盖:VECTOR 类型的使用、两种向量索引(HNSW / IVF)的原理与选型、混合查询实战、RAG 集成流水线、性能调优要点。
核心概念
VECTOR 数据类型
TiDB 引入了一个新的数据类型 VECTOR(n),其中 n 是向量的维度(最大 16384)。向量值以 JSON 数组字面量表示。
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(256),
description TEXT,
embedding VECTOR(1536), -- OpenAI text-embedding-ada-002 / text-embedding-3-small 默认维度
category VARCHAR(64),
price DECIMAL(10,2),
created_at DATETIME DEFAULT NOW()
);
插入向量就像普通列一样简单:
INSERT INTO products (name, description, embedding, category, price)
VALUES (
'蓝牙降噪耳机',
'40mm 驱动单元,主动降噪,续航 30 小时',
'[0.0123, -0.0456, 0.0789, ...]', -- 1536 个 float32 值
'电子产品',
299.00
);
向量距离函数
TiDB 内置了四种常用的向量距离函数:
| 函数 | 含义 | 适用场景 |
|---|---|---|
VEC_COSINE_DISTANCE(a, b) |
余弦距离(1 - 余弦相似度) | 文本语义检索(最常用) |
VEC_L2_DISTANCE(a, b) |
欧几里得距离(平方) | 图像/几何特征 |
VEC_INNER_PRODUCT(a, b) |
负内积(-<a,b>) | 归一化后的余弦等价 |
VEC_L1_DISTANCE(a, b) |
曼哈顿距离 | 稀疏向量 / 特定场景 |
所有距离函数都可以在 ORDER BY 和 WHERE 中使用,与普通列无异。
向量索引:HNSW 与 IVF
如果没有索引,每次相似度检索都需要全表扫描并计算距离——百万级数据时延迟不可接受。TiDB 提供两种近似最近邻(ANN)索引:
HNSW(Hierarchical Navigable Small World)
HNSW 是当前学术界和工业界公认的 低延迟 ANN 索引结构。其核心思想是构建一个多层的「跳表图」:
- 底层(Layer 0):包含所有向量点,边稠密,负责高召回。
- 上层:按概率随机抽样进入,边稀疏,负责快速跳跃、缩小搜索范围。
- 查询时:从最高层开始贪婪搜索,逐渐下降到 Layer 0,每一层都通过「局部邻居图」的边快速逼近最近邻。
HNSW 的特点:
- 查得快:通常在 1–3 ms 内完成 top-10 ANN 查询(百万级数据)。
- 内存大:需要存储图结构(每个节点约 50–100 条边),额外内存常为原始向量数据量的 2–3 倍。
- 构建慢:逐一插入并生成邻居边,时间复杂度 O(N·logN·M),其中 M 是每层的最大邻居数。
- 适合高 QPS 在线服务。
-- 创建 HNSW 索引
ALTER TABLE products
ADD VECTOR INDEX idx_embedding (embedding)
WITH (
distance = 'cosine',
type = 'hnsw',
m = 16, -- 每层最大邻居数(默认 16,越大召回率越高但内存越大)
ef_construction = 64 -- 构建时的搜索宽度(默认 64,越大质量越高但构建越慢)
);
IVF(Inverted File)
IVF 使用聚类思想:把所有向量用 k-means 聚为若干簇,查询时先找最近的若干簇,然后仅在候选簇内计算精确距离。
- 构建快:k-means 聚类 + 分配,时间复杂度远小于 HNSW。
- 内存小:只存聚类中心(几十 KB)加倒排列表(id 数组),几乎没有额外内存开销。
- 查得稍慢:需要扫多个簇,延迟通常比 HNSW 高 2–5 倍。
- 适合插入频繁的写密集场景。
ALTER TABLE products
ADD VECTOR INDEX idx_embedding (embedding)
WITH (
distance = 'cosine',
type = 'ivf',
nlist = 256 -- 聚类数(默认 128,越大精度越高但速度越慢)
);
HNSW vs IVF 决策表
| 维度 | HNSW | IVF |
|---|---|---|
| 查询延迟(p50) | 1–3 ms | 5–15 ms |
| 索引构建时间 | 慢(数倍于 IVF) | 快 |
| 额外内存 | 2–3× 向量数据 | 极小(聚类中心 + 倒排列表) |
| 召回率(top-10) | 95–99% | 90–97%(取决于 nlist) |
| 增量插入 | 支持,但图质量可能退化 | 支持,聚类分布可能漂移 |
| 推荐场景 | 在线服务、低延迟、高 QPS | 批量写入、内存紧张、原型阶段 |
混合检索:TiDB 的真正杀手级能力
绝大多数 RAG 场景并不只是「找到与问题最相似的 5 个文档片段」。真实需求往往是:
- 「找到最近 7 天内发布的、属于 '后端开发' 类别的、与用户问题最相关的 5 篇文章」
- 「找到价格在 ¥100–500 之间、库存大于 0 的、与用户搜索词最相似的 10 件商品」
在这些场景中,你需要 精确的 SQL 过滤 + 向量相似度排序,而 TiDB 的优化器可以把两者在一条 SQL 里统一调度。
混合查询示例
SELECT id, name, description, price,
VEC_COSINE_DISTANCE(embedding, :query_embedding) AS dist
FROM products
WHERE category = '电子产品'
AND price BETWEEN 100 AND 500
AND status = 'active'
ORDER BY dist
LIMIT 10;
执行计划:
- 优化器识别
VECTOR INDEX,使用索引做 ANN 召回(返回候选集,通常为LIMIT * ef_search大小)。 - 对候选集应用
WHERE过滤——丢弃不满足条件的行。 - 对剩余行按
dist排序,取前 10 行返回。
关键点:过滤发生在召回之后但在精确排序之前。也就是说,数据库不会把全表的向量距离都算一遍再过滤,而是「先用索引快速捞出一批语义相似的候选,再用 WHERE 精确筛选,最后才排序」。这在百万级数据上会把延迟从秒级降到毫秒级。
语义重排(Semantic Reranking)
如果你的场景需要更高精度,可以在混合查询之后再做一步重排:
-- 第一步:ANN 召回 + 业务过滤(高速)
SELECT id, name, embedding
FROM products
WHERE category = '电子产品'
ORDER BY VEC_COSINE_DISTANCE(embedding, :query_embedding)
LIMIT 50;
-- 第二步:在应用层对 50 个候选用更昂贵的交叉编码器(cross-encoder)重排,
-- 或者直接用 TiDB 再做一次精确排序(此时数据量已在 50 条以内,成本可控)
RAG 流水线集成:从零到一
用 TiDB 做 RAG 的完整流水线如下:
步骤 1:文档切块与向量化
from openai import OpenAI
import tiktoken
client = OpenAI()
def chunk_text(text: str, max_tokens: int = 512) -> list[str]:
"""简单按自然段落切块,再按 token 数合并"""
enc = tiktoken.get_encoding("cl100k_base")
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
chunks = []
current = ""
current_tokens = 0
for p in paragraphs:
tokens = len(enc.encode(p))
if current_tokens + tokens > max_tokens:
chunks.append(current)
current = p
current_tokens = tokens
else:
current += "\n\n" + p
current_tokens += tokens
if current:
chunks.append(current)
return chunks
def embed(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:
resp = client.embeddings.create(input=texts, model=model)
return [r.embedding for r in resp.data]
步骤 2:存入 TiDB
import pymysql
conn = pymysql.connect(host="127.0.0.1", user="root", database="rag_db")
for i, (chunk, emb) in enumerate(zip(chunks, embeddings)):
emb_str = "[" + ",".join(f"{v:.6f}" for v in emb) + "]"
conn.cursor().execute(
"INSERT INTO documents (id, content, embedding, category) VALUES (%s, %s, %s, %s)",
(i, chunk, emb_str, "tech_blog")
)
conn.commit()
步骤 3:构建索引
ALTER TABLE documents
ADD VECTOR INDEX idx_doc_embedding (embedding)
WITH (distance = 'cosine', type = 'hnsw', m = 16, ef_construction = 64);
在大批量导入时,建议 先导完数据再建索引,速度远快于逐行插入 + 在线构建。
步骤 4:RAG 检索
def rag_query(question: str, category: str | None = None, top_k: int = 5) -> str:
q_emb = embed([question])[0]
q_emb_str = "[" + ",".join(f"{v:.6f}" for v in q_emb) + "]"
if category:
sql = """
SELECT content, VEC_COSINE_DISTANCE(embedding, %s) AS dist
FROM documents
WHERE category = %s
ORDER BY dist
LIMIT %s
"""
rows = conn.cursor().execute(sql, (q_emb_str, category, top_k)).fetchall()
else:
sql = """
SELECT content, VEC_COSINE_DISTANCE(embedding, %s) AS dist
FROM documents
ORDER BY dist
LIMIT %s
"""
rows = conn.cursor().execute(sql, (q_emb_str, top_k)).fetchall()
context = "\n\n---\n\n".join(r[0] for r in rows)
return context
# 然后送给 LLM
system_prompt = f"你是一个技术助手。使用以下参考材料回答问题:\n\n{context}"
步骤 5:端到端查询
context = rag_query("TiDB 的向量索引怎么选?", category="tech_blog")
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": f"使用以下材料回答:\n\n{context}"},
{"role": "user", "content": "TiDB 的向量索引怎么选?"}
]
)
print(response.choices[0].message.content)
这样,从文档入库到 RAG 问答,全程不需要额外的向量数据库——TiDB 一手包办。
性能与调优要点
1. 向量维度和存储
VECTOR(1536)每条占 6 KB(1536 × 4 字节 float32)。百万条 = 6 GB 纯向量数据,加 HNSW 索引约为 15–20 GB。- 如果你的用例对精度要求不高,可以先用 text-embedding-3-small(1536 维)或对大型模型返回的结果使用 PCA / Matryoshka embedding 截断为 256–512 维——存储和索引内存减半以上。
2. 批量导入优化
- 使用
LOAD DATA或 batch INSERT(每批 500–1000 行),关闭autocommit,单事务提交。 - 先加载数据,后创建向量索引(
tidb_ddl_reorg_batch_size可调大以加速索引构建)。
3. 查询优化
LIMIT配合索引:向量索引的召回数量受LIMIT影响,较大的LIMIT会触发更大范围的 ANN 搜索。一般取LIMIT的 3–5 倍作为内部搜索量(由ef_search等参数控制)。- 利用覆盖索引:如果查询只涉及少量列,可以在向量索引被使用的同时走覆盖索引,减少回表。
- 分区表:如果数据天然可以按时间或类别分区,向量索引 + 分区裁剪可以极大缩小搜索空间。
4. 召回率与延迟的权衡
m和ef_construction越大,索引质量越高,召回率越高,但构建时间越长、内存占用越大。ef_search(查询时的搜索宽度)可以在查询时动态设置。值越大召回率越高但延迟也更高。
-- 查询时动态调整搜索宽度
SET SESSION tidb_vector_index_ef_search = 40; -- 默认 20,调大提升召回率
SELECT ... ORDER BY VEC_COSINE_DISTANCE(embedding, '[...]') LIMIT 10;
与其他向量数据库的对比
| 维度 | TiDB | Milvus | Pinecone | pgvector |
|---|---|---|---|---|
| 混合查询(向量+SQL过滤) | ✅ 原生一行 SQL | ✅ 部分支持 | ❌(需应用层) | ✅ 原生 |
| 事务(ACID) | ✅ 分布式事务 | ❌ | ❌ | ✅ 单机 |
| 水平扩展 | ✅ 原生 | ✅ | ✅(SaaS) | ❌ |
| SQL 兼容性 | ✅ MySQL 兼容 | ❌ | ❌ | ✅ PostgreSQL |
| 部署复杂度 | 中(分布式) | 中-高 | 低(SaaS) | 低(PG 扩展) |
| 向量搜索延迟(百万级) | 2–15 ms | 2–10 ms | 5–20 ms(网络) | 3–10 ms |
TiDB 的独特优势:你不需要在「事务一致性」和「向量搜索能力」之间二选一。同一套集群、同一条 SQL、同一个事务——这就是原生集成的真正价值。
常见问题(FAQ)
Q1:TiDB 的向量索引与 pgvector 有什么本质区别?
两者都支持 SQL + 向量的混合查询,但 TiDB 是原生分布式设计——pgvector 的索引是单机方案,数据量和 QPS 增长到一定程度需要换分布式方案(如 Citus + pgvector),TiDB 则原生支持水平扩展。
Q2:什么时候应该用专用的向量数据库而不是 TiDB?
如果你的业务场景纯粹是「存向量、搜向量」——没有任何 SQL 过滤需求、不需要事务、不需要 JOIN 业务表——专用的向量数据库(Milvus / Qdrant)在纯粹向量检索微调上有更深度的优化。但一旦你需要「向量搜索结果是业务视图的一部分」或者「向量检索+精确过滤+事务操作在同一个请求里」,TiDB 的优势就显露出来了。
Q3:向量索引在分布式 TiDB 中如何工作?
每个 TiKV 节点上的 Region 内独立构建本地向量索引。查询时,TiDB 把 ANN 搜索下推到各 TiKV 节点并行执行,最后在 TiDB 层聚合排序——这是 TiDB 的 MPP(Massively Parallel Processing)架构的天然优势。
Q4:迁移已有 pgvector 数据到 TiDB 复杂吗?
不复杂。pgvector 和 TiDB 都使用 VECTOR(n) 类型 + JSON 数组字面量格式,数据导出为 CSV 后可直接 LOAD DATA 导入。主要需要适配的是索引创建语法(TiDB 的 WITH 子句参数名不同)。
Q5:生产环境中 HNSW 索引会因大量插入而退化吗?
会,但可以通过定期 ALTER TABLE ... REBUILD VECTOR INDEX 重建索引来恢复图结构质量。如果业务是频繁插入 + 实时查询的场景,建议使用 IVF 索引(对插入更友好),或者设计定期重建策略。
总结
TiDB 的向量搜索功能不是为了在 benchmark 上打败专用的向量数据库,而是为了消除「交易库 + 向量库」之间的同步鸿沟。当你需要在一个系统里同时完成「写订单、查库存、搜商品」这些操作时,不再需要把向量检索单独抽出来交给另一个系统——一条 SQL,一次完成。
推荐你的学习 / 评估路径:
- 用 TiDB Playground(
tiup playground)本地起一个单节点; - 创建表 + VECTOR 列 + HNSW 索引;
- 导入 10 万条公开数据集(如 Hugging Face 的
sentence-transformers/all-MiniLM-L6-v2对维基百科 snippet 的嵌入); - 分别测试纯向量检索、纯 SQL 过滤、混合查询的性能和召回率;
- 体验
EXPLAIN ANALYZE看执行计划——感受优化器如何把向量索引和 SQL 过滤融合在一起。