☰
ProxySQL RAG 索引嵌入与向量检索设计:Chunk 级 Embedding 的生产级蓝图
2026/10/7 2:42:20 网站建设 项目流程
  • 后端
  • 数据库
  • 负载均衡

【免费下载链接】proxysql

High-performance proxy for MySQL and PostgreSQL

项目地址:https://gitcode.com/gh_mirrors/pr/proxysql
点击查看免费下载

本篇指南围绕 ProxySQL 仓库中doc/RAG/embeddings-design.md的 v0→v1 实现蓝图展开,系统讲解 RAG 索引中"嵌入什么文本、如何用embedding_json定义、向量存到哪里、何时重算、如何查询"的完整设计链路。读完本文,你将掌握从rag_sources.embedding_json配置、rag_vec_chunks(sqlite3-vec) 存储、内容哈希增量更新,到 MCP 层向量检索与混合检索(RRF 融合 / FTS 候选重排)的落地方法,并能直接对照仓库中的 schema.sql 与 rag_ingest.cpp 源码验证每个设计决策。

该设计以三个既有能力为前提:chunk 化已由rag_chunks承载;ProxySQL 已集成 sqlite3-vec 并提供vec0(...)虚拟表rag_vec_chunks;检索入口主要通过 MCP 工具暴露(详见 mcp-tools.md)。


1. 设计目标:为什么嵌入要按 Chunk 粒度单独设计

embeddings-design.md开篇就明确了六个核心目标,它们决定了后面所有 schema 与流水线的形态:

  1. Chunk 级嵌入:每个 chunk 拥有自己的 embedding,保证检索精度(而非按整篇文档粗粒度嵌入)。
  2. 确定性嵌入输入:被嵌入的文本由配置显式定义,而不是由实现"推断",保证可复现、可观测。
  3. 模型敏捷性:系统可以在不破坏已存数据与对外 API 的前提下更换嵌入模型/维度。
  4. 高效更新:只有当 chunk 的"嵌入输入"真正变化时才重算 embedding,避免无谓成本。
  5. 成本与延迟有界:嵌入生成本身昂贵,必须限定资源开销。
  6. 异步可扩展:为后续引入异步嵌入任务预留空间。

