You need to enable JavaScript to run this app.
导航

基于 ES 的排序学习实践

最近更新时间2024.04.07 19:15:43

首次发布时间2023.10.16 11:59:39

本文基于火山引擎云搜索服务 ES,以及开源 Metarank 排序工具,实现召回、排序、重排三个阶段的排序学习。当用户输入查询时,能够返回个性化的搜推结果。

应用场景

排序学习(Learning to Rank, LTR)是一种机器学习技术,其应用场景非常广泛。

  • 电商推荐领域,可以帮助电商平台对用户的购买历史、搜索记录、浏览行为等数据进行分析和建模。
  • 广告投放领域,可以帮助搜索引擎对用户的搜索关键词进行分析建模;可以提供最精准和最有效的广告投放方案。
  • 金融风控领域,可以帮助金融机构分析客户的信用评级和欺诈风险,提高风控能力和业务效率。

背景介绍

火山引擎云搜索服务的搜索过程一般包含召回+排序两个阶段。通过用户输入的文本作为关键词,使用 BM25 打分算法,遍历数据库挑选出分数最高的文档并进行排序后返回查询结果。由于 BM25 算法模型考虑的主要是文本的词频、逆文档频率等因素,因此搜索结果的排序仅取决于与所检索文本的相关性。
在大部分场景使用召回+排序便可满足需求,但是有些应用场景用户则想要实现个性化推荐效果。 为了实现个性化推荐,需要在已有召回、排序的基础上,引入重排阶段。相较于前两个阶段,重排阶段考虑的因素则偏向于用户行为,通过用户点击、收藏、购买等反馈特征,引入机器学习算法,针对特征与反馈自动学习并调整参数,预估用户对于返回结果的偏好,最终实现个性化搜推结合的效果。这个排序训练过程,也被称为排序学习(Learning to Rank, LTR)。
在火山引擎云搜索服务 ES 中,为了实现重排阶段,目前支持使用内置插件和开源工具两种方式。

  • 使用内置插件:将重排阶段以插件的形式安装到 ES 实例中,比如 elasticsearch-learning-to-rank 插件。
    用户输入查询,返回搜推结果。整个流程对业务保持透明,业务只需与搜索服务完成交互。
    图片
  • 使用开源工具:正常使用 ES 实例的召回+排序阶段,重排阶段则使用开源工具实现,比如 metarank 工具。
    用户输入查询,经过 ES 实例的召回+排序阶段得到中间结果;再将中间结果作为输入,与 LTR 模型工具进行交互,最后返回搜推结果。整个流程需要业务侧自行处理中间结果,完成与搜索引擎服务和 LTR 模型工具的交互,灵活性更高。
    本文主要介绍的是使用开源工具实现排序学习的流程。
    图片

步骤一:准备环境

  1. 登录云搜索服务控制台,然后创建一个 7.10 版本的 ES 实例。
    图片
  2. 安装 Python Client 依赖。
    pip install -U elasticsearch7==7.10.1 # ES数据库相关
    pip install -U pandas #分析splash的csv
    

步骤二:准备数据集

本文选择使用开源 Metarank 排序工具文档中推荐的 RankLens 数据集,您可以下载 dataset/metadata.jsonl.gz 原始数据集。
经过解压后可得到约 2500 条数据,每条数据包含电影海报、演员、评分等信息。示例信息如下:

{
    ...
    "description": "When a rare phenomenon gives police officer John Sullivan the chance to speak to his father, 30 years in the past, he takes the opportunity to prevent his dad's tragic death.  After his actions inadvertently give rise to a series of brutal murders he and his father must find a way to fix the consequences of altering time.",
    "director": {
        "gender": 2,
        "id": 17812,
        "name": "Gregory Hoblit",
        "popularity": 1.62
    },
    "id": 3510,
    "overview": "When a rare phenomenon gives police officer John Sullivan the chance to speak to his father, 30 years in the past, he takes the opportunity to prevent his dad's tragic death.  After his actions inadvertently give rise to a series of brutal murders he and his father must find a way to fix the consequences of altering time.",
    "poster": "https://image.tmdb.org/t/p/original/eu3Hrjj271dnBdNAF0HqfmwWASt.jpg",
    "releaseDate": "2000-04-28",
    "tags": [
        "time travel",
        "father-son relationship",
        "alternate reality",
        "father son relationship",
        "supernatural"
    ],
    "title": "Frequency",
    "tmdbId": 10559,
    "tmdbPopularity": 10.95,
    "tmdbVoteAverage": 7.2,
    "tmdbVoteCount": 1254,
    "topActors": [
        {
            "gender": 1,
            "id": 31167,
            "name": "Elizabeth Mitchell",
            "popularity": 8.646
        },
        ...
    ]
}

步骤三:连接 ES 实例

  1. 在 ES 实例详情页面,获取实例访问地址。
    如果需要在公网环境访问 ES 实例,请提前为实例开启公网访问。相关文档,请参见开启实例公网访问
    图片
  2. 连接实例。
    # 连接 ES 实例。如果遗忘实例访问用户(admin)的密码,可以选择重置密码。
    cloudSearch = CloudSearch("https://{user}:{password}@{ES_URL}", 
                        verify_certs=False, 
                        ssl_show_warn=False)
    

步骤四:连接 Metarank 服务

在本地启动 Metarank 服务。

  • 数据集参数(--data)指定转化后的数据集,包括数据的元信息及用户点击率信息。
  • 配置文件参数(--config)指定模型配置。

参数配置及文件下载,可参见Metarank Quickstart

java -jar metarank-0.7.1.jar standalone --data events.jsonl.gz  --config events-config.yml

步骤五:写入数据集

将 RankLens 数据集写入 ES 实例。

import json

path = '${下载的数据集所在路径}'
with open(path, 'r') as f:
  bulk_docs = []
  n = 0
  for line in f.readlines():
    doc = json.loads(line.rstrip())
    if 'title' in doc:
        n += 1
        bulk_docs.append({"index": {"_id": doc['id']}})
        bulk_docs.append(doc)
        
        ## 每次批量写入50条数据
        if n % 50 == 0:        
          resp = cloudSearch.bulk(bulk_docs, index='events2')
          bulk_docs = []  

结果验证

  1. 文本查询和 Metarank 重排。

    @app.route('/search', methods=['GET'])
    def search():
        return innerSearch()
    
    def innerSearch():
        # 获取参数
        query = request.args.get('query')
        method = request.args.get('retrieval')
        n = int(request.args.get('size'))
        rank = request.args.get('rank')
        start = time.time()
        
        # 文本查询
        docs = retrieve(method, query, n) 
        done1 = time.time()
        if len(docs['hits']['hits']) == 0:
            return render_template('search.html', help=False, query=query, method=method, rank=rank, size=n, took={"search": 1000*(done1-start), "rank": 0, "total": 1000*(done1-start)})
    
        # Metarank重排
        sorted = rerank(rank, query, docs['hits']['hits'])
        done2 = time.time()
        return render_template('search.html', help=False, query=query, docs=sorted, method=method, rank=rank, size=n, took={"search": 1000*(done1-start), "rank": 1000*(done2-done1), "total": 1000*(done2-start)})
    
  2. 用户点击反馈。将用户的偏好反馈写入 Metarank 。

    @app.route('/feedback', methods=['GET'])
    def feedback():
        item = request.args.get('item')
        
        # 点击反馈
        interaction = metarank.feedbackInteraction(item)
    
        return innerSearch()
    

视频演示