注册
DM9向量功能体验:用DM9向量数据库给收藏文本做语义检索 #达梦数据库 #达梦同行者征文
专栏/技术分享/ 文章详情 /

DM9向量功能体验:用DM9向量数据库给收藏文本做语义检索 #达梦数据库 #达梦同行者征文

青空 2026/09/18 476 0 0
摘要

#达梦数据库 #达梦同行者征文

1. 背景

最近在学习DM9数据库的使用,其中有一个功能是向量数据库,刚好平时有个稍微困扰我的问题可以通过文本向量化来解决。

这个问题是这样的,我上网时会下载很多文本,也会做一些笔记。当我想找一些文本的时候,我知道它确实在我的电脑上,但我换了很多关键词都搜不到

比如我下载了一篇文章,我印象里它是讲述“赋比兴”的,但如果直接搜标题带“赋比兴”的文件是搜不到的,因为它的标题是《怎样解释诗经的赋、比、兴?》,被两个标点分隔开了,还好正文里出现了一次“赋比兴”三个字,让我通过内容搜索能搜到这篇文章

但如果标题和正文里没有我想出的关键字就搜不到了,比如像我想找“玩游戏刷视频的时间算不算浪费”,而它的标题是《罗素的“你能在浪费时间中获得乐趣,就不是浪费时间。”的言论怎么理解?》,用“玩游戏”去搜索,是根本找不到结果的,用“浪费时间”当关键字又会搜出一堆结果,还需要肉眼找半天,效率非常之低

我相信这不是我一个人会遇到的问题,我认为这是因为每个人习惯的表述不同,即使对同一个话题,两个人表述出来的关键词也不同,就导致通过关键词搜索不一定能找到自己脑中对应的文本,而向量化技术将文本转为向量就能比较好地解决这个问题,因为即使表述不同,但针对同一个话题的描述在向量化之后有很高的余弦相似度,可以借助这个相似度实现根据语义的搜索

这里用到的环境是这样的:

组件 说明
数据库 达梦 DM9,实例端口 5237
Python Python 3.10 + 官方 dmPython 驱动
embedding 火山方舟 doubao-embedding-vision-251215,输出 2048 维
数据 21 篇知乎问答导出的 markdown,带 frontmatter(title/author/url/upvote_num 等)

2. 思路

所以这里的思路就是把要找的文本转成一个高维向量,这个向量代表文档的语义。查询时把提问也转成向量,然后在高维空间里找距离最近的几个文档向量。这样即使标题里没有查询词的字面,只要内容讲的是这个意思,向量距离就会很近。

这个流程也就是RAG(检索增强生成)里最核心的向量化和检索环节,至于后面结合llm进行回答的部分在这个需求下暂时用不到,能够找到想找的文本原文就可以了

既然想好用embedding思路来解决这个问题了,需要来选定使用什么embedding模型,以及使用什么数据库来存储向量化数据

这里embedding模型选用火山方舟平台的doubao-embedding-vision-251215,因为有免费额度可以用。(跑完之后才想起来公司有提供免费的qwen3-embedding,之后可以用这个重做一遍)

而数据库领域,目前比较常见的支持向量的数据库产品分三类:

  1. 关系库内置:Oracle 23ai、DM9
  2. 关系库扩展插件:PostgreSQL的pgvector、MySQL9的VECTOR
  3. 专用向量数据库:Milvus、Qdrant、Weaviate等

对比如下

能力 DM9 pgvector(PG 扩展) Oracle 23ai Milvus
定位 关系库内置向量 关系库插件 关系库内置向量 专用分布式向量库
向量类型 VECTOR(维, 格式, 存储格式),1~65535 维,INT8/FLOAT32/FLOAT64/BINARY,稠密/稀疏 vector 最大 16000 维,另有 halfvec/sparsevec/bit VECTOR(维, 格式, 存储格式),INT8/FLOAT32/FLOAT64,稠密/稀疏 多向量字段,Float16/BFloat16/二进制/稀疏
向量索引 HNSW / IVFFLAT / DISKANN / BMP HNSW / IVFFLAT HNSW / IVF(另有混合向量索引) HNSW / IVF 系列 / DISKANN / GPU 索引等,种类最多
距离函数 余弦/L2/L1/内积 + VECTOR_DISTANCE 统一入口 距离操作符(<=> 等)四种度量 VECTOR_DISTANCE 多种度量 L2/内积/余弦等
近似查询 FETCH APPROX + TARGET ACCURACY ORDER BY 距离 LIMIT k(有索引即近似,ef_search 调精度) TARGET ACCURACY / approx 分桶 search_params 调 nprobe/ef
混合检索 CONTEXT INDEX 全文 + 向量,同库 SQL 组合 tsvector 全文 + 向量,SQL 组合 混合向量索引(全文+向量一体) BM25 稀疏 + 稠密,RRF 融合
SQL 生态 完整(JOIN/子查询/存储过程) 完整 完整 无 JOIN,查询表达力有限
事务/备份/权限 完整 完整 完整 弱,以最终一致为主
部署形态 已有 DM 环境即用 PG + 装扩展 需 Oracle 23ai 独立集群(依赖 etcd、对象存储等组件)