从源码看,rag_ingest.cpp的 v0 实现已经兑现了其中大部分目标:parse_embedding_json()(rag_ingest.cpp#L611-L651)按配置解析模型、维度、provider、批大小与超时;ingest_source()(rag_ingest.cpp#L1570-L1832)在 chunk 创建后同步生成并存储向量。而目标 4(内容哈希增量更新)与 6(异步 worker)则作为 v1 推荐演进,本文第 6、11 节会专门展开。


2. 嵌入什么、不嵌入什么:输入文本的语义边界

2.1 推荐嵌入的文本:提升语义召回

对知识库类内容(StackOverflow 帖子、技术文档、工单、runbook),每个 chunk 的推荐嵌入输入是:

  • 文档标题(若存在)
  • 标签(以纯文本形式)
  • Chunk 正文

推荐模板如下:

{Title} Tags: {Tags} {ChunkBody}

这一模板在仓库样例中得到了完全一致的落地:RAG_POC/sample_sqlite.sql第 37 行的embedding_json配置正是

{"enabled":true,"dim":1536,"model":"text-embedding-3-large","input":{"concat":[{"col":"Title"},{"lit":"\nTags: "},{"col":"Tags"},{"lit":"\n\n"},{"chunk_body":true}]}}

而 rag_ingest.cpp#L1466-L1477 的build_embedding_input()会读取input.concat规范并通过eval_concat()(rag_ingest.cpp#L694-L716)逐段拼接:col从源行取值、lit输出固定字面量、chunk_body注入当前 chunk 正文——这正是"确定性嵌入输入"原则的代码级体现。

2.2 默认不嵌入数值型元数据

Score、ViewCount、OwnerUserId、时间戳等字段不应进入嵌入文本。它们应当保持结构化,用于:

  • 过滤(filtering)
  • 提升权重(boosting)
  • 打破平局(tie-breaking)
  • 结果整形(result shaping)

把数值元数据混入嵌入文本通常只会引入噪声、降低语义质量。这一点与 mcp-tools.md 中的共享过滤模型一致:source_ids、doc_ids、min_score、tags_any/tags_all、created_after/created_before等结构化过滤全部走元数据通道,而不是靠向量相似度。

2.3 代码与 HTML 的处理策略

若 chunk 正文包含 HTML 或代码:

  • v0:直接嵌入原始文本(可用,但可能含噪声)。
  • v1:做归一化以提升质量:
    • 剥离 HTML 标签(保留文本内容);
    • 代码块以纯文本保留,但考虑剔除过多标记;
    • 对代码密集的源,可选择性创建专门的"code-only" chunk。

归一化策略应在embedding_json.normalize中按源可配置(见下一节),例如{"strip_html": true, "collapse_whitespace": true}。


3. 嵌入输入规则定义在哪里:rag_sources.embedding_json

嵌入输入规则必须显式声明并按源存储。rag_sources表在 schema.sql#L14-L46 中预留了embedding_json TEXT列(L42),注释明确指出"v0 可留空,后续定义而无需改表结构"。

3.1 推荐的 schema

{ "enabled": true, "model": "text-embedding-3-large", "dim": 1536, "input": { "concat": [ {"col":"Title"}, {"lit":"\nTags: "}, {"col":"Tags"}, {"lit":"\n\n"}, {"chunk_body": true} ] }, "normalize": { "strip_html": true, "collapse_whitespace": true } }

3.2 各字段语义

字段语义
enabled是否为此源计算/存储 embedding
model逻辑模型名(用于可观测性与兼容性检查)
dim向量维度,必须与rag_vec_chunks的 vec0 声明维度一致
input.concat如何拼接嵌入输入文本(col / lit / chunk_body)
normalize可选的归一化步骤

3.3 源码中的实际解析与校验

parse_embedding_json()(rag_ingest.cpp#L611-L651)读取enabled、dim、model、input,并额外支持 v0 落地需要的运行时字段provider、api_base、api_key、batch_size、timeout_ms,随后做防御性校验:

  • dim <= 0→ 回退默认 1536;
  • batch_size <= 0→ 回退默认 16;
  • timeout_ms <= 0→ 回退默认 20000ms。

注意 v0 的EmbeddingConfig尚未解析normalize(strip_html 等属 v1 建议),因此"normalize 按源可配置"目前是蓝图层面的设计,落地时需在parse_embedding_json中扩展。


4. 存储 schema 与模型/版本演进

4.1 v0 现状:单向量表

rag_vec_chunks存储:

  • embedding 向量
  • chunk_id
  • doc_id/source_id便捷列
  • updated_at

schema.sql#L134-L141 给出了准确定义:

CREATE VIRTUAL TABLE IF NOT EXISTS rag_vec_chunks USING vec0( embedding float[1536], -- change if you use another dimension chunk_id TEXT, -- join key back to rag_chunks doc_id TEXT, -- optional convenience source_id INTEGER, -- optional convenience updated_at INTEGER -- optional convenience );

这在假设"单一嵌入模型/单一维度"的 v0 阶段完全够用。rag_ingest.cpp的init_schema()(rag_ingest.cpp#L1880-L2033)会在建表时按--vec-dim参数(默认 1536)动态生成float[dim];若 sqlite-vec 扩展未加载,建表失败时仅告警并禁用向量功能,不会阻塞其余索引。

4.2 v1 演进:支持多模型

产品化场景常需要多个嵌入模型(如通用模型 vs 代码专用模型),两种支持方式:

方案 A:在rag_vec_chunks中增加模型身份列

  • 新增model TEXT、dim INTEGER(若按模型固定维度则可选);
  • 允许每个chunk_id存在多行,唯一键变为(chunk_id, model);
  • 需要 schema 变更,并对 vec0 的元数据列与唯一性约束谨慎设计。

方案 B:每模型一张 vec 表(若 vec0 约束受限,推荐)

  • 分别建rag_vec_chunks_1536_v1、rag_vec_chunks_1024_code_v1等;
  • MCP 工具按请求的模型或默认配置选择对应表。

建议:仅当你的 sqlite3-vec 构建能轻松按 model 过滤时才选方案 A;否则方案 B 在运维上更干净(维度天然隔离、重建单个模型不影响其他模型)。


5. 嵌入生成流水线:何时创建、何时更新

5.1 创建时机:chunk 之后、入库之时

v0 是同步流水线:

ingest row → create chunks → compute embedding → store vector

ingest_source()中的真实顺序(rag_ingest.cpp#L1736-L1757):对每个 chunk 依次执行insert_chunk(写入rag_chunks)→insert_fts(写入 FTS5)→ 若ecfg.enabled则build_embedding_input收集到pending_embeddings批中;当积压达到batch_size时立即flush_embedding_batch()。全部行处理完后,若还有残余 pending 批,会再 flush 一次(rag_ingest.cpp#L1773-L1777)。

5.2 更新时机:以"嵌入输入"的变化为准

只要以下任一变化,就必须重算 embedding:

  • 标题变化
  • 标签变化
  • chunk 正文变化
  • 归一化规则变化(如 strip_html)
  • 嵌入模型变化

因此更新逻辑应基于嵌入输入的内容哈希(content hash)判定,而不是盲目全量重算。


6. 内容哈希:高效增量更新的基石(v1 推荐)

6.1 为什么需要哈希

没有哈希,每次同步都可能对未变化的 chunk 重复调用昂贵的嵌入服务:耗时、费钱、且让增量同步失去意义。

6.2 推荐做法:每个 chunk × 每个模型存一份embedding_input_hash

方案 A:存入rag_chunks.metadata_json

{ "chunk_index": 0, "embedding_hash": "sha256:...", "embedding_model": "text-embedding-3-large" }

优点:无需 schema 变更;缺点:JSON 解析开销。

方案 B:专用侧表(推荐)

CREATE TABLE rag_chunk_embedding_state ( chunk_id TEXT NOT NULL, model TEXT NOT NULL, dim INTEGER NOT NULL, input_hash TEXT NOT NULL, updated_at INTEGER NOT NULL DEFAULT (unixepoch()), PRIMARY KEY(chunk_id, model) );

优点:查找快,避免 JSON 解析;缺点:多一张表。

v1 建议采用方案 B。

6.3 仓库中已有的哈希雏形

虽然rag_chunk_embedding_state尚属 v1 蓝图,但 v0 的rag_ingest.cpp已经为文档级增量更新实现了同一思路:

  • compute_content_hash()(rag_ingest.cpp#L481-L492)对title|body|metadata_json拼接串做 SHA-256,输出 64 位十六进制;
  • rag_documents表在运行时通过ALTER TABLE ... ADD COLUMN content_hash VARCHAR(64)增加该列(rag_ingest.cpp#L1939);
  • 同步时对比新旧哈希:哈希相同则跳过该文档(skipped_docs++);不同则软删除旧文档并重建其 chunks / FTS / vec 行(rag_ingest.cpp#L1711-L1730)。

v1 只需把这一已验证的机制下移到 chunk 级并纳入embedding_json的输入串(标题+标签+正文+归一化规则+模型标识),即可实现"仅重算输入真正变化的 chunk"。


7. 嵌入模型集成选项:外部服务 vs 进程内运行时

7.1 外部嵌入服务(初期推荐)

ProxySQL 调用嵌入服务,可选:

  • OpenAI 兼容端点;
  • 本地服务(如 llama.cpp server);
  • 厂商专用嵌入 API。

优点:模型选型迭代容易;ML 运行时与 ProxySQL 进程隔离。缺点:存在网络延迟,需要缓存与超时控制。

仓库 v0 落地:rag_ingest.cpp通过EmbeddingProvider抽象基类(rag_ingest.cpp#L1136-L1149)定义embed(inputs, dim)批接口,build_embedding_provider()(rag_ingest.cpp#L1319-L1329)根据provider字段分派:

  • "openai"→OpenAIEmbeddingProvider(rag_ingest.cpp#L1207-L1317):libcurl POST${api_base}/embeddings,Bearer 认证,请求体含model、input(批量数组)、dimensions,并校验响应维度与输入数量;
  • 其他(含默认)→StubEmbeddingProvider(rag_ingest.cpp#L1163-L1170):对输入文本做确定性哈希生成归一化伪向量,用于无网络、无成本的开发与测试。

7.2 进程内嵌入运行时

ProxySQL 直接链接嵌入运行时(如 llama.cpp)。

优点:无网络依赖;调优后延迟可预期。缺点:增大内存占用;需要精细的资源控制。

建议:先从外部嵌入提供商起步,同时保持模块化接口(仓库中的EmbeddingProvider抽象正是为此设计),后续可无痛切换。


8. 查询嵌入生成:MCP 层的标准流程

向量检索需要查询嵌入,应在 MCP 层完成:

  1. 取query_text;
  2. 应用查询归一化(可选但推荐);
  3. 使用与 chunk 相同的模型计算查询嵌入;
  4. 以绑定向量执行向量检索 SQL。

明确禁止:

  • 不加校验就接受不可信调用方传入的任意嵌入向量;
  • 允许无界的查询长度。

仓库 v0 对照:rag_ingest的query子命令实现了这条链路(rag_ingest.cpp#L2328-L2484):加载启用源 → 解析embedding_json→ 用同一 provider 对--text生成查询嵌入 → 转成 SQLiteX'...'十六进制 BLOB 字面量(float_to_hex_blob,rag_ingest.cpp#L250-L263)→ 执行 vec0 KNN 查询:

SELECT c.chunk_id, c.source_id, SUBSTR(c.body, 1, 200) as content, v.distance, d.title FROM rag_vec_chunks v JOIN rag_chunks c ON c.chunk_id = v.chunk_id JOIN rag_documents d ON d.doc_id = c.doc_id WHERE v.embedding MATCH ( SELECT X'<query_hex>' AS embedding ) AND k = <limit> ORDER BY v.distance

(rag_ingest.cpp#L2428-L2438)。vec0 的 KNN 采用"子查询提供查询向量 +k = n"的写法,并可用AND c.source_id = ?叠加源过滤——这正是 MCP 层rag.search_vector工具(见 mcp-tools.md 第 4 节)内部 SQL 的原型。


9. 向量检索语义:距离、相似度与统一打分

9.1 距离 vs 相似度

根据嵌入模型与检索原语,向量检索可能返回:

  • 余弦距离(越小越好)
  • 余弦相似度(越大越好)
  • L2 距离(越小越好)

建议:在 MCP 响应中统一为"越大越好"的分数:

  • 若原始值是距离:score_vec = 1 / (1 + distance)或其他单调变换。

原始距离可保留在 debug 字段中。这与 mcp-tools.md 的约定一致:score_vec归一化、distance_raw可选返回;混合检索的统一score也是 higher-is-better。注意rag_ingest.cpp的 v0 直接输出v.distance并按ORDER BY v.distance升序——MCP 封装层需要完成上述变换。

9.2 过滤

过滤能力包括:

  • source_id限定;
  • 可选元数据过滤(文档级或 chunk 级)。

v0 中最容易的是按source_id过滤,因为rag_vec_chunks已将source_id存为元数据列(schema.sql 中的便捷列设计),可直接参与 WHERE/JOIN。


10. 混合检索集成:嵌入作为检索的一条腿

嵌入是混合检索(hybrid retrieval)的组成部分。mcp-tools.md 定义了两种推荐模式:

  1. Fuse(融合):FTS 与向量各取 top-N,按chunk_id合并,用 RRF 融合打分。
  2. FTS then vector(先 FTS 后向量):FTS 产宽候选集,再在候选内做向量重排。

两种模式下嵌入的职责不同:

  • Fuse 模式需要全局向量检索 top-N;
  • 候选模式需要把向量检索限制在候选 chunk_id 集合内(chunk_id IN (...), 通常更便宜、更精确,尤其当查询含强精确词元时)。

RRF 融合公式(来自 architecture-runtime-retrieval.md):

score = w_fts/(k0 + rank_fts) + w_vec/(k0 + rank_vec)

典型参数:k0=60、w_fts=1.0、w_vec=1.0。RRF 的优势在于无需分数校准即可稳健融合 bm25 与余弦距离这类异构分数域。


11. 运维控制:资源上限、批处理与背压

11.1 资源限制

嵌入生成必须被以下项约束:

  • 可嵌入的 chunk 最大尺寸;
  • 每个文档最多嵌入的 chunk 数;
  • 每个源嵌入速率限制;
  • 调用嵌入 provider 的超时。

v0 已落实超时与批量约束:timeout_ms(默认 20000)通过CURLOPT_TIMEOUT_MS生效;batch_size(默认 16)控制单次 API 请求的输入数。MCP 层的硬上限建议(来自 mcp-tools.md):k_max=50、candidates_max=500、query_max_bytes=8192、response_max_bytes=5_000_000、timeout_ms按工具类型 250–2000ms。

11.2 批量嵌入

为提升吞吐,批量嵌入:收集 N 个 chunk → 一次请求嵌入 N 个输入 → 存储结果。flush_embedding_batch()(rag_ingest.cpp#L1535-L1564)正是这一实现,其注释给出量化收益:100 个 chunk、batch_size=16 时仅需 7 次 API 调用(16×6+4),而非 100 次。

11.3 背压与异步嵌入(v1)

考虑将嵌入生成与摄入解耦:

  • 摄入只存 chunk;
  • 嵌入 worker 处理"pending"chunk 并回填向量。

收益:摄入保持快速;嵌入可独立扩缩;嵌入失败可重试。该设计中需为每个 chunk 存储状态记录:pending / ok / error、最后错误消息、重试计数。这一状态表与第 6 节的rag_chunk_embedding_state(可增加status、last_error、retry_count列)天然合流。


12. 推荐实现步骤(coding agent checklist)

v0(同步嵌入)

  1. 在 ingester 中实现embedding_json解析;
  2. 为每个 chunk 构建嵌入输入字符串;
  3. 调用嵌入 provider(开发期可用 stub);
  4. 向rag_vec_chunks插入向量行;
  5. 用"查询嵌入 + 向量 SQL"实现rag.search_vectorMCP 工具。

仓库中的 sample_mysql.sql(10 行 posts 样例数据,含长正文、空值、高分/低分过滤用例)与 sample_sqlite.sql(含完整embedding_json的源配置)可直接用于验证 v0 全链路。

v1(高效增量嵌入)

  1. 新增rag_chunk_embedding_state表;
  2. 按 chunk × model 存input_hash;
  3. 仅当哈希变化时重嵌入;
  4. 增加可选异步嵌入 worker;
  5. 为嵌入吞吐与失败增加指标。

13. 总结

  • 按 chunk 计算嵌入,而非按整篇文档;
  • 在rag_sources.embedding_json中显式定义嵌入输入;
  • 向量存入rag_vec_chunks(vec0);
  • 生产环境加入基于哈希的更新检测与可选异步嵌入 worker;
  • 在 MCP 响应中归一化向量分数(higher-is-better),并保留原始距离用于调试。

整套设计在仓库中的证据链清晰可循:schema 见 schema.sql 与 rag_ingest.cpp 的init_schema();嵌入配置样例见 sample_sqlite.sql;provider 抽象、批量嵌入与查询向量 SQL 见 rag_ingest.cpp(build_embedding_provider、flush_embedding_batch、query 子命令);检索侧接口契约见 mcp-tools.md 与 architecture-runtime-retrieval.md。

  • 后端
  • 数据库
  • 负载均衡

【免费下载链接】proxysql

High-performance proxy for MySQL and PostgreSQL

项目地址:https://gitcode.com/gh_mirrors/pr/proxysql
点击查看免费下载

相关推荐

上一篇:OpenClaw Mission Control生产部署终极指南:Docker Compose、systemd与反向代理TLS完整清单
下一篇:用StatsPAI旗舰Skill复现Card(1995):AERS为何自动发现IV估计竟然超过OLS

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询