通过Function Score Query优化Elasticsearch搜索结果

2023-11-03

在使用Elasticsearch进行全文搜索时,搜索结果默认会以文档的相关度进行排序,如果想要改变默认的排序规则,也可以通过sort指定一个或多个排序字段。

但是使用sort排序过于绝对,它会直接忽略掉文档本身的相关度(根本不会去计算)。在很多时候这样做的效果并不好,这时候就需要对多个字段进行综合评估,得出一个最终的排序。

function_score

在Elasticsearch中function_score是用于处理文档分值的DSL,它会在查询结束后对每一个匹配的文档进行一系列的重打分操作,最后以生成的最终分数进行排序。它提供了几种默认的计算分值的函数:

  • weight:设置权重
  • field_value_factor:将某个字段的值进行计算得出分数。
  • random_score:随机得到0到1分数
  • 衰减函数:同样以某个字段的值为标准,距离某个值越近得分越高
  • script_score:通过自定义脚本计算分值

它还有一个属性boost_mode可以指定计算后的分数与原始的_score如何合并,有以下选项:

  • multiply:将结果乘以_score
  • sum:将结果加上_score
  • min:取结果与_score的较小值
  • max:取结果与_score的较大值
  • replace:使结果替换掉_score

接下来本文将详细介绍这些函数的用法,以及它们的使用场景。

weight

weight的用法最为简单,只需要设置一个数字作为权重,文档的分数就会乘以该权重。

他最大的用途应该就是和过滤器一起使用了,因为过滤器只会筛选出符合标准的文档,而不会去详细的计算每个文档的具体得分,所以只要满足条件的文档的分数都是1,而weight可以将其更换为你想要的数值。

field_value_factor

field_value_factor的目的是通过文档中某个字段的值计算出一个分数,它有以下属性:

  • field:指定字段名

  • factor:对字段值进行预处理,乘以指定的数值(默认为1)

  • modifier将字段值进行加工,有以下的几个选项:

    • none:不处理
    • log:计算对数
    • log1p:先将字段值+1,再计算对数
    • log2p:先将字段值+2,再计算对数
    • ln:计算自然对数
    • ln1p:先将字段值+1,再计算自然对数
    • ln2p:先将字段值+2,再计算自然对数
    • square:计算平方
    • sqrt:计算平方根
    • reciprocal:计算倒数

举一个简单的例子,假设有一个商品索引,搜索时希望在相关度排序的基础上,销量(sales)更高的商品能排在靠前的位置,那么这条查询DSL可以是这样的:

{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "雨伞"
        }
      },
      "field_value_factor": {
        "field":    "sales",
        "modifier": "log1p",
        "factor":   0.1
      },
      "boost_mode": "sum"
    }
  }
}

这条查询会将标题中带有雨伞的商品检索出来,然后对这些文档计算一个与库存相关的分数,并与之前相关度的分数相加,对应的公式为:

_score = _score + log(1 + 0.1 * sales)

random_score

这个函数的使用相当简单,只需要调用一下就可以返回一个0到1的分数。

它有一个非常有用的特性是可以通过seed属性设置一个随机种子,该函数保证在随机种子相同时返回值也相同,这点使得它可以轻松地实现对于用户的个性化推荐。

衰减函数

衰减函数(Decay Function)提供了一个更为复杂的公式,它描述了这样一种情况:对于一个字段,它有一个理想的值,而字段实际的值越偏离这个理想值(无论是增大还是减小),就越不符合期望。这个函数可以很好的应用于数值、日期和地理位置类型,由以下属性组成:

  • 原点(origin):该字段最理想的值,这个值可以得到满分(1.0)
  • 偏移量(offset):与原点相差在偏移量之内的值也可以得到满分
  • 衰减规模(scale):当值超出了原点到偏移量这段范围,它所得的分数就开始进行衰减了,衰减规模决定了这个分数衰减速度的快慢
  • 衰减值(decay):该字段可以被接受的值(默认为0.5),相当于一个分界点,具体的效果与衰减的模式有关

