本文基于京东内部向量数据库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
"retrieval_type": "HNSW",
"retrieval_param": {
"metric_type": "InnerProduct",
"nlinks": 32,
"efConstruction": 40
}
注意: 1、向量存储只支持MemoryOnly
2、创建索引不需要训练,index_size 值大于0均可
具体的建表示例见后文 。
4、 分片数和副本数结合实际数据量评估,如果无法评估 , 按照最少资源申请即可,后续可扩展 。
实践
1、 建表(space)
为了简化操作,实行db(库)-space(表)一对一的方案,弱化库的概念 。经过一系列探索之后定义出了通用的space结构:
{
"name": "demphah",
"partition_num": 3,
"replica_num": 3,
"engine": {
"name": "gamma",
"index_size": 1,
"id_type": "String",
"retrieval_type": "HNSW",
"retrieval_param": {
"metric_type": "InnerProduct",
"nlinks": 32,
"efConstruction": 100,
"efSearch": 64
}
},
"properties": {
"vectorVal": {
"type": "vector",
"dimension": 1536
},
"contentVal": {
"type": "string"
},
"chunkFlagId": {
"type": "string",
"index": true
},
"chunkIndexId": {
"type": "integer",
"index": true
}
}
}
字段说明:
engine、partition_num等都是固定的参数,properties中所列字段皆为通用字段,如果有扩展字段如:skuId,storeId追加即可
字段名含义类型说明vectorVal文本向量vector维度与选用模型有关contentVal源文本string?
chunkFlagId文件唯一idstring文件的标识id,用于串联分块后的片段chunkIndexId文件分段位置integer从0开始,递增skuId ... ?
扩展字段见上
这里file的概念可以理解为一个单元,可能是一个文件,也可能是一个url,总之就是一个数据整体 。
2、 分段写入
这里针对通用文件描述,比如提供一个pdf文件如何导入向量库:
a. 首先上传文件到oss , 然后根据对应的fileKey获取到文件数据流
b. 再根据各种拆分场景(按行、字节数、正则拆分等)分成片段
c. 分段写入向量库:
/**
* 将字符串转换为向量并插入数据库
* <p>
* 目前所有的知识库管理端写入全走这个方法
* @param dbName 数据库名称
* @param spaceName 空间名称
* @param str 字符串
* @param flagId 标志ID
* @param chunkIndexId 块索引ID
* @param properties 属性
*/
private void embeddingsAndInsert(String dbName, String spaceName, String str, String flagId, Integer chunkIndexId, Map<String, Object> properties) {
// 先向数据库写入一条记录,记录当前文档的写入操作
int success = knbaseDocRecordService.writeDocRecord(spaceName, flagId, chunkIndexId.longValue(), 0, str);
if (success <= 0) {
log.error("writeDocRecord失败 {},{},{}", spaceName, flagId, chunkIndexId);
}
// 分块转向量并写入
List<Float> embeddings = GatewayUtil.baseEmbeddings(str);
if (CollectionUtils.isEmpty(embeddings)) {
推荐阅读
- 如何正确选择NoSQL数据库
- 哈啰云原生架构落地实践
- 为什么数据库连接池不采用 IO 多路复用?
- 泰国旅游落地签要多少钱一年 泰国旅游落地签要多少钱
- 过去一年,我看到了数据库领域的十大发展趋势
- 让数据库和缓存数据保持一致的三种策略
- 无法落地的凄美爱情,多年后哈林才惊知伊能静深情
- 落地生根是什么生肖,落地生根的是什么动物
- MySQL数据库如何生成分组排序的序号
- 一篇文章,彻底理解数据库操作语言:DDL、DML、DCL、TCL