1、背景作为一家搜索引擎公司,我们会很倚赖 ES 帮忙处理包括文章召回,数据源划分,实体、标签管理等任务,而且都收到了不错的结果。 最近我们需要对行业知识库进行建模,其中可能会涉及到实体匹配、模糊搜索、向量搜索等多种召回和算分方式,最终我们选择了通过 ES 7.X (最终选择 7.10)里的新功能,Dense vector 帮忙一起完成这部分的需求。 2、技术选型2.1 解决方案需求
2.2 使用场景设计
- 根据
query 理解结果构建的 query 语句进行数据召回
2.3 数据结构设计在确定了数据的使用场景我们确定了数据结构中,大致会包含以下一些字段 - 分类 flag:知识主要分类及推荐 category 等
- 其他属性:包括生效、删除、修改时间等支持性的通用属性

2.4 解决方案对比为了能支持上述的使用需求,我们对比了包括 ES 、Faiss 等多种解决方案。其中,Faiss 和 SPTAG 只是核心算法库,需要进行二次开发包装成服务;Milvus 的 1.x 版本中只能存储 id 和 向量 ,不能完整的满足我们的使用需求;基于集群稳定性和可维护性等考虑,相对于后置插件的部署,我们更倾向于使用 ES 的原生功能,所以选择 ES 的原生向量搜索功能作为我们的最终选择。 对比参考: 种类 | 实现语言 | 客户端支持 | 多条件召回 | 学习成本 | 引入成本 | 运维成本 | 分布式 | 性能 | 社区 | 备注 |
---|
Elasticsearch | Java | Java/Python | yes | 低 | 低 | 中 | yes | 中 | 活跃 | 原生功能 | Faiss | Python | Python | no | 中 | 高 | 高 | no | 高 | 一般 | 需要二次开发 | Milvus | Python + GoLang | Python/Java/GoLang | no | 中 | 中 | 中 | no | 高 | 一般 | 1.x 功能不全 | OpenDistro Elasticsearch KNN | Java + C++ | Java/Python | yes | 中 | 中 | 中 | yes | 中 | 一般 | 内置插件 | SPTAG | C++ | Python + C# | no | 高 | 中 | 中 | no | 高 | 一般 | 需要二次开发 |
3、数据流转流程3.1 离线数据处理部分
3.2 在线数据召回部分
4、ES 向量搜索的使用示例4.1 索引设计Settings :
{ "settings": { "number_of_shards": 3, "number_of_replicas": 2, "index": { "routing": { "allocation": { "require": { "node_group": "hot" // 1) } } }, "store": { "preload": [ // 2) "knowledge", "category", "available", "confidence", "del", "kid" ] }, "search": { "slowlog": { "threshold": { "query": { "warn": "1s" // 3) }, "fetch": { "warn": "1s" // 3) } } } }, "translog": { "flush_threshold_size": "512mb", // 4) "sync_interval": "5m", // 4) "durability": "async" // 4) }, "sort": { "field": [ // 5) "kid", "confidence" ], "order": [ // 5) "asc", "desc" ] } } } }
- 由于向量数据较大,所以倾向于将整个索引都放置在硬件性能更好的节点
- 知识库的重建是离线的,会在更新时进行大量写入,所以对
translog 的提交间隔拉长,加快写入速度 - 在实际使用中kid是自增id,同时可能会对知识的置信度做排序等,所以会使用
sort field 存储这两个字段
Mapping :
{ "mappings": { "properties": { "kid": { "type": "keyword" }, "knowledge": { "type": "keyword" }, "knowledge_phrase": { // 1) "type": "text", "analyzer": "faraday" }, "attribue": { // 1) "type": "keyword", "fields": { "phrase": { "type": "text", "analyzer": "faraday" } } }, "value": { // 1) "type": "keyword", "fields": { "phrase": { "type": "text", "analyzer": "faraday" } } }, "confidence": { // 2) "type": "double" }, "category": { "type": "keyword" }, "vector": { // 3) "type": "dense_vector", "dims": 512 }, "ref": { "type": "text", "index": false }, "available": { "type": "keyword" }, "del": { "type": "keyword" }, "create_timestamp": { "type": "date", "format": [ "strict_date_hour_minute_second", "yyyy-MM-dd HH:mm:ss" ] }, "update_timestamp": { "type": "date", "format": [ "strict_date_hour_minute_second", "yyyy-MM-dd HH:mm:ss" ] } } } }
- 除了对知识条目的完整搜索之外,还会需要进行模糊检索,我们使用了自研的
farady 分词器对知识条目的各部分进行了分词处理 - 知识库中的知识条目会有一部分进行专家/人工审核和维护,所以会对不同的条目设置不同的置信度
- 数据预处理之后会转成 512 位的向量存在这个字段中
4.2 数据流转- 此处
模型A 模型B 为自研模型,运用了包括知识密度计算等算法以及 bert tersonflow 等框架
- 专家团队会针对数据库中的知识条目进行审核、修改和迭代
- 算法团队会根据知识条目的更新以及其他的标注对数据链路中的模型进行迭代,对在线知识库进行更新
- 前端收到请求之后调用
query 理解 组件进行分析 - 剔除无效内容之后,找出
query 里的分类信息等意图之后,构建用来召回的向量和相关的筛选条件 - 通过组合出来的
ES 的 query 条件对知识库进行筛选,并配合置信度等对结果进行调整 - 对召回结果进行不同策略的分数调整和排序,最后输出给前端
4.3 示例 queryPOST knowledge_current_reader/_search { "query": { "script_score": { "query": { "bool": { "filter": [ { "term": { "del": 0 } }, { "term": { "available": 1 } } ], "must": { "bool": { "should": [ { "term": { "category": "type_1", "boost": 10 } }, { "term": { "category": "type_2", "boost": 5 } } ] } }, "should": [ { "match_phrase": { "knowledge_phrase": { "query": "some_query", "boost": 10 } } }, { "match": { "attribute": { "query": "some_query", "boost": 5 } } }, { "match": { "value": { "query": "some_query", "boost": 5 } } }, { "term": { "knowledge": { "value": "some_query", "boost": 30 } } }, { "term": { "attribute": { "value": "some_query", "boost": 15 } } }, { "term": { "value": { "value": "some_query", "boost": 10 } } } ] } }, "script": { "source": "cosineSimilarity(params.query_vector, 'vector') + sigmoid(1, Math.E, _score) + (1 / Math.log(doc['confidence'].value))", "params": { "query_vector": [ ... ] } } } } }
- 上述
query 的条件、参数仅做示意,属于实际线上使用的脱敏、简化版 - 边界条件及空值在辅助服务和
pipeline 中进行处理,简化了其中边界条件处理和判断部分逻辑
5、遇到的问题5.1 响应时间长由于需要进行向量计算,ES 需要耗费大量时间、资源做距离计算,为此我们进行了以下一些优化: - 为了保证特征的表征,我们并没有调整由
bert 框架输出的向量位数 - 在权衡了存取效率、数据精度和计算速度之后,我们将每一个
label 的精度由16 位截取为5 位小数 - 这样虽然损失了部分精度(约
X% ),但是大大降低了存取和计算时间(约 Y% )
- 在进行
query 之前预先对意图、可能分类进行分析 - 为了减少纳入计算排序的数据,我们会在
query 组装之前对原始 query 内容进行分析 - 配合用户行为埋点和专家的先验知识,将知识进行大致分类,并对
query 和分类进行不同权重的匹配 - 这样虽然降低了召回率(约
X% ),但增加了准确性(约 Y% ),同时也提高了部分计算效率(约 Z% )
- 将一部分分数计算的逻辑外置,尽可能精简
ES 需要处理的运算逻辑 - 在召回之后增加多种打分策略,通过配置进行应用、权重调整等操作
- 这样降低了
ES 的响应时间(约 X% ),同时通过外置的打分公式调整,间接的提高了准确性(约 Y% )
5.2 知识质量参差不齐由于知识条目是通过算法进行抽取的,而且知识还会存在一定的时效性,可能造成知识的不准确等问题,为此我们进行了以下一些优化: - 选取更加优质的知识抽取结果对线上数据进行全量/增量更新
- 经过
X 批次的迭代,将知识的正确性从 Y% 提高到了 Z%
- 将仅存在部分助词(如
的 )差异的知识条目进行过滤、合并 - 给部分热门的知识条目设置过期时间,并通过部分人工审核的方式干预知识条目的生产
- 维护了
X 类目的 Y 条专家知识,同时经过人工干预了大概 Z% 的知识条目,将知识的正确性从 W% 提高到了 K%
结论与展望本文依托我们公司的使用场景,对围绕 ES 向量字段(Dense vector )构建的一个系统进行了大致描述,同时对一些常见问题及解决方案进行了阐述。 目前该方案支持了我们对于知识库的相关搜索功能,相较于之前的纯基于实体识别和 ngram 匹配的方案整体准确率和召回率都有将近两位数百分比的提升。 未来我们会对整个系统的响应速度、稳定性进行提升,并对知识库的构建效率以及知识的准确性持续进行迭代。 作者介绍死敌wen,Elastic 认证工程师,搜索架构师,10年+工作经验,毕业于复旦大学。 博客:https://blog.csdn.net/weixin_40601534 Github:https://github.com/godlockin 说明
上个月,死磕 Elasticsearch 知识星球搞了:“群智涌现”杯输出倒逼输入——Elastic干货输出活动。 后续会不定期逐步推出系列文章,目的:以文会友,“输出倒逼输入”。 更短时间更快习得更多干货!
已带领77位球友通过 Elastic 官方认证!
|