例如我们想要买一样东西:

  • 它的理想价格是50元,这个值为原点
  • 但是我们不可能非50元就不买,而是会划定一个可接受的价格范围,例如45-55元,±5就为偏移量
  • 当价格超出了可接受的范围,就会让人觉得越来越不值。如果价格是70元,评价可能是不太想买,而如果价格是200元,评价则会是不可能会买,这就是由衰减规模和衰减值所组成的一条衰减曲线

或者如果我们想租一套房:

  • 它的理想位置是公司附近
  • 如果离公司在5km以内,是我们可以接受的范围,在这个范围内我们不去考虑距离,而是更偏向于其他信息
  • 当距离超过5km时,我们对这套房的评价就越来越低了,直到超出了某个范围就再也不会考虑了

衰减函数还可以指定三种不同的模式:线性函数(linear)、以e为底的指数函数(Exp)和高斯函数(gauss),它们拥有不同的衰减曲线:

衰减曲线

将上面提到的租房用DSL表示就是:

{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "公寓"
        }
      },
      "gauss": {
        "location": {
          "origin": { "lat": 40, "lon": 116 },
          "offset": "5km",
          "scale":  "10km"
           }
         },
         "boost_mode": "sum"
    }
  }
}

我们希望租房的位置在40, 116坐标附近,5km以内是满意的距离,15km以内是可以接受的距离。

script_score

虽然强大的field_value_factor和衰减函数已经可以解决大部分问题了,但是也可以看出它们还有一定的局限性:

  1. 这两种方式都只能针对一个字段计算分值
  2. 这两种方式应用的字段类型有限,field_value_factor一般只用于数字类型,而衰减函数一般只用于数字、位置和时间类型

这时候就需要script_score了,它支持我们自己编写一个脚本运行,在该脚本中我们可以拿到当前文档的所有字段信息,并且只需要将计算的分数作为返回值传回Elasticsearch即可。

注:使用脚本需要首先在配置文件中打开相关功能:

script.groovy.sandbox.enabled: true
script.inline: on
script.indexed: on
script.search: on
script.engine.groovy.inline.aggs: on

举一个之前做不到的例子,假如我们有一个位置索引,它有一个分类(category)属性,该属性是字符串枚举类型,例如商场、电影院或者餐厅等。现在由于我们有一个电影相关的活动,所以需要将电影院在搜索列表中的排位相对靠前。

之前的两种方式都无法给字符串打分,但是如果我们自己写脚本的话却很简单,使用Groovy(Elasticsearch的默认脚本语言)也就是一行的事:

return doc['category'].value == '电影院' ? 1.1 : 1.0

接下来只要将这个脚本配置到查询语句中就可以了:

{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "name": "天安门"
        }
      },
      "script_score": {
        "script": "return doc['category'].value == '电影院' ? 1.1 : 1.0"
      }
    }
  }
}

或是将脚本放在elasticsearch/config/scripts下,然后在查询语句中引用它:

category-score.groovy:

return doc['category'].value == '电影院' ? 1.1 : 1.0
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "name": "天安门"
        }
      },
      "script_score": {
        "script": {
         "file": "category-score"
        }
      }
    }
  }
}

script中还可以通过params属性向脚本传值,所以为了解除耦合,上面的DSL还能接着改写为:

category-score.groovy:

return doc['category'].value == recommend_category ? 1.1 : 1.0
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "name": "天安门"
        }
      },
      "script_score": {
        "script": {
         "file": "category-score",
         "params": {
            "recommend_category": "电影院"
         }
        }
      }
    }
  }
}

这样就可以在不更改大部分查询语句和脚本的基础上动态修改推荐的位置类别了。

同时使用多个函数

上面的例子都只是调用某一个函数并与查询得到的_score进行合并处理,而在实际应用中肯定会出现在多个点上计算分值并合并,虽然脚本也许可以解决这个问题,但是应该没人愿意维护一个复杂的脚本吧。这时候通过多个函数将每个分值都计算出在合并才是更好的选择。

在function_score中可以使用functions属性指定多个函数。它是一个数组,所以原有函数不需要发生改动。同时还可以通过score_mode指定各个函数分值之间的合并处理,值跟最开始提到的boost_mode相同。下面举两个例子介绍一些多个函数混用的场景。