达梦和pgvector、Oracle 23ai都是“关系库+向量”,都有VECTOR类型 + HNSW/IVF 索引 + SQL 内混合查询。DM9 的差异点在索引种类上多了 DISKANN(磁盘友好,适合内存装不下的大规模场景)和 BMP(稀疏向量位图索引),并支持 INT8/BINARY 量化存储降成本。语法风格上 DM9 与 Oracle 23ai 接近(VECTOR_DISTANCE、TARGET ACCURACY 等),从Oracle迁移到达梦的迁移成本较低;

和专用向量库(Milvus)相比,差距在规模和向量特化能力上,专用库的主场是十亿级向量、多向量列、GPU 加速检索等领域,但往往需要独立集群部署,以及放弃 SQL 和强事务。如果向量数据本身就是达梦库里的业务数据,规模在千万级以内,为向量检索单独维护一套 Milvus 集群(加 etcd、对象存储)通常不划算

因为我对达梦数据库也比较熟悉,达梦9对向量功能的支持比较齐全,并且能满足我这个任务的需求,所以这里选择了达梦数据库,现在的DM9 原生支持向量类型(VECTOR)、向量索引和距离计算函数,向量、元数据、原文放同一张表,一条 SQL 就能完成检索

3. 做法

3.1. 数据准备

3.1.1. 解析 markdown

我下载的知乎回答是一个 md 文件,格式如下:

--- title: 怎样解释诗经的赋、比、兴? url: https://www.zhihu.com/question/22114515/answer/646662887 author: 杨小荷 upvote_num: 479 --- 赋比兴的概念原本非常简单,但是汉代及以后的经学家……

frontmatter 是 YAML,保存着作者、链接、点赞数等元数据,可以用 PyYAML 解析后,单独存列,检索命中后可以带出来展示

3.1.2. 分块:短文整篇输入,长文切段

分块粒度直接决定检索粒度,我选择的分块策略是按文章长度分层:

如果是正文少于3000字符的短篇,直接把整篇作为一个块。知乎回答围绕一个问题展开,语义集中,没必要切。21 篇里有 13 篇属于这类。

如果是正文长于3000字符的长篇:按空行拆自然段,段落累积到约 1500 字符落一块,超长单行则直接按 1500 硬切。并且在每块开头拼上文章标题作为上下文前缀,避免切出来的段落脱离标题后embedding 模型不知道它在讲什么。

切完后共 67 块,块长 111~2433 字符,平均 1318。其中一篇 2 万字的《如果让 AI 生成随机数》切成 6 块——这篇文章同时讲了伪随机、硬件熵源、LLM 采样多个子话题,整篇揉成一个向量的话,哪个子话题都沾一点、哪个都不突出;切开后,每个子话题有自己的向量,检索能定位到篇内的具体部分。

使用的代码如下

SHORT_LIMIT = 3000 # 短于此长度的整篇一块 BLOCK_TARGET = 1500 # 长文每块目标长度 def chunk_body(title, body): if len(body) <= SHORT_LIMIT: return [f"{title}\n{body}"] paras = [p.strip() for p in body.split("\n") if p.strip()] blocks, cur = [], "" for p in paras: while len(p) > BLOCK_TARGET * 1.5: # 超长单行硬切 if cur: blocks.append(f"{title}\n{cur}") cur = "" blocks.append(f"{title}\n{p[:BLOCK_TARGET]}") p = p[BLOCK_TARGET:] if cur and len(cur) + len(p) + 1 > BLOCK_TARGET: blocks.append(f"{title}\n{cur}") # 攒够一块,落盘 cur = p else: cur = f"{cur}\n{p}" if cur else p if cur: blocks.append(f"{title}\n{cur}") return blocks

3.2. 入库 DM9

3.2.1. 建表:VECTOR 列

