• Elasticsearch:过滤搜索结果 - filter 及 post_filter


    在 Elasticsearch 中,过滤搜索的结果是我们经常要做的事。在我刚开始接触 Elasticsearch,我就了解到有两种可以过滤搜索结果的方法。当时还不是很明白,为什么有的地方用 filter,而有的地方需要使用到 post filter。在今天的文章中,我来用一个鲜活的例子来进行展示。

    总体说来,我们可以使用如下的两个方法来过滤搜索的结果:

    • 使用带有 filter 子句的布尔查询。 搜索请求将布尔过滤器应用于搜索命中和聚合。
    • 使用搜索 API 的 post_filter 参数。 搜索请求仅将 post filters 应用于搜索命中,而不是聚合。 你可以使用 post filters 根据更广泛的结果集计算聚合,然后进一步缩小结果。讲得通俗一点:在已经计算聚合之后,post filter 将应用于搜索请求最后的搜索命中。从这里的描述中,我们可以看出来,post filters 的使用和 aggregation 相关。

    你还可以在 post filter 之后重新对命中进行评分,以提高相关性并重新排序结果。

    Post filter

    当你使用 post_filter 参数过滤搜索结果时,会在计算聚合后过滤搜索命中。 Post filter 对聚合结果没有影响。

    例如,你销售的衬衫具有以下属性:

    1. PUT shirts
    2. {
    3. "mappings": {
    4. "properties": {
    5. "brand": { "type": "keyword"},
    6. "color": { "type": "keyword"},
    7. "model": { "type": "keyword"}
    8. }
    9. }
    10. }

    我们使用如下的命令来摄入 3 个文档:

    1. PUT shirts/_doc/1?refresh
    2. {
    3. "brand": "gucci",
    4. "color": "red",
    5. "model": "slim"
    6. }
    7. PUT shirts/_doc/2?refresh
    8. {
    9. "brand": "polo",
    10. "color": "red",
    11. "model": "large"
    12. }
    13. PUT shirts/_doc/3?refresh
    14. {
    15. "brand": "polo",
    16. "color": "blue",
    17. "model": "medium"
    18. }

    假想你有一个用户,他想买一个 red 的衣服。通常你会使用如下的 bool query

    1. GET shirts/_search?filter_path=**.hits
    2. {
    3. "query": {
    4. "bool": {
    5. "filter": [
    6. {
    7. "term": {
    8. "color": "red"
    9. }
    10. }
    11. ]
    12. }
    13. }
    14. }

    上面显示的结果为:

    1. {
    2. "hits" : {
    3. "hits" : [
    4. {
    5. "_index" : "shirts",
    6. "_id" : "1",
    7. "_score" : 0.0,
    8. "_source" : {
    9. "brand" : "gucci",
    10. "color" : "red",
    11. "model" : "slim"
    12. }
    13. },
    14. {
    15. "_index" : "shirts",
    16. "_id" : "2",
    17. "_score" : 0.0,
    18. "_source" : {
    19. "brand" : "polo",
    20. "color" : "red",
    21. "model" : "large"
    22. }
    23. }
    24. ]
    25. }
    26. }

    显然搜索的结果显示了所有 red 的衣服。但是,你还想使用分面导航来显示用户可以单击的其他选项列表(比如大小尺寸)。 也许你有一个 model 字段,允许用户将搜索结果限制为红色 Gucci T 恤或 Polo 的衣服。这可以通过 terms aggregation 来完成:

    1. GET shirts/_search
    2. {
    3. "query": {
    4. "bool": {
    5. "filter": [
    6. {
    7. "term": {
    8. "color": "red"
    9. }
    10. }
    11. ]
    12. }
    13. },
    14. "aggs": {
    15. "models": {
    16. "terms": {
    17. "field": "model"
    18. }
    19. }
    20. }
    21. }

    在上面,我们通过 terms 聚合来显示各个尺寸(model)的文档数。最多的将排在前面。上面命令显示的结果为:

    1. {
    2. "took" : 0,
    3. "timed_out" : false,
    4. "_shards" : {
    5. "total" : 1,
    6. "successful" : 1,
    7. "skipped" : 0,
    8. "failed" : 0
    9. },
    10. "hits" : {
    11. "total" : {
    12. "value" : 2,
    13. "relation" : "eq"
    14. },
    15. "max_score" : 0.0,
    16. "hits" : [
    17. {
    18. "_index" : "shirts",
    19. "_id" : "1",
    20. "_score" : 0.0,
    21. "_source" : {
    22. "brand" : "gucci",
    23. "color" : "red",
    24. "model" : "slim"
    25. }
    26. },
    27. {
    28. "_index" : "shirts",
    29. "_id" : "2",
    30. "_score" : 0.0,
    31. "_source" : {
    32. "brand" : "polo",
    33. "color" : "red",
    34. "model" : "large"
    35. }
    36. }
    37. ]
    38. },
    39. "aggregations" : {
    40. "models" : {
    41. "doc_count_error_upper_bound" : 0,
    42. "sum_other_doc_count" : 0,
    43. "buckets" : [
    44. {
    45. "key" : "large",
    46. "doc_count" : 1
    47. },
    48. {
    49. "key" : "slim",
    50. "doc_count" : 1
    51. }
    52. ]
    53. }
    54. }
    55. }

    在上面,我们可以看出颜色为 red 的衣服,各个 model 的统计情况:large 及 slim 个一件。显然这个是我们想要的结果。我们注意到的一点是 aggregation 是基于前面的 boolean filter 所过滤后的数据集来进行统计的。其统计结果都是是红色的衣服。

    但也许你还想告诉用户有多少 polo 衬衫可供选择而不是所有的品牌。我们可以使用如下的搜索:

    1. GET shirts/_search
    2. {
    3. "query": {
    4. "bool": {
    5. "filter": [
    6. {
    7. "term": {
    8. "color": "red"
    9. }
    10. }
    11. ]
    12. }
    13. },
    14. "aggs": {
    15. "models": {
    16. "terms": {
    17. "field": "model"
    18. }
    19. }
    20. },
    21. "post_filter": {
    22. "term": {
    23. "brand": "polo"
    24. }
    25. }
    26. }

    在上面,我们使用 filter 把 red 的文档搜索出来,然后使用 terms aggregatiion 来对所有 red 的文档进行 model 的统计。我们接下来使用 post_filter 来对我们的搜索结果再次过滤。在这里需要注意的是:post_filter 的使用不会对 aggs 的结果产生任何的影响。如同上面写的顺序一样,post_filter 是在最后面运行的。上面的命令产生的结果是:

    1. {
    2. "took" : 0,
    3. "timed_out" : false,
    4. "_shards" : {
    5. "total" : 1,
    6. "successful" : 1,
    7. "skipped" : 0,
    8. "failed" : 0
    9. },
    10. "hits" : {
    11. "total" : {
    12. "value" : 1,
    13. "relation" : "eq"
    14. },
    15. "max_score" : 0.0,
    16. "hits" : [
    17. {
    18. "_index" : "shirts",
    19. "_id" : "2",
    20. "_score" : 0.0,
    21. "_source" : {
    22. "brand" : "polo",
    23. "color" : "red",
    24. "model" : "large"
    25. }
    26. }
    27. ]
    28. },
    29. "aggregations" : {
    30. "models" : {
    31. "doc_count_error_upper_bound" : 0,
    32. "sum_other_doc_count" : 0,
    33. "buckets" : [
    34. {
    35. "key" : "large",
    36. "doc_count" : 1
    37. },
    38. {
    39. "key" : "slim",
    40. "doc_count" : 1
    41. }
    42. ]
    43. }
    44. }
    45. }

    如上所示,我们最终得到的搜索结果是 color:red 并且 brand:polo 的搜索结果,但是 aggregations 的结果是针对 color:red 而的出来的。我们可以看到上面的 slim 统计结果是来自 gucci 品牌的而不是 polo。

    更为复杂的查询是这样的:

    1. GET shirts/_search
    2. {
    3. "query": {
    4. "bool": {
    5. "filter": {
    6. "term": { "brand": "polo" }
    7. }
    8. }
    9. },
    10. "aggs": {
    11. "colors": {
    12. "terms": { "field": "color" }
    13. },
    14. "color_red": {
    15. "filter": {
    16. "term": { "color": "red" }
    17. },
    18. "aggs": {
    19. "models": {
    20. "terms": { "field": "model" }
    21. }
    22. }
    23. }
    24. },
    25. "post_filter": {
    26. "term": { "color": "red" }
    27. }
    28. }

    在上面,我们首先使用的 filter 来过滤数据集。只有 brand:polo 的文档才可以进行聚合。aggs 里含有两个 aggregations。一个是按照 colors 来进行的分类,另外一个是先过滤 red 颜色的 polo,然后再按照 model 进行分类。在最后,我们使用 post_fitler 来过滤结果。最终的搜索结果(位于 hits 里)是 brand:polo 并且 color:red:

    1. {
    2. "took" : 0,
    3. "timed_out" : false,
    4. "_shards" : {
    5. "total" : 1,
    6. "successful" : 1,
    7. "skipped" : 0,
    8. "failed" : 0
    9. },
    10. "hits" : {
    11. "total" : {
    12. "value" : 1,
    13. "relation" : "eq"
    14. },
    15. "max_score" : 0.0,
    16. "hits" : [
    17. {
    18. "_index" : "shirts",
    19. "_id" : "2",
    20. "_score" : 0.0,
    21. "_source" : {
    22. "brand" : "polo",
    23. "color" : "red",
    24. "model" : "large"
    25. }
    26. }
    27. ]
    28. },
    29. "aggregations" : {
    30. "color_red" : {
    31. "doc_count" : 1,
    32. "models" : {
    33. "doc_count_error_upper_bound" : 0,
    34. "sum_other_doc_count" : 0,
    35. "buckets" : [
    36. {
    37. "key" : "large",
    38. "doc_count" : 1
    39. }
    40. ]
    41. }
    42. },
    43. "colors" : {
    44. "doc_count_error_upper_bound" : 0,
    45. "sum_other_doc_count" : 0,
    46. "buckets" : [
    47. {
    48. "key" : "blue",
    49. "doc_count" : 1
    50. },
    51. {
    52. "key" : "red",
    53. "doc_count" : 1
    54. }
    55. ]
    56. }
    57. }
    58. }

    重新评分过滤的搜索结果

    重新评分有助于提高精度,方法是仅对查询和 post_filter 阶段返回的顶部(例如 100 - 500 个)文档进行重新排序,使用另外的(通常成本更高)算法,而不是将成本高昂的算法应用于索引中的所有文档。

    在每个分片返回结果以由处理整个搜索请求的节点排序之前,在每个分片上执行重新评分(rescore)请求。

    目前 rescore API 只有一种实现:query rescorer,它使用查询来调整评分。 将来,可能会提供替代的记分器,例如,成对的记分器。

    注意:如果 rescore 查询提供了显式 sort(除 _score 降序排列),则会引发错误。

    注意:当向你的用户公开分页时,你不应在逐步浏览每个页面时更改 window_size(通过传递不同的值),因为这会改变热门点击,导致结果在用户浏览页面时发生混乱的变化。

    query rescorer

    查询 rescorer 仅对 querypost_filter 阶段返回的 Top-K 结果执行第二次查询。 将在每个分片上检查的文档数可以由 window_size 参数控制,默认为 10。

    默认情况下,原始查询和重新评分查询的分数线性组合以生成每个文档的最终 _score。 原始查询和重新评分查询的相对重要性可以分别通过 query_weight 和 rescore_query_weight 来控制。 两者都默认为 1。

    例如:

    1. POST /_search
    2. {
    3. "query" : {
    4. "match" : {
    5. "message" : {
    6. "operator" : "or",
    7. "query" : "the quick brown"
    8. }
    9. }
    10. },
    11. "rescore" : {
    12. "window_size" : 50,
    13. "query" : {
    14. "rescore_query" : {
    15. "match_phrase" : {
    16. "message" : {
    17. "query" : "the quick brown",
    18. "slop" : 2
    19. }
    20. }
    21. },
    22. "query_weight" : 0.7,
    23. "rescore_query_weight" : 1.2
    24. }
    25. }
    26. }

    分数的组合方式可以通过 score_mode 来控制:

    Score mode描述
    total添加原始分数和重新评分查询分数。 默认。
    multiply将原始分数乘以重新评分查询分数。 对 function query 重新评分很有用。
    avg平均原始分数和重新评分查询分数。
    max取原始分数和重新分数查询分数的最大值。
    min取原始分数和重新评分查询分数的最小值。

    多次重新评分

    也可以按顺序执行多个重新评分:

    1. POST /_search
    2. {
    3. "query" : {
    4. "match" : {
    5. "message" : {
    6. "operator" : "or",
    7. "query" : "the quick brown"
    8. }
    9. }
    10. },
    11. "rescore" : [ {
    12. "window_size" : 100,
    13. "query" : {
    14. "rescore_query" : {
    15. "match_phrase" : {
    16. "message" : {
    17. "query" : "the quick brown",
    18. "slop" : 2
    19. }
    20. }
    21. },
    22. "query_weight" : 0.7,
    23. "rescore_query_weight" : 1.2
    24. }
    25. }, {
    26. "window_size" : 10,
    27. "query" : {
    28. "score_mode": "multiply",
    29. "rescore_query" : {
    30. "function_score" : {
    31. "script_score": {
    32. "script": {
    33. "source": "Math.log10(doc.count.value + 2)"
    34. }
    35. }
    36. }
    37. }
    38. }
    39. } ]
    40. }

    第一个得到查询的结果,然后第二个得到第一个的结果,依此类推。第二个重新评分将 “看到” 第一个重新评分完成的排序,因此可以在第一个重新评分上使用一个大窗口来 将文档拉入较小的窗口以进行第二次重新评分。

    性能考虑

    仅当你需要区分过滤器搜索结果和聚合时才使用 post_filter。 有时人们会使用 post_filter 进行常规搜索。post_filter 的性质意味着它在查询之后运行,因此过滤(例如缓存)的任何性能优势都完全丧失了。post_filter 应该仅与聚合结合使用,并且仅在你需要差分过滤时使用。

    仅在需要时使用 post_filter

    post_filter 参数有一个别名 filter。 这是为了向后兼容,因为在 ElasticSearch 的早期版本中,post_filter 曾经被命名为过滤器。 改名是有原因的。 虽然在创建只应过滤结果的请求时使用 post_filter 代替查询参数当然是可能且更方便的,但在性能方面不如使用查询参数好。 因此,即使你在调试时不需要使用 post_filter,也可以随意使用它,但仅在实际需要针对生产集群时使用它。

    不要使用 post_filter ,除非你确实需要它来进行聚合

    参考:

    【1】Filter search results | Elasticsearch Guide [8.2] | Elastic

  • 相关阅读:
    Mybatis 下划线_英文 _test 与 下划线_数字 _1 等特殊字段无法映射为实体类的字段问题
    万字总结:CSS伪元素和伪类全网最全解析
    【力扣刷题】数组实现栈、后缀表达式(逆波兰表达式)求值、中缀表达式转换为后缀表达式(无括号&&有括号)
    ASCII 码对照表详解
    挑战 Google 搜索?OpenAI 发布最强 AI 对话系统 ChatGPT
    数据结构 | 队列的实现
    【听课笔记】复旦大学遗传学_10肿瘤遗传学
    java八股文面试[设计模式]——23种设计模式
    108页6万字某小区施工组织设计方案
    Python---格式化输出与%百分号----涉及转义符 \ 反斜杠的使用
  • 原文地址:https://blog.csdn.net/UbuntuTouch/article/details/125500211