ElasticSearch 论坛搜索查询语句

概述

研究论坛搜索如何综合时间和TF/IDF权重。
自定义权重计算的效率问题javascript

数据结构

假设有一个论坛的搜索
字段包括:html

subject:标题
message:内容
dateline:发布时间
tagid:论坛idjava

直接经过注释一个查询语句来直观了解如何使用json来查询数据。

{
//为每一个全文索引字段定义highlight(高亮)格式
  "highlight": {
    "fields": {
      "subject": {},
      "message": {}
    }
  },
//不返回所有数据
  "_source": false,
//只返回subject字段
  "fields": [
    "subject"
  ],
//一个查询语句
  "query": {
//自定义score(分数)
    "function_score": {
      "query": {
//带过滤器的查询须要用filtered  包裹
        "filtered": {
//过滤器部分
          "filter": {
            "term": {
              "tagid": 1
            }
          },
//查询语句部分(全文索引)
          "query": {
//要查的部分有2个,用or链接起来(should 相似or)
            "bool": {
              "should": [
                {
//match来按照全文索引来查
                  "match": {
//查标题
                    "subject": {
//标题关键字
                      "query": "手榴弹",
//权重boost会作个乘法
                      "boost": 5
                    }
                  }
                },
//内容字段权重较低,配置基本相同
                {
                  "match": {
                    "message": {
                      "query": "手榴弹",
                      "boost": 1
                    }
                  }
                }
              ]
            }
          }
        }
      },
//额外的发布时间权重,时间越大,权重越大,也是乘法(默认)
//可是因为log在输入值巨大的状况下(时间戳)y轴增加缓慢,几乎没法影响到score,因此下面这个配置,思想是好的,结果是废的
      "field_value_factor": {
        "field": "dateline",
//log(1 + dateline)
        "modifier": "log1p",
        "factor": 0.1,
        "missing": 1//没有这个字段的处理方式,返回分数1
      }
    }
  }
}

note: 你可能注意到我用了手榴弹一词,由于咱们论坛中几乎不会出现这个词,因此在测试中能够方便测试词频、标题(subject),内容(message)的权重问题,而减小其余用户数据干扰算法

使用groovy语言来控制排序用的score字段

若是你使用比较新版本的ES,好比>=2.0,你可能须要先配置一下服务器以便支持groovy语句
_score是ES经过TF/IDF和其余自定义算法计算获得的一个分数,用来表达和搜索预期的接近程度,值越大越接近理想的结果。经过控制这个值,就能够改变搜索的排序结果。上面的boost是其中一种,经过设置boost,获得 _score = _score * boost的效果,至关于咱们喜欢使用的“权重”。
而上面的field_value_factor的控制方式为:express

_score = oldscore * log(1 + dateline * 0.1)

其实这个语句的目的就是为了让时间大的(靠近如今)的数据排序靠前一点,好比新闻什么的,时间越近也是越好的,然而这个分数在dateline有巨大差别的状况下,只有万分之几的变化,不能知足要求,一个简单的方法就是 1 /(当前时间 - 发布时间),因为这个分数的底数是从0开始算起的,而 f(x) = 1/x 靠近1的部分,数值差别比较大,远离的部分(旧数据,趋于0)。这个公式还有个问题,就是底数多是0,若是你有N台服务器,而服务器之间有必定的时间差,就可能遇到这个问题。
改成下面这样:apache

1 / (当前时间 - 发布时间 > 0 ? 当前时间 - 发布时间 : 1)json

可是若是旧数据时间趋于0也会致使一个新问题就是,基于TF/IDF的分数失效了,也不是咱们想要的,因此简单办法就是给这个分数 + 1
还有一个问题就是当时间差别为2秒的时候,数值已经降低了50%,这也太快了一点,经过一个因数控制一下降低速度服务器

1 / (当前时间 - 发布时间 >= 1000 ? (当前时间 - 发布时间)/ 1000 : 1) + 1数据结构

1000秒的效果
elasticsearch

8小时的效果,基本上在10天后,分值趋近于1,也就是彻底由TF/IDF决定

这样确保告终果在(1,2]之间变化,而分值衰减50%须要2000秒之后才会达到,最近1000秒内的数据分值相同,他们是平等的(除非新闻专题,半小时内发布的东西对于用户来讲,前后的重要程度并不高,若是是论坛更是如此,在论坛中我可能会增长到4-8小时)。

{
  "highlight": {
    "fields": {
      "subject": {},
      "message": {}
    }
  },
  "query": {
    "function_score": {
      "query": {
        "filtered": {
          "filter": {
            "term": {
              "tagid": 1
            }
          },
          "query": {
            "bool": {
              "should": [
                {
                  "match": {
                    "subject": {
                      "query": "手榴弹",
                      "boost": 1,
                      "operator": "or"
                    }
                  }
                },
                {
                  "match": {
                    "message": {
                      "query": "手榴弹",
                      "boost": 1,
                      "operator": "or"
                    }
                  }
                }
              ]
            }
          }
        }
      },
//这里是差别的部分
      "functions": [
        {
          "script_score": {
            "script": "return 1 /( now - doc['dateline'].value > 1000 ? ( now - doc['dateline'].value ) / 1000 : 1);",
            "params": {
              "now": 1448806722
            }
          }
        }
      ]
    }
  }
}

2015年12月03更新:groovy 效率极低

在实际应用的时候,咱们数据量大概是 1400 万条,约12G。
测试机器硬件配置:
Intel(R) Xeon(R) CPU E5506 @ 2.13GHz 4核
内存16G
集群设置:

  1. 单节点
  2. 分片5,备份 0 分词插件为 ik

这个查询跑下来须要3000 ms 以上,关键的问题就是groovy,这个数据不是预先计算好的,而是每次从新计算。
其实除了groovy,ES还支持多种脚本语言。
目前测试下来比较快的是expression ,内置,不须要插件,速度快,只支持数值运算,符合咱们的须要。

//function 部分改成以下,注意:不须要return,这是个算数表达式。不须要分号结尾。
      "functions": [
        {
          "script_score": {
            "lang": "expression",
            "script": "1 /( now - doc['dateline'].value > 259200 ? ( now - doc['dateline'].value ) / 259200 : 1)",
            "params": {
              "now": $time
            }
          }
        }
      ]

对比状况为:

groovy 3000ms 以上
expression 35 - 50 ms 间浮动

  • 速度相差约 60 倍 *

官方说法是expression的速度是能够匹敌原生java语言的插件。可是表达式的各个部分,只能是数字。

参考资料:

有关script_score的官方文档
https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_lucene_expressions_scripts

lucene expression 官方文档
http://lucene.apache.org/core/4_9_0/expressions/index.html?org/apache/lucene/expressions/js/package-summary.html

Es权威指南第二部分,Search in Depth - script-score https://www.elastic.co/guide/en/elasticsearch/guide/current/script-score.html

相关文章
相关标签/搜索