第一个例子是类似于大众点评的餐厅应用。该应用希望向用户推荐一些不错的餐馆,特征是:范围要在当前位置的5km以内,有停车位是最重要的,有WIFI更好,餐厅的评分(1分到5分)越高越好,并且对不同用户最好展示不同的结果以增加随机性。

那么它的查询语句应该是这样的:

{
  "query": {
    "function_score": {
      "filter": {
        "geo_distance": {
          "distance": "5km",
          "location": {
            "lat": $lat,
            "lon": $lng
          }
        }
      },
      "functions": [
        {
          "filter": {
            "term": {
              "features": "wifi"
            }
          },
          "weight": 1
        },
        {
          "filter": {
            "term": {
              "features": "停车位"
            }
          },
          "weight": 2
        },
        {
            "field_value_factor": {
               "field": "score",
               "factor": 1.2
             }
        },
        {
          "random_score": {
            "seed": "$id"
          }
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}

注:其中所有以$开头的都是变量。

这样一个饭馆的最高得分应该是2分(有停车位)+ 1分(有wifi)+ 6分(评分5分 * 1.2)+ 1分(随机评分)。

另一个例子是类似于新浪微博的社交网站。现在要优化搜索功能,使其以文本相关度排序为主,但是越新的微博会排在相对靠前的位置,点赞(忽略相同计算方式的转发和评论)数较高的微博也会排在较前面。如果这篇微博购买了推广并且是创建不到24小时(同时满足),它的位置会非常靠前。

{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "content": "$text"
        }
      },
      "functions": [
        {
          "gauss": {
            "createDate": {
              "origin": "$now",
              "scale": "6d",
              "offset": "1d"
            }
          }
        },
        {
          "field_value_factor": {
            "field": "like_count",
            "modifier": "log1p",
            "factor": 0.1
          }
        },
        {
          "script_score": {
            "script": "return doc['is_recommend'].value && doc['create_date'] > time ? 1.5 : 1.0",
            params: {
                "time": $time
            }
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

它的公式为:

_score * gauss(create_date, $now, "1d", "6d") * log(1 + 0.1 * like_count) * is_recommend ? 1.5 : 1.0
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

通过Function Score Query优化Elasticsearch搜索结果 的相关文章

随机推荐

  • AI软件记录

    1 AI裁剪图像 步骤1 裁剪图像 参考链接ai如何裁剪图片 百度知道 baidu com 步骤2 经过步骤1导出的图片有白框 去掉白框 只留需要的部分 参考链接 130条消息 AI中去掉剪切蒙版中的多余部分 ai剪切蒙版如何去掉多余部分
  • 攻防世界The_Maya_Society

    The Maya Society 这道题目有三个附件 一个素材 一个html网页 还有一个ELF文件 这里刚开始猜测为html网页逆向 但是网页逆向一般是要给一个js文件 该附件中是没有js文件的 所以应该不是网页逆向 那么猜测应该是附件中
  • vite vue3项目打包部署空白页面问题的处理

    问题 vite vue3项目打包部署上线后 发现是空白页面问题的处理 解决方法 1 在我们vite config js文件中检查是否有路径的指向 2 查看我们的路由模式 将路由模式修改为createWebHashHistory 总结 vit
  • 线上Elastcisearch遇到的问题 org.elasticsearch.transport.ReceiveTimeoutTransportException

    记录 本着使用ES能够快速方便的获取数据 线下搜索模块使用了ES 结果一直报错 org elasticsearch transport ReceiveTimeoutTransportException 10 2 2 121 9200 clu
  • 需求管理

    需求管理 Requirement management 是完整管理模式中的一环 同其他特性诸如完整性 一致性等不可分割 彼此相关而成一体 一套需求管理应当是已知系统需求的完整体现 每部分解决方案都是对总体需求一定比例的满足 甚至是充分满足
  • Redis——Redis介绍

    一 概述 Redis Remote Dictionary Server 即远程字典服务器 是开源免费的 用C语言编写的 高性能的 key value 分布式内存数据库 是一个遵守BSD协议 基于内存运行并支持持久化的NoSQL数据库 是当前
  • Ubuntu18.04安装PCL保姆级教程

    系统环境 Ubuntu18 04 6 LTS 1 安装依赖包 sudo apt get update sudo apt get install git build essential linux libc dev sudo apt get
  • 【Unity小帮手】VuforiaAR解决虚拟按键IVirtuaButtonEventHandler停用问题

    在最新的版本中 已经停用了IVirtuaButtonEventHandler 并且ReisterEventHandler this 使用方法发生了改变 1 修改后主要取消了继承IVirtuaButtonEventHandler类 2 修改R
  • fatal: You are not currently on a branch.To push the history leading to the current (detached HEAD)

    这个错误消息表示你当前处于 detached HEAD 状态 意味着你没有在任何分支上 这可能是由于你使用了git checkout命令切换到了一个特定的提交记录 而不是一个分支 要解决这个问题 你需要创建一个新的分支并将其推送到远程仓库
  • Mysql等保2.0测评

    Mysql等保2 0测评 后续会根据工作中的具体项目要求进行修改 一 身份鉴别 a 应对登录的用户进行身份标识和鉴别 身份标识具有唯一性 身份鉴别信息具有复杂度要求并定期更换 1 登录mysql查看是否使用了口令和密码的组合鉴别身份 mys
  • smop Matlab转成Python

    最近老板有一堆 m文件要我转成python文件 因为我们实验室不是每个人都装了matlab 但是这么多文件 自己写得猴年马月去 秉承能用程序就绝不动手的原则 我去GitHub上找到了smop小工具 这个是GitHub的链接 简述一下安装过程
  • 六、二手房数据分析

    六 二手房数据分析 6 1 背景介绍 6 1 1 实验背景 随着房地产市场发展 房价越来越高 为了的到影响房价的增长因素 现在从数据角度出发 分析以下左右房价的因素 数据介绍 CATE 城区 bedrooms 卧室数量 halls 客厅 A
  • c语言嵌入式web服务器,用C语言实现的简单Web服务器(Linux

    file http session c include include include include include include include include include include include include http
  • IT伦理与道德

    1 个人隐私问题 个人隐私包括传统的个人隐私和现代个人数据 传统的个人隐私有姓名 出生年月 身份证编号 婚姻家庭 教育等 现代个人数据有用户名和密码 IP地址等 合理合法的隐私应受到保护 在计算机时代 隐私极易受到侵害 这最直接的影响就是公
  • LVS负载均衡服务器搭建

    LVS简介 现在LVS已经是Linux标准内核的一部分 在Linux2 4内核以前 使用LVS时必须重新编译内核以支持LVS功能模块 但是从Linux2 4内核心之后 已经完全内置了LVS的各个功能模块 无需给内核打任何补丁 可以直接使用L
  • 异步复位信号的 recovery和removal

    简而言之 DFF的复位置位信号不要在clk的跳变沿附近变化 而是要远离clk沿 一般逻辑对此时序不用关心 比如很多模块的操作流程是复位完了 才开启模块时钟 再启动模块工作 这种流程可以保证不会出现recovery和removal的问题 因为
  • IO流总结

    1 什么是IO I Input O Output 通过IO可以完成硬盘文件的读和写 Java中所有的流都在java io 下 2 IO流的分类 有多种分类方式 输入流 输出流 字节流 字符流 1 一种方式是按照流的方向进行分类 以内存作为参
  • 【C++】空间配置器

    目录 一 空间配置器概念 二 为什么需要空间配置器 三 SGI STL空间配置器实现原理 3 1 一级空间配置器 3 2 二级空间配置器 3 2 1 内存池 3 2 2 SGI STL中二级空间配置器设计 3 2 3 SGI STL二级空间
  • spyder的使用(python编辑器)

    spyder是Anaconda种自带的一种python编辑器 这个编辑器里面保存的是py文件 spyder 创建工程 运行 1 运行整个脚本文件 2 运行当前代码块 3 运行当前代码块 并跳至下一个 4 运行当前命令行 或选中的命令行 5
  • 通过Function Score Query优化Elasticsearch搜索结果

    在使用Elasticsearch进行全文搜索时 搜索结果默认会以文档的相关度进行排序 如果想要改变默认的排序规则 也可以通过sort指定一个或多个排序字段 但是使用sort排序过于绝对 它会直接忽略掉文档本身的相关度 根本不会去计算 在很多