CREATE TABLE zhihu_qa ( id INT IDENTITY PRIMARY KEY, title VARCHAR(500), author VARCHAR(100), url VARCHAR(500), upvote INT, chunk_index INT, chunk_text TEXT, embedding VECTOR(2048, FLOAT32) );

向量列的语法是 VECTOR(维度, 数据格式, 存储格式):

  • 维度取值是 1~65535,必须和实际插入的向量长度一致,建议建表时写死。DM9 也支持 VECTOR(*) 不定维列,但不定维会让维度一致性检查推迟到查询阶段,在做文本向量化的时候如果维度不同不会报错,不容易发现;
  • 数据格式支持 INT8(1 字节)、FLOAT32(4 字节,默认)、FLOAT64(8 字节)以及 BINARY。因为embedding 模型输出的是浮点向量,所以用 FLOAT32 存储
  • 存储格式支持 DENSE(稠密,默认)和 SPARSE(稀疏)。文本 embedding 是稠密向量,用默认值。

手册里明确了几条向量列的限制,比如向量列不能作主键或唯一键(所以用自增 id)、只能建向量索引(不能建普通索引)、HUGE 表不支持向量类型。

3.2.2. 插入:向量是字符串字面量

DM9 里向量的外部表示就是 ‘[0.12,0.34,…]’ 这样的字符串。插入时有两种写法:

-- 写法一:字符串直接赋给向量列,隐式转换 INSERT INTO zhihu_qa(title, chunk_text, embedding) VALUES('t', '正文', '[0.1,0.2,...]'); -- 写法二:TO_VECTOR 显式转换,可在参数里核对维度和格式 INSERT INTO zhihu_qa(title, chunk_text, embedding) VALUES(?, ?, TO_VECTOR(?, 2048, FLOAT32));

这里用写法二,TO_VECTOR(字符串, 维度, 格式) 的后两个参数和列定义对不上会直接报错,等于多一道校验。Python 侧用 dmPython 的 ? 占位符绑定,批量插入用 executemany:

INSERT_SQL = """ INSERT INTO zhihu_qa(title, author, url, upvote, chunk_index, chunk_text, embedding) VALUES (?, ?, ?, ?, ?, ?, TO_VECTOR(?, 2048, FLOAT32))""" rows = [(c["title"], c["author"], c["url"], c["upvote"], c["chunk_index"], c["chunk_text"], vec_to_str(vec)) for c, vec in zip(chunks, vectors)] cur.executemany(INSERT_SQL, rows) conn.commit()

vec_to_str 就是把模型输出的浮点数组拼成 ‘[0.05,-0.02,…]’:

def vec_to_str(vec): return "[" + ",".join(f"{v:.7g}" for v in vec) + "]"

调用 embedding API 的部分很简单:67 块逐条 POST,拿到 2048 维浮点数组。

3.2.3. 建向量索引

CREATE VECTOR INDEX idx_qa_hnsw ON zhihu_qa(embedding) ORGANIZATION NEIGHBOR GRAPH DISTANCE COSINE PARAMETERS(TYPE HNSW, NEIGHBOR 16, EFCONSTRUCTION 32);

DM9 内置四种向量索引:

索引 原理 适合
HNSW 多层近邻图,查询从顶层稀疏图逐层向下定位 在线检索,查询快
IVFFLAT 向量聚类成若干簇,查询只搜最近的几个簇 大规模数据,建索引快
DISKANN 磁盘友好图索引 内存装不下的大规模数据
BMP 块结构位图 稀疏向量

参数说明:

  • DISTANCE COSINE:建索引时就要定距离度量。文本 embedding 只关心方向不关心模长,用余弦;这个选择必须和查询时一致,不一致近似查询会拒绝走索引;
  • NEIGHBOR 16(图节点最大邻居数)、EFCONSTRUCTION 32(建图候选节点数):没写的话由 TARGET ACCURACY(预期准确率,默认 90)推导——准确率 ≤70 时默认 8/16,70~90 时 16/32,90~100 时 32/64。
  • 手册还支持 SHARD/SINGLE 存储方式(默认 SHARD 分片图,单片 5 万节点),不过小数据量不用关心。

这个demo只有 67 行数据,建 HNSW 索引不到 1 秒。这个量级建不建索引、精确还是近似,查询耗时都在 20ms 上下,看不出差别;但流程是完整的,数据量上到十万行以上,向量索引的价值才会体现。

3.3. 检索

3.3.1. 精确查询与近似查询

