#达梦数据库 #达梦同行者征文
最近在学习DM9数据库的使用,其中有一个功能是向量数据库,刚好平时有个稍微困扰我的问题可以通过文本向量化来解决。
这个问题是这样的,我上网时会下载很多文本,也会做一些笔记。当我想找一些文本的时候,我知道它确实在我的电脑上,但我换了很多关键词都搜不到
比如我下载了一篇文章,我印象里它是讲述“赋比兴”的,但如果直接搜标题带“赋比兴”的文件是搜不到的,因为它的标题是《怎样解释诗经的赋、比、兴?》,被两个标点分隔开了,还好正文里出现了一次“赋比兴”三个字,让我通过内容搜索能搜到这篇文章
但如果标题和正文里没有我想出的关键字就搜不到了,比如像我想找“玩游戏刷视频的时间算不算浪费”,而它的标题是《罗素的“你能在浪费时间中获得乐趣,就不是浪费时间。”的言论怎么理解?》,用“玩游戏”去搜索,是根本找不到结果的,用“浪费时间”当关键字又会搜出一堆结果,还需要肉眼找半天,效率非常之低
我相信这不是我一个人会遇到的问题,我认为这是因为每个人习惯的表述不同,即使对同一个话题,两个人表述出来的关键词也不同,就导致通过关键词搜索不一定能找到自己脑中对应的文本,而向量化技术将文本转为向量就能比较好地解决这个问题,因为即使表述不同,但针对同一个话题的描述在向量化之后有很高的余弦相似度,可以借助这个相似度实现根据语义的搜索
这里用到的环境是这样的:
| 组件 | 说明 |
|---|---|
| 数据库 | 达梦 DM9,实例端口 5237 |
| Python | Python 3.10 + 官方 dmPython 驱动 |
| embedding | 火山方舟 doubao-embedding-vision-251215,输出 2048 维 |
| 数据 | 21 篇知乎问答导出的 markdown,带 frontmatter(title/author/url/upvote_num 等) |
所以这里的思路就是把要找的文本转成一个高维向量,这个向量代表文档的语义。查询时把提问也转成向量,然后在高维空间里找距离最近的几个文档向量。这样即使标题里没有查询词的字面,只要内容讲的是这个意思,向量距离就会很近。
这个流程也就是RAG(检索增强生成)里最核心的向量化和检索环节,至于后面结合llm进行回答的部分在这个需求下暂时用不到,能够找到想找的文本原文就可以了
既然想好用embedding思路来解决这个问题了,需要来选定使用什么embedding模型,以及使用什么数据库来存储向量化数据
这里embedding模型选用火山方舟平台的doubao-embedding-vision-251215,因为有免费额度可以用。(跑完之后才想起来公司有提供免费的qwen3-embedding,之后可以用这个重做一遍)
而数据库领域,目前比较常见的支持向量的数据库产品分三类:
对比如下
| 能力 | 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 就能完成检索
我下载的知乎回答是一个 md 文件,格式如下:
---
title: 怎样解释诗经的赋、比、兴?
url: https://www.zhihu.com/question/22114515/answer/646662887
author: 杨小荷
upvote_num: 479
---
赋比兴的概念原本非常简单,但是汉代及以后的经学家……
frontmatter 是 YAML,保存着作者、链接、点赞数等元数据,可以用 PyYAML 解析后,单独存列,检索命中后可以带出来展示
分块粒度直接决定检索粒度,我选择的分块策略是按文章长度分层:
如果是正文少于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
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(维度, 数据格式, 存储格式):
VECTOR(*) 不定维列,但不定维会让维度一致性检查推迟到查询阶段,在做文本向量化的时候如果维度不同不会报错,不容易发现;手册里明确了几条向量列的限制,比如向量列不能作主键或唯一键(所以用自增 id)、只能建向量索引(不能建普通索引)、HUGE 表不支持向量类型。
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 维浮点数组。
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 | 块结构位图 | 稀疏向量 |
参数说明:
这个demo只有 67 行数据,建 HNSW 索引不到 1 秒。这个量级建不建索引、精确还是近似,查询耗时都在 20ms 上下,看不出差别;但流程是完整的,数据量上到十万行以上,向量索引的价值才会体现。
精确查询(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 转成字符串绑定。
向量查询 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更少
本次主要测试了
这样向量、元数据、原文在同一个库里,WHERE 过滤、ORDER BY、事务、备份全都是我们很熟悉的sql语法,很方便上手。
/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 与建表、建索引语句即上文各节所列,完整代码随文档归档。
TO_VECTOR 的维度、列定义的维度、模型输出维度三者必须一致。embedding 模型换一个,维度往往就变了(同为火山系模型,不同版本有 2048/1024 之分),所以先实测维度再建表
手册 4.2 列了近似查询退化为精确计算的条件,最常踩的:
发现 FETCH APPROX 比精确查询还慢、或总返回全表行数时,可以先对照这几条排查。
一是 embedding 服务商的接口不都是标准 OpenAI 格式,接新模型先跑通“单条文本 → 拿到定长向量”再动手写库表代码;二是虚拟机 NAT 网络下新建 HTTPS 连接偶发被拒,代码里 HTTP 连接复用(requests.Session)加失败重试即可,与数据库无关。
文章
阅读量
获赞
