ES3_6算分和排序

Doc Values / fielddata - 正排索引

在搜索的时候,我们能通过搜索关键词快速得到结果集。当排序的时候,我们需要倒排索引里面某个字段值的集合,此时倒排索引无法发挥作用。换句话说,我们需要 转置 倒排索引。转置 结构在其他系统中经常被称作 列存储 。实质上,它将所有单字段的值存储在单数据列中,这使得对其进行操作是十分高效的,例如排序。
ES有2种方法实现:

  • Fielddata(可以存储Text类型)
  • Doc Values(列式存储,对Text类型无效)
Doc Values Field data
何时创建 索引时,和倒排索引一起创建 搜索时动态创建
创建位置 磁盘文件 JVM Heap
优点 避免大量内存占用 索引速度快,不占用额外的磁盘空间
缺点 降低索引速度,占用额外磁盘空间 文档过多时,动态创建开销大,占用过多JVM Heap
缺省值 ES 2.x 之后 ES 1.x 及之前

当 working set 远小于节点的可用内存,系统会自动将所有的文档值保存在内存中,使得其读写十分高速; 当其远大于可用内存,操作系统会自动把 Doc Values 加载到系统的页缓存中,从而避免了 jvm 堆内存溢出异常。

关闭 Doc Values

Doc Value默认是启用的,可以通过Mapping设置关闭

1
2
3
4
5
6
7
8
9
PUT test_keyword/_mapping
{
"properties": {
"user_name": {
"type": "keyword",
"doc_values": false
}
}
}
  • 关闭有什么好处?增加索引速度、减少磁盘占用空间
  • 关闭会有什么问题?如果后续需要重新打开,则需要重建索引
  • 什么时候需要关闭?明确不需要做排序及聚合分析

排序与相关性

  • sort将目标字段转换为排序所需的格式,date 字段的值表示为自 epoch (January 1, 1970 00:00:00 UTC)以来的毫秒数,通过 sort 字段的值进行返回。
  • 如果字段是一个数组(多值),可以使用 mode 指定其中 min 、 max 、 avg 或是 sum 进行排序;
  • 评分的计算方式取决于 查询类型 不同的查询语句用于不同的目的: fuzzy 查询会计算与关键词的拼写相似程度,terms 查询会计算 找到的内容与关键词组成部分匹配的百分比,但是通常我们说的 relevance 是我们用来计算全文本字段的值相对于全文本检索词相似程度的算法,Elasticsearch 的相似度算法 被定义为检索词频率/反向文档频率(TF/IDF),包括以下内容:
    • 检索词频率
        检索词在该字段出现的频率?出现频率越高,相关性也越高。 字段中出现过 5 次要比只出现过 1 次的相关性高。
    • 反向文档频率
        每个检索词在索引中出现的频率?频率越高,相关性越低。检索词出现在多数文档中会比出现在少数文档中的权重更低。
    • 字段长度准则
        字段的长度是多少?长度越长,相关性越低。 检索词出现在一个短的 title 要比同样的词出现在一个长的 content 字段权重更大。
  • 单个查询可以联合使用 TF/IDF 和其他方式,比如短语查询中检索词的距离或模糊查询里的检索词相似度。如果多条查询子句被合并为一条复合查询语句 ,比如 bool 查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。
  • 字符串索引后(analusis)会有变化,排序时希望使用原字段(not_analyzed)进行排序,我们想要对同一个字段索引两次,而不是在_source 中保存两份字符串字段,这可以通过为字段添加一个 not_analyzed 子字段来实现:主字段用于搜索、子字段用于排序:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    "tweet": { 
    "type": "string",
    "analyzer": "english",
    "fields": {
    "raw": {
    "type": "string",
    "index": "not_analyzed"
    }
    }
    }
    "sort" : {
    "date": {
    "order": "desc",
    "mode": "min"
    }
    }
  • 默认情况下,返回结果是按相关性倒序排列的
  • 可以在查询参数中加入 explain 参数解释排序结果
    1
    2
    3
    4
    GET /_search?explain 
    {
    "query" : { "match" : { "tweet" : "honeymoon" }}
    }

分页

几种分页方式及应用场景

  • Regular
    平时查询ES只会返回头部的10条数据,一般用于实时获取顶部的部分文档,例如查询最新的订单。
  • Scroll
    需要全部文档时,例如导出全部数据
  • Pagination
    from + size的方式
    如果需要深度分页,则选用Search After