精确查询(k-NN):查询向量与全表所有向量算距离,取最小的 k 个。结果全局最优,代价 O(N):

SELECT title, author, upvote, chunk_index, chunk_text, COSINE_DISTANCE(embedding, TO_VECTOR(?, 2048, FLOAT32)) AS dist FROM zhihu_qa ORDER BY COSINE_DISTANCE(embedding, TO_VECTOR(?, 2048, FLOAT32)) FETCH FIRST 5 ROWS ONLY;

近似查询:同一个查询,FETCH 后面加一个 APPROX,就走 HNSW 索引,只检查“可能最近”的一部分候选:

SELECT title, author, upvote, chunk_index, chunk_text, COSINE_DISTANCE(embedding, TO_VECTOR(?, 2048, FLOAT32)) AS dist FROM zhihu_qa ORDER BY COSINE_DISTANCE(embedding, TO_VECTOR(?, 2048, FLOAT32)) FETCH APPROX FIRST 5 ROWS ONLY;

近似查询还可以带 WITH TARGET ACCURACY 90,或对 HNSW 指定 PARAMETERS(EFSEARCH n) 控制查图时的搜索宽度,在准确率和速度之间调。

距离函数方面,除了 COSINE_DISTANCE(a, b),DM9 还提供 L2_DISTANCE(欧氏)、L1_DISTANCE(曼哈顿)、INNER_PRODUCT(内积),以及统一入口 VECTOR_DISTANCE(a, b, COSINE)。文本检索用余弦的原因:文本向量的模长本身没有语义,语义体现在方向上。

Python 侧:

def search(conn, query_vec_str, top_k=5, approx=False): tpl = APPROX_SQL if approx else EXACT_SQL # 两者只差 APPROX 关键字 cur = conn.cursor() cur.execute(tpl.format(k=top_k), (query_vec_str, query_vec_str)) return cur.fetchall()

查询向量同样走 embedding API 得到,再 vec_to_str 转成字符串绑定。

3.3.2. 实测效果

向量查询 1:能 ctrl+F 搜到的。“怎么理解赋比兴”:

排名 标题 相似度
1 怎样解释诗经的赋、比、兴? 0.785
2 什么是文章的起承转合? 0.431
3 长篇小说写作中如何写好起承转合? 0.375

第一名的相似度和第二名拉开 0.35——正确答案断层领先,第二、三名的“写作手法”属于语义上确实沾边的合理结果。

查询 2:ctrl+F 搜不到的。“玩游戏刷视频的时间算不算浪费生命”

排名 标题 相似度
1 罗素的“你能在浪费时间中获得乐趣,就不是浪费时间。”的言论怎么理解? 0.462
2 文学翻译时一般都是如何处理谐音类的笑话? 0.298
3 老师为什么不录一次课用一辈子,而是要重复劳动? 0.294

我们在原文里 grep 过:“玩游戏”“刷视频”“浪费生命”三个词在对应的文章里一次也没出现过,导致任何字面匹配手段(编辑器 ctrl+F、SQL LIKE、全文索引)对这个查询都无能为力,而向量检索把它排到了第一,这就是语义检索的优势。那篇回答的内容恰好是在反驳“死宅看剧打游戏不算浪费时间”的说法,语义上和查询高度相关。

近似查询(FETCH APPROX)对以上查询的返回与精确查询完全一致,符合小数据量下 HNSW 召回率接近 100% 的预期。

查询 3:命中长文的中间块。“为什么大模型每次回答都不一样”:

排名 标题 命中块 相似度
1 如果让AI生成随机数……是真随机吗? 块 14 0.536
2 同上 块 0 0.499
3 同上 块 3 0.469

前三名全部来自这篇 2 万字长文的不同部分——这正是分块策略的意义:整篇一个向量只能告诉定位到“答案在这篇文章里”,切块之后能定位到“讲 LLM 输出不确定性的是第 14 块”,内容文本更少,内容更聚焦,如果是 RAG 场景,更适合喂给LLM,占用的token更少

4. 总结

本次主要测试了

  1. 通过embedding把文本变成向量,可以方便地通过距离判断语义相似性;
  2. 使用DM9 的 VECTOR 列存向量,字符串字面量 + TO_VECTOR 完成转换,和普通列一起增删查改;

这样向量、元数据、原文在同一个库里,WHERE 过滤、ORDER BY、事务、备份全都是我们很熟悉的sql语法,很方便上手。

5. 改进方向与展望

5.1.1. 改进方向

  • 分块方式有待提高:目前的分块只看字符数,虽然对普通段落够用,但会把 markdown 里的表格、代码块拦腰切断,导致语义不连贯。更好的做法是先识别文档结构,表格、代码块整体保留不切,按标题层级分节,每块带上标题路径。社区内的其他帖子也实践过,这样做在细节类问题上的命中率能翻倍;
  • 相似度阈值过滤:目前永远返回 top-k,拿一个库里根本没有答案的问题来查,它也会硬凑 k 条回来。可以加一个距离过滤(WHERE COSINE_DISTANCE(…) <= 阈值),让库里没有相关内容就返回 0 条;
  • 混合检索:向量擅长语义,但抓不牢精确的词——查一个具体的函数名、报错码,语义相近但没出现这个词的块可能排到真正含该词的块前面。DM9 的全文索引(CONTEXT INDEX + CONTAINS)恰好相反,认词不认义。两路各取 top-N 后用 RRF(倒数排名融合)合并,互相兜底;
  • 可以接入LLM生成回答:把检索出的块拼进 prompt 喂给大模型,并约束它“只依据片段回答、没有就直说”,知识库就从“找得到”升级为“答得出”,构成完整的 RAG。

5.1.2. 未来展望

  • 图片向量化:本次用的 doubao-embedding-vision-251215 本身就是多模态模型,官方支持文本、图像等多种模态输入,本文只用了它的文本模态。我的收藏夹里除了文章还有大量图片、截图,把图片同样向量化存进 DM9 的 VECTOR 列,就可以实现“用一句话描述,搜出收藏里的那张图”,数据库侧的表结构、索引、检索 SQL 甚至都不用改;
  • 更大规模下的索引实测:本次 67 行数据的规模体现不出向量索引的收益,后续可以造十万级以上的向量数据,实测 HNSW / IVFFLAT / DISKANN 在建索引耗时、查询延迟、召回率上的差异,验证第六节参数选择的实际效果。

6. 附录

6.1. 脚本结构

/Work/dm9Vector/
├── parse.py     # 解析 md,提取 frontmatter,分块
├── embed.py     # 调 embedding API,向量 <-> 字符串转换
├── db.py        # 建表、插入、建索引、检索 SQL
├── build_db.py  # 一键建库:python310 build_db.py --force
└── search.py    # 检索入口:python310 search.py "查询词" [--approx] [top_k]

db.py 中的检索 SQL 与建表、建索引语句即上文各节所列,完整代码随文档归档。

6.2. 常见问题

6.2.1. 维度不一致

TO_VECTOR 的维度、列定义的维度、模型输出维度三者必须一致。embedding 模型换一个,维度往往就变了(同为火山系模型,不同版本有 2048/1024 之分),所以先实测维度再建表

6.2.2. 近似查询不走索引的几种情况

手册 4.2 列了近似查询退化为精确计算的条件,最常踩的:

  • 查询的距离度量和索引不一致。索引建的是 COSINE,查询写 EUCLIDEAN,静默不走索引;
  • HNSW 索引查询 k >= 1000 时退化;
  • 在视图上做近似查询不走索引;
  • 表只有一行数据时不走索引。

发现 FETCH APPROX 比精确查询还慢、或总返回全表行数时,可以先对照这几条排查。

6.2.3. 环境相关的两个小坑

一是 embedding 服务商的接口不都是标准 OpenAI 格式,接新模型先跑通“单条文本 → 拿到定长向量”再动手写库表代码;二是虚拟机 NAT 网络下新建 HTTPS 连接偶发被拒,代码里 HTTP 连接复用(requests.Session)加失败重试即可,与数据库无关。

6.3. 参考

  • 《DM9-向量数据开发指南》,达梦安装目录 /dm9/doc/语言与开发/(本文所有向量语法均以此手册核对)
  • 《用DM9当向量数据库,给自己建个RAG知识库》,达梦技术社区:https://eco.dameng.com/community/post/20260910203904DA0DV8RJMECM6I066O
  • 火山方舟 doubao-embedding-vision-251215(Seed1.6-Embedding)模型文档,火山引擎方舟控制台
  • pgvector:https://github.com/pgvector/pgvector
  • Oracle Blog - Using HNSW Vector Indexes in AI Vector Search:https://blogs.oracle.com/database/using-hnsw-vector-indexes-in-ai-vector-search
  • Milvus Docs - Index Explained:https://milvus.io/docs/index-explained.md
  • Milvus Docs - Full Text Search:https://milvus.io/docs/full-text-search.md
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服