前端模糊搜索 Elasticsearch 向量搜索的工程化实战

1、背景

作为一家搜索引擎公司,我们会很倚赖 ES 帮忙处理包括文章召回,数据源划分,实体、标签管理等任务,而且都收到了不错的结果。

最近我们需要对行业知识库进行建模,其中可能会涉及到实体匹配、模糊搜索、向量搜索等多种召回和算分方式,最终我们选择了通过 ES 7.X (最终选择 7.10)里的新功能,Dense vector 帮忙一起完成这部分的需求。

2、技术选型2.1 解决方案需求支持向量搜索支持多维度筛选、过滤吞吐速率学习、使用成本运维成本2.2 使用场景设计离线数据准备在离线数据构建完成后,存入该引擎引擎对数据中各字段进行索引在线数据召回根据 query 理解结果构建的 query 语句进行数据召回对结果进行一定的筛选对结果进行一定的打分排序2.3 数据结构设计

在确定了数据的使用场景我们确定了数据结构中,大致会包含以下一些字段

唯一 id:用以做知识的去重和快速获取实体、属性、取值:用来描述知识的具体内容置信度:用来描述知识的可信度分类 flag:知识主要分类及推荐 category 等向量表示:作为知识相似性、相关性召回、打分的依据ref 信息:用来回溯解析/获取该知识的源信息其他属性:包括生效、删除、修改时间等支持性的通用属性

前端模糊搜索_仿51job前端搜索_ios 数据库模糊搜索

2.4 解决方案对比

为了能支持上述的使用需求,我们对比了包括 ES、Faiss 等多种解决方案。其中,Faiss 和 SPTAG 只是核心算法库,需要进行二次开发包装成服务;Milvus 的 1.x 版本中只能存储 id 和 向量,不能完整的满足我们的使用需求;基于集群稳定性和可维护性等考虑,相对于后置插件的部署,我们更倾向于使用 ES 的原生功能,所以选择 ES 的原生向量搜索功能作为我们的最终选择。

对比参考:

种类实现语言客户端支持多条件召回学习成本引入成本运维成本分布式性能社区备注ElasticsearchJavaJava/Pythonyes低低中yes中活跃原生功能FaissPythonPythonno中高高no高一般需要二次开发MilvusPython + GoLangPython/Java/GoLangno中中中no高一般1.x 功能不全OpenDistro Elasticsearch KNNJava + C++Java/Pythonyes中中中yes中一般内置插件SPTAGC++Python + C#no高中中no高一般需要二次开发

3、数据流转流程3.1 离线数据处理部分从多数据源采集数据数据清洗及预处理通过算法引擎提取知识通过算法引擎将知识转换为向量将知识的基础信息连同向量数据存入 ES3.2 在线数据召回部分从前端获取搜索条件通过 query 理解模块进行检索条件解析从 ES 中进行搜索对结果进行分数调整返回前端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 将知识条目转化成向量此处 模型A 模型B 为自研模型,运用了包括知识密度计算等算法以及 bert tersonflow 等框架将原文、知识条目等核心内容插入数据库将核心知识内容、向量等组装成检索单元插入 ES专家团队会针对数据库中的知识条目进行审核、修改和迭代算法团队会根据知识条目的更新以及其他的标注对数据链路中的模型进行迭代,对在线知识库进行更新前端收到请求之后调用 query 理解 组件进行分析剔除无效内容之后,找出 query 里的分类信息等意图之后,构建用来召回的向量和相关的筛选条件通过组合出来的 ES 的 query 条件对知识库进行筛选,并配合置信度等对结果进行调整对召回结果进行不同策略的分数调整和排序,最后输出给前端4.3 示例 query

POST 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年+工作经验,毕业于复旦大学。

博客:

Github:

———END———
限 时 特 惠:本站每日持续更新海量各大内部创业教程,一年会员只需128元,全站资源免费下载点击查看详情
站 长 微 信:jiumai99

滚动至顶部