TiDB 向量搜索实战(2026):VECTOR 索引、混合检索与 RAG 落地

数据库(更新于 2026年7月20日)

你为什么需要看这篇文章

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 BYWHERE 中使用,与普通列无异。


向量索引: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;

执行计划

  1. 优化器识别 VECTOR INDEX,使用索引做 ANN 召回(返回候选集,通常为 LIMIT * ef_search 大小)。
  2. 对候选集应用 WHERE 过滤——丢弃不满足条件的行。
  3. 对剩余行按 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. 召回率与延迟的权衡

  • mef_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,一次完成。

推荐你的学习 / 评估路径

  1. 用 TiDB Playground(tiup playground)本地起一个单节点;
  2. 创建表 + VECTOR 列 + HNSW 索引;
  3. 导入 10 万条公开数据集(如 Hugging Face 的 sentence-transformers/all-MiniLM-L6-v2 对维基百科 snippet 的嵌入);
  4. 分别测试纯向量检索、纯 SQL 过滤、混合查询的性能和召回率;
  5. 体验 EXPLAIN ANALYZE 看执行计划——感受优化器如何把向量索引和 SQL 过滤融合在一起。
#TiDB#向量搜索#向量索引#混合检索#HTAP#RAG