from 和 size(分页)、Search After

ES中的分页是从每个分片上获取from + size条数据,然后协调节点聚合所有结果,再选取前from + size条数据。
因为是from + size,所以from特别大时会有深分页问题
解决办法是Search After

1
2
3
4
5
6
7
8
9
10
11
12
POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"search_after": [13, "idididid"],
"sort": [
{"age": "desc"},
{"_id": "asc"}
]
}

缺点是:

  • 不支持指定页数(From)
  • 只能往下翻

需要指定搜索sort:

  • 需要保证值是唯一的,可以加入_id保证唯一性
  • 每次查询使用上一次查询得到的最后一个文档的sort值进行查询(即上边的13和”idididid”)

Search After会通过唯一排序值定位,将每次要处理的文档数都控制在size个。

游标查询 Scroll

scroll 查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。
游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor
游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图 一样。
深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段 _doc 来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。
启用游标查询可以通过在查询的时候设置参数 scroll 的值为我们期望的游标查询的过期时间。 游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。 这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。 设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。

1
2
3
4
5
6
GET /old_index/_search?scroll=1m // 保持游标查询窗口一分钟。
{
"query": { "match_all": {}},
"sort" : ["_doc"], // 关键字 _doc 是最有效的排序顺序。
"size": 1000
}

这个查询的返回结果包括一个字段 _scroll_id, 它是一个 base64 编码的长字符串。现在我们能传递字段 _scroll_id_search/scroll 查询接口获取下一批结果:

1
2
3
4
5
GET /_search/scroll
{
"scroll": "1m", // 注意再次设置游标查询过期时间为一分钟。
"scroll_id" : "cXVlcnlUaGVuRmV0Y2g7NTsxMDk5NDpkUmpiR2FjOFNhNnlCM1ZDMWpWYnRROzEwOTk1OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MTA5OTM6ZFJqYkdhYzhTYTZ5QjNWQzFqVmJ0UTsxMTE5MDpBVUtwN2lxc1FLZV8yRGVjWlI2QUVBOzEwOTk2OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MDs="
}

这个游标查询返回的下一批结果。 尽管我们指定字段 size 的值为 1000,我们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size 作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards
当没有更多结果返回的时候,我们就处理完所有匹配的文档了。

缺点:

  • Scroll会创建一个快照,如果查询期间有新的数据写入以后,无法被查到

算分优化 - Function Score Query

算分函数

ES提供了几种默认的计算分值的函数:

weight

设置权重

field_value_factor

使用某个数值修改_score的值,比如乘以某个系数
原算分乘以某个字段得到最终结果,比如下面就是乘以原文档中的count字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET doc/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "a"
}
},
"field_value_factor": {
"field": "count"
}
}
}
}

还可以根据某个函数来计算评分,比如如下命令新算分 = 老算分 * log(1 + factor * count)

1
2
3
4
5
6
7
8
9
10
11
12
13
GET doc/_search
{
"query": {
"function_score": {
...
"field_value_factor": {
"field": "count",
"modifier": "log1p",
"factor": 0.1
}
}
}
}

random_score

为每一个用户使用一个不同的,随机算分结果
使用场景:让每个用户能看到不同的随机排名,但是也希望同一个用户访问时,结果的相对顺序保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET doc/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "a"
}
},
"random_score": {
"seed": 911119
}
}
}
}

衰减函数

以某个字段的值为标准,距离某个值越近,得分越高

script_score

自定义脚本完全控制所需逻辑
elasticsearch painless脚本评分
Elasticsearch中使用painless实现评分

boost

  • boost mode
    multiply:默认方式,算分与函数值的乘积
    sum:算分与函数的和
    min / max:算分与函数取最小/最大值
    replace:使用函数值取代算分
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    GET doc/_search
    {
    "query": {
    "function_score": {
    "query": {
    "match": {
    "title": "a"
    }
    },
    "field_value_factor": {
    "field": "count"
    },
    "boost_mode": "sum"
    }
    }
    }
  • max boost
    将算分控制在一个最大值

算分原理

计算目标文档和origin之间的距离

  • NumericFieldDataScoreFunction#distance
    数值距离=max(0, |doubleValue - origin| - offset)
  • GeoFieldDataScoreFunction#distance:
    地域距离

衰减函数

  • GaussDecayFunction
    高斯衰减=e^(distance^2 / 2scale)