死敌wen 稿

1、背景

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

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

2、技术选型

2.1 解决方案需求

  1. 支持向量搜索
  2. 支持多维度筛选、过滤
  3. 吞吐速率
  4. 学习、使用成本
  5. 运维成本

2.2 使用场景设计

  1. 离线数据准备
    1. 在离线数据构建完成后,存入该引擎
    2. 引擎对数据中各字段进行索引
  2. 在线数据召回
    1. 根据 query 理解结果构建的 query 语句进行数据召回
    2. 对结果进行一定的筛选
    3. 对结果进行一定的打分排序

2.3 数据结构设计

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

  1. 唯一 id:用以做知识的去重和快速获取

  2. 实体、属性、取值:用来描述知识的具体内容

  3. 置信度:用来描述知识的可信度

  4. 分类 flag:知识主要分类及推荐 category 等

  5. 向量表示:作为知识相似性、相关性召回、打分的依据

  6. ref 信息:用来回溯解析/获取该知识的源信息

  7. 其他属性:包括生效、删除、修改时间等支持性的通用属性

2.4 解决方案对比

为了能支持上述的使用需求,我们对比了包括 ESFaiss 等多种解决方案。其中, FaissSPTAG 只是核心算法库,需要进行二次开发包装成服务; Milvus1.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 离线数据处理部分

  1. 从多数据源采集数据
  2. 数据清洗及预处理
  3. 通过算法引擎提取知识
  4. 通过算法引擎将知识转换为向量
  5. 将知识的基础信息连同向量数据存入 ES

3.2 在线数据召回部分

  1. 从前端获取搜索条件
  2. 通过 query 理解模块进行检索条件解析
  3. ES 中进行搜索
  4. 对结果进行分数调整
  5. 返回前端

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"
                ]
            }
        }
    }
}
  • 说明:
  1. 由于向量数据较大,所以倾向于将整个索引都放置在硬件性能更好的节点
  2. 为了支持高性能过滤,将常用的字段预先加载在内存中
  3. 对慢查询开启日志方便后续性能问题的调查
  4. 知识库的重建是离线的,会在更新时进行大量写入,所以对 translog 的提交间隔拉长,加快写入速度
  5. 在实际使用中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🇲🇲ss"
                ]
            },
            "update_timestamp": {
                "type": "date",
                "format": [
                    "strict_date_hour_minute_second",
                    "yyyy-MM-dd HH🇲🇲ss"
                ]
            }
        }
    }
}
  • 说明:
  1. 除了对知识条目的完整搜索之外,还会需要进行模糊检索,我们使用了自研的 farady 分词器对知识条目的各部分进行了分词处理
  2. 知识库中的知识条目会有一部分进行专家/人工审核和维护,所以会对不同的条目设置不同的置信度
  3. 数据预处理之后会转成 512 位的向量存在这个字段中

4.2 数据流转

  • 离线部分:
  1. 数据采集及清洗

  2. 通过 模型A 从文章中找到知识条目

  3. 通过 模型B 将知识条目转化成向量

    1. 此处 模型A 模型B 为自研模型,运用了包括知识密度计算等算法以及 bert tersonflow 等框架
  4. 将原文、知识条目等核心内容插入数据库

  5. 将核心知识内容、向量等组装成检索单元插入 ES

  6. 专家团队会针对数据库中的知识条目进行审核、修改和迭代

  7. 算法团队会根据知识条目的更新以及其他的标注对数据链路中的模型进行迭代,对在线知识库进行更新

  • 在线部分:
  1. 前端收到请求之后调用 query 理解 组件进行分析
  2. 剔除无效内容之后,找出 query 里的分类信息等意图之后,构建用来召回的向量和相关的筛选条件
  3. 通过组合出来的 ESquery 条件对知识库进行筛选,并配合置信度等对结果进行调整
  4. 对召回结果进行不同策略的分数调整和排序,最后输出给前端

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": [ ... ]
                }
            }
        }
    }
}
  • 说明:
  1. 上述 query 的条件、参数仅做示意,属于实际线上使用的脱敏、简化版
  2. 计算公式为迭代中某