一、前言
本文基于京东内部向量数据库vearch进行实践。Vearch 是对大规模深度学习向量进行高性能相似搜索的弹性分布式系统。详见: https://github.com/vearch/zh_docs/blob/v3.3.X/docs/source/overview.rst
二、探索
初次认识向量数据库,一脸懵逼?

向量是什么?如何将文本转换为向量?如何确定维度?如何定义表结构?如何选择索引方式,建表参数如何配置?检索参数如何配置?分片数副本数如何选择等等
随着对文档的逐渐熟悉以及和vearch相关同事的沟通,以上问题迎刃而解,具体的不再赘述。主要记住以下几点:
1、 文本转向量:采用大模型网关接口 domain/embeddings 传入对应的模型如:text-embedding-ada-002-2和待转换的文本即可;
2、 向量维度:这个和向量转换所采用的模型有关,细节不用关注;
3、 建表参数的选择以及表结构:主要在于retrieval_type 检索模型的选择,具体的可以参考文档。经过综合考虑,决定采用 HNSW:
| 字段标识 | 字段含义 | 类型 | 是否必填 | 备注 |
|---|---|---|---|---|
| metric_type | 计算方式 | string | 是 | L2或者InnerProduct |
| nlinks | 节点邻居数量 | int | 是 | 默认32 |
| efConstruction | 构图时寻找节点邻居过程中在图中遍历的深度 | int | 是 | 默认40 |
1"retrieval_type": "HNSW", 2"retrieval_param": { 3 "metric_type": "InnerProduct", 4 "nlinks": 32, 5 "efConstruction": 40 6} 7 8注意: 1、向量存储只支持MemoryOnly 9 2、创建索引不需要训练,index_size 值大于0均可
具体的建表示例见后文。
4、 分片数和副本数结合实际数据量评估,如果无法评估,按照最少资源申请即可,后续可扩展。
三、实践
1、 建表(space)
为了简化操作,实行db(库)-space(表)一对一的方案,弱化库的概念。经过一系列探索之后定义出了通用的space结构:
1{ 2 "name": "demphah", 3 "partition_num": 3, 4 "replica_num": 3, 5 "engine": { 6 "name": "gamma", 7 "index_size": 1, 8 "id_type": "String", 9 "retrieval_type": "HNSW", 10 "retrieval_param": { 11 "metric_type": "InnerProduct", 12 "nlinks": 32, 13 "efConstruction": 100, 14 "efSearch": 64 15 } 16 }, 17 "properties": { 18 "vectorVal": { 19 "type": "vector", 20 "dimension": 1536 21 }, 22 "contentVal": { 23 "type": "string" 24 }, 25 "chunkFlagId": { 26 "type": "string", 27 "index": true 28 }, 29 "chunkIndexId": { 30 "type": "integer", 31 "index": true 32 } 33 } 34}
字段说明:
engine、partition_num等都是固定的参数,properties中所列字段皆为通用字段,如果有扩展字段如:skuId,storeId追加即可
| 字段名 | 含义 | 类型 | 说明 |
|---|---|---|---|
| vectorVal | 文本向量 | vector | 维度与选用模型有关 |
| contentVal | 源文本 | string | |
| chunkFlagId | 文件唯一id | string | 文件的标识id,用于串联分块后的片段 |
| chunkIndexId | 文件分段位置 | integer | 从0开始,递增 |
| skuId ... | | | 扩展字段见上 |
这里file的概念可以理解为一个单元,可能是一个文件,也可能是一个url,总之就是一个数据整体。
2、 分段写入
这里针对通用文件描述,比如提供一个pdf文件如何导入向量库:
a. 首先上传文件到oss,然后根据对应的fileKey获取到文件数据流
b. 再根据各种拆分场景(按行、字节数、正则拆分等)分成片段
c. 分段写入向量库:
1 /** 2 * 将字符串转换为向量并插入数据库 3 * <p> 4 * 目前所有的知识库管理端写入全走这个方法 5 * 6 * @param dbName 数据库名称 7 * @param spaceName 空间名称 8 * @param str 字符串 9 * @param flagId 标志ID 10 * @param chunkIndexId 块索引ID 11 * @param properties 属性 12 */ 13 private void embeddingsAndInsert(String dbName, String spaceName, String str, String flagId, Integer chunkIndexId, Map<String, Object> properties) { 14 // 先向数据库写入一条记录,记录当前文档的写入操作 15 int success = knbaseDocRecordService.writeDocRecord(spaceName, flagId, chunkIndexId.longValue(), 0, str); 16 if (success <= 0) { 17 log.error("writeDocRecord失败 {},{},{}", spaceName, flagId, chunkIndexId); 18 } 19 20 // 分块转向量并写入 21 List<Float> embeddings = GatewayUtil.baseEmbeddings(str); 22 if (CollectionUtils.isEmpty(embeddings)) { 23 return; 24 } 25 26 KnBaseVecDto knBaseVecDto = buildKnBaseVecDto(new FeaVector(embeddings),flagId,chunkIndexId,str); 27 28 Map<String, Object> newPros = JsonUtil.obj2Map(knBaseVecDto); 29 if (MapUtils.isNotEmpty(properties)) { 30 newPros.putAll(properties); 31 } 32 // {"_index":"kn_base_file_db","_type":"kn_base_file_space","_id":"-8182839813441244911","status":200} 33 String insert = VearchUtil.insert(dbName, spaceName, null, newPros); 34 if (StringUtils.isBlank(insert) || !insert.contains("_index")) { 35 log.error("写入失败的块:{},{}", chunkIndexId, insert); 36 } 37 }
3、 数据记录
上文写知识库的过程有个 knbaseDocRecordService.writeDocRecord 的逻辑,用于记录写入的片段。下文详细介绍其中用到的mysql表:
1、 表1 space记录表
注:主要用于记录创建的space,以及查询管控,如禁用某个space等
1CREATE TABLE `xxx_vearch_spaces` ( 2 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 3 `type` tinyint(3) NOT NULL COMMENT '类型', 4 `status` tinyint(3) NOT NULL COMMENT '状态', 5 `space` varchar(127) NOT NULL COMMENT '空间标识', 6 `db` varchar(127) NOT NULL COMMENT '库标识', 7 `desc` varchar(127) NOT NULL COMMENT '空间描述', 8 `ext` varchar(4095) NOT NULL DEFAULT '' COMMENT '扩展字段', 9 `creator` varchar(127) NOT NULL DEFAULT '' COMMENT '创建人', 10 `created` timestamp NOT NULL COMMENT '创建时间', 11 `modifier` varchar(127) NOT NULL DEFAULT '' COMMENT '修改人', 12 `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 13 `deleted` tinyint(3) NOT NULL DEFAULT '0' COMMENT '已删除(0:否;1:是)', 14 PRIMARY KEY (`id`) USING BTREE, 15 UNIQUE KEY `uniq_space_db` (`space`,`db`) USING BTREE 16) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='xxx向量空间'
2、 表2 file记录表
注:主要用于记录space下的file,以及查询管控,如禁用某个file,以及关联查询对应的全部片段。
1CREATE TABLE `xxx_spaces_knbase` ( 2 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 3 `status` tinyint(3) NOT NULL COMMENT '状态', 4 `space` varchar(127) NOT NULL COMMENT '空间标识', 5 `file_name` varchar(255) NOT NULL COMMENT '文件名', 6 `file_desc` varchar(511) NOT NULL COMMENT '文件描述', 7 `byte_num` bigint(20) unsigned NOT NULL COMMENT '字符数', 8 `hit_count` int(10) unsigned NOT NULL COMMENT '命中次数', 9 `ext` varchar(4095) NOT NULL DEFAULT '' COMMENT '扩展字段', 10 `creator` varchar(127) NOT NULL DEFAULT '' COMMENT '创建人', 11 `created` timestamp NOT NULL COMMENT '创建时间', 12 `modifier` varchar(127) NOT NULL DEFAULT '' COMMENT '修改人', 13 `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 14 `deleted` tinyint(3) NOT NULL DEFAULT '0' COMMENT '已删除(0:否;1:是)', 15 `file_flag_id` varchar(255) DEFAULT NULL COMMENT '文件唯一标识', 16 PRIMARY KEY (`id`) USING BTREE, 17 KEY `idx_space` (`space`) USING BTREE 18) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='xxx空间知识库'
3、 表3 paragraph记录表
注:主要用于记录file拆分的片段,包括当前位置,查询命中数等。
1CREATE TABLE `xxx_knbase_doc_record` ( 2 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', 3 `space` varchar(127) NOT NULL COMMENT '空间标识', 4 `file_flag_id` varchar(255) NOT NULL COMMENT '文件标识', 5 `d_index` bigint(20) unsigned NOT NULL COMMENT '文档位置', 6 `hit_count` int(10) unsigned NOT NULL COMMENT '命中次数', 7 `ext` varchar(4095) NOT NULL DEFAULT '' COMMENT '扩展字段', 8 `creator` varchar(127) NOT NULL DEFAULT '' COMMENT '创建人', 9 `created` timestamp NOT NULL COMMENT '创建时间', 10 `modifier` varchar(127) NOT NULL DEFAULT '' COMMENT '修改人', 11 `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', 12 `deleted` tinyint(3) NOT NULL DEFAULT '0' COMMENT '已删除(0:否;1:是)', 13 PRIMARY KEY (`id`) USING BTREE, 14 UNIQUE KEY `uniq_space_file_idx` (`space`,`file_flag_id`,`d_index`) USING BTREE 15) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='xxx知识文档记录'
四、总结
向量数据库对于大模型应用落地来说至关重要,有些不可外露的内部数据可以存储在向量库中,用于内部检索。随着向量库中数据的丰富,大模型推理回答的能力也将更加精准。
上文的设计比如space中的chunkFlagId可以关联出原始的整个文件;chunkIndexId可以控制数据的查询范围,另一方面可以通过此字段实现分页(vearch目前不支持分页查询)以及全文导出。xxx_knbase_doc_record表中记录了片段的记录,可用于计算片段的chunkIndexId,一方面避免重复,另一方面保证属性的递增,可用于扩展很多能力。
目前向量数据库的检索只支持基本的向量检索和关键字检索,后续会逐步优化混合检索等方案以提高检索准确率等。
