文档章节

Elasticsearch 5.x 字段折叠的使用

Medcl
 Medcl
发布于 2017/02/03 20:19
字数 2067
阅读 925
收藏 25

199aon3omgg1vjpg.jpg


在 Elasticsearch 5.x 有一个字段折叠(Field Collapsing,#22337)的功能非常有意思,在这里分享一下,
 
字段折叠是一个很有历史的需求了,可以看这个 issue,编号#256,最初是2010年7月提的issue,也是讨论最多的帖子之一(240+评论),熬了6年才支持的特性,你说牛不牛,哈哈。
 
目测该特性将于5.3发布,尝鲜地址:Elasticsearch-5.3.0-SNAPSHOT,文档地址:search-request-collapse
 
So,什么是字段折叠,可以理解就是按特定字段进行合并去重,比如我们有一个菜谱搜索,我希望按菜谱的“菜系”字段进行折叠,即返回结果每个菜系都返回一个结果,也就是按菜系去重,我搜索关键字“鱼”,要去返回的结果里面各种菜系都有,有湘菜,有粤菜,有中餐,有西餐,别全是湘菜,就是这个意思,通过按特定字段折叠之后,来丰富搜索结果的多样性。
 
说到这里,有人肯定会想到,使用 term agg+ top hits agg 来实现啊,这种组合两种聚和的方式可以实现上面的功能,不过也有一些局限性,比如,不能分页,#4915;结果不够精确(top term+top hits,es 的聚合实现选择了牺牲精度来提高速度);数据量大的情况下,聚合比较慢,影响搜索体验。
 
而新的的字段折叠的方式是怎么实现的的呢,有这些要点:

  1. 折叠+取 inner_hits 分两阶段执行(组合聚合的方式只有一个阶段),所以 top hits 永远是精确的。
  2. 字段折叠只在 top hits 层执行,不需要每次都在完整的结果集上对为每个折叠主键计算实际的 doc values 值,只对 top hits 这小部分数据操作就可以,和 term agg 相比要节省很多内存。
  3. 因为只在 top hits 上进行折叠,所以相比组合聚合的方式,速度要快很多。
  4. 折叠 top docs 不需要使用全局序列(global ordinals)来转换 string,相比 agg 这也节省了很多内存。
  5. 分页成为可能,和常规搜索一样,具有相同的局限,先获取 from+size 的内容,再合并。
  6. search_after 和 scroll 暂未实现,不过具备可行性。
  7.  折叠只影响搜索结果,不影响聚合,搜索结果的 total 是所有的命中纪录数,去重的结果数未知(无法计算)。

 
下面来看看具体的例子,就知道怎么回事了,使用起来很简单。

  • 先准备索引和数据,这里以菜谱为例,name:菜谱名,type 为菜系,rating 为用户的累积平均评分
DELETE recipes
PUT recipes
POST recipes/type/_mapping
{
  "properties": {
    "name":{
      "type": "text"
    },
    "rating":{
      "type": "float"
    },"type":{
      "type": "keyword"
    }
  }
}
POST recipes/type/
{
  "name":"清蒸鱼头","rating":1,"type":"湘菜"
}

POST recipes/type/
{
  "name":"剁椒鱼头","rating":2,"type":"湘菜"
}

POST recipes/type/
{
  "name":"红烧鲫鱼","rating":3,"type":"湘菜"
}

POST recipes/type/
{
  "name":"鲫鱼汤(辣)","rating":3,"type":"湘菜"
}

POST recipes/type/
{
  "name":"鲫鱼汤(微辣)","rating":4,"type":"湘菜"
}

POST recipes/type/
{
  "name":"鲫鱼汤(变态辣)","rating":5,"type":"湘菜"
}

POST recipes/type/
{
  "name":"广式鲫鱼汤","rating":5,"type":"粤菜"
}

POST recipes/type/
{
  "name":"鱼香肉丝","rating":2,"type":"川菜"
}

POST recipes/type/
{
  "name":"奶油鲍鱼汤","rating":2,"type":"西菜"
} 

 

  • 现在我们看看普通的查询效果是怎么样的,搜索关键字带“鱼”的菜,返回3条数据
POST recipes/type/_search
{
  "query": {"match": {
    "name": "鱼"
  }},"size": 3
} 

全是湘菜,我的天,最近上火不想吃辣,这个第一页的结果对我来说就是垃圾,如下:

{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": 0.26742277,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYF_OA-dG63Txsd",
        "_score": 0.26742277,
        "_source": {
          "name": "鲫鱼汤(变态辣)",
          "rating": 5,
          "type": "湘菜"
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHXO_OA-dG63Txsa",
        "_score": 0.19100356,
        "_source": {
          "name": "红烧鲫鱼",
          "rating": 3,
          "type": "湘菜"
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHWy_OA-dG63TxsZ",
        "_score": 0.19100356,
        "_source": {
          "name": "剁椒鱼头",
          "rating": 2,
          "type": "湘菜"
        }
      }
    ]
  }
}

我们再看看,这次我想加个评分排序,大家都喜欢的是那些,看看有没有喜欢吃的,执行查询:

POST recipes/type/_search
{
  "query": {"match": {
    "name": "鱼"
  }},"sort": [
    {
      "rating": {
        "order": "desc"
      }
    }
  ],"size": 3
} 

结果稍微好点了,不过3个里面2个是湘菜,还是有点不合适,结果如下:

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": null,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYF_OA-dG63Txsd",
        "_score": null,
        "_source": {
          "name": "鲫鱼汤(变态辣)",
          "rating": 5,
          "type": "湘菜"
        },
        "sort": [
          5
        ]
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYW_OA-dG63Txse",
        "_score": null,
        "_source": {
          "name": "广式鲫鱼汤",
          "rating": 5,
          "type": "粤菜"
        },
        "sort": [
          5
        ]
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHX7_OA-dG63Txsc",
        "_score": null,
        "_source": {
          "name": "鲫鱼汤(微辣)",
          "rating": 4,
          "type": "湘菜"
        },
        "sort": [
          4
        ]
      }
    ]
  }
}

现在我知道了,我要看看其他菜系,这家不是还有西餐、广东菜等各种菜系的么,来来,帮我每个菜系来一个菜看看,换 terms agg 先得到唯一的 term 的 bucket,再组合 top_hits agg,返回按评分排序的第一个 top hits,有点复杂,没关系,看下面的查询就知道了:

GET recipes/type/_search
{
  "query": {
    "match": {
      "name": "鱼"
    }
  },
  "sort": [
    {
      "rating": {
        "order": "desc"
      }
    }
  ],"aggs": {
    "type": {
      "terms": {
        "field": "type",
        "size": 10
      },"aggs": {
        "rated": {
          "top_hits": {
            "sort": [{
              "rating": {"order": "desc"}
            }], 
            "size": 1
          }
        }
      }
    }
  }, 
  "size": 0,
  "from": 0
} 

看下面的结果,虽然 json 结构有点复杂,不过总算是我们想要的结果了,湘菜、粤菜、川菜、西菜都出来了,每样一个,不重样:

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "type": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "湘菜",
          "doc_count": 6,
          "rated": {
            "hits": {
              "total": 6,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYF_OA-dG63Txsd",
                  "_score": null,
                  "_source": {
                    "name": "鲫鱼汤(变态辣)",
                    "rating": 5,
                    "type": "湘菜"
                  },
                  "sort": [
                    5
                  ]
                }
              ]
            }
          }
        },
        {
          "key": "川菜",
          "doc_count": 1,
          "rated": {
            "hits": {
              "total": 1,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYr_OA-dG63Txsf",
                  "_score": null,
                  "_source": {
                    "name": "鱼香肉丝",
                    "rating": 2,
                    "type": "川菜"
                  },
                  "sort": [
                    2
                  ]
                }
              ]
            }
          }
        },
        {
          "key": "粤菜",
          "doc_count": 1,
          "rated": {
            "hits": {
              "total": 1,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYW_OA-dG63Txse",
                  "_score": null,
                  "_source": {
                    "name": "广式鲫鱼汤",
                    "rating": 5,
                    "type": "粤菜"
                  },
                  "sort": [
                    5
                  ]
                }
              ]
            }
          }
        },
        {
          "key": "西菜",
          "doc_count": 1,
          "rated": {
            "hits": {
              "total": 1,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHY3_OA-dG63Txsg",
                  "_score": null,
                  "_source": {
                    "name": "奶油鲍鱼汤",
                    "rating": 2,
                    "type": "西菜"
                  },
                  "sort": [
                    2
                  ]
                }
              ]
            }
          }
        }
      ]
    }
  }
}

上面的实现方法,前面已经说了,可以做,有局限性,那看看新的字段折叠法如何做到呢,查询如下,加一个 collapse 参数,指定对那个字段去重就行了,这里当然对菜系“type”字段进行去重了:

GET recipes/type/_search
{
  "query": {
    "match": {
      "name": "鱼"
    }
  },
  "collapse": {
    "field": "type"
  },
  "size": 3,
  "from": 0
}

结果很理想嘛,命中结果还是熟悉的那个味道(和查询结果长的一样嘛),如下:

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": null,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoDNlRJ_OA-dG63TxpW",
        "_score": 0.018980097,
        "_source": {
          "name": "鲫鱼汤(微辣)",
          "rating": 4,
          "type": "湘菜"
        },
        "fields": {
          "type": [
            "湘菜"
          ]
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoDNlRk_OA-dG63TxpZ",
        "_score": 0.013813315,
        "_source": {
          "name": "鱼香肉丝",
          "rating": 2,
          "type": "川菜"
        },
        "fields": {
          "type": [
            "川菜"
          ]
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoDNlRb_OA-dG63TxpY",
        "_score": 0.0125863515,
        "_source": {
          "name": "广式鲫鱼汤",
          "rating": 5,
          "type": "粤菜"
        },
        "fields": {
          "type": [
            "粤菜"
          ]
        }
      }
    ]
  }
}

我再试试翻页,把 from 改一下,现在返回了3条数据,from 改成3,新的查询如下:

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": null,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoDNlRw_OA-dG63Txpa",
        "_score": 0.012546891,
        "_source": {
          "name": "奶油鲍鱼汤",
          "rating": 2,
          "type": "西菜"
        },
        "fields": {
          "type": [
            "西菜"
          ]
        }
      }
    ]
  }
}

上面的结果只有一条了,去重之后本来就只有4条数据,上面的工作正常,每个菜系只有一个菜啊,那我不乐意了,帮我每个菜系里面多返回几条,我好选菜啊,加上参数 inner_hits 来控制返回的条数,这里返回2条,按 rating 也排个序,新的查询构造如下:

GET recipes/type/_search
{
  "query": {
    "match": {
      "name": "鱼"
    }
  },
  "collapse": {
    "field": "type",
    "inner_hits": {
      "name": "top_rated",
      "size": 2,
      "sort": [
        {
          "rating": "desc"
        }
      ]
    }
  },
  "sort": [
    {
      "rating": {
        "order": "desc"
      }
    }
  ],
  "size": 2,
  "from": 0
}

查询结果如下,完美:

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": null,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYF_OA-dG63Txsd",
        "_score": null,
        "_source": {
          "name": "鲫鱼汤(变态辣)",
          "rating": 5,
          "type": "湘菜"
        },
        "fields": {
          "type": [
            "湘菜"
          ]
        },
        "sort": [
          5
        ],
        "inner_hits": {
          "top_rated": {
            "hits": {
              "total": 6,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYF_OA-dG63Txsd",
                  "_score": null,
                  "_source": {
                    "name": "鲫鱼汤(变态辣)",
                    "rating": 5,
                    "type": "湘菜"
                  },
                  "sort": [
                    5
                  ]
                },
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHX7_OA-dG63Txsc",
                  "_score": null,
                  "_source": {
                    "name": "鲫鱼汤(微辣)",
                    "rating": 4,
                    "type": "湘菜"
                  },
                  "sort": [
                    4
                  ]
                }
              ]
            }
          }
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYW_OA-dG63Txse",
        "_score": null,
        "_source": {
          "name": "广式鲫鱼汤",
          "rating": 5,
          "type": "粤菜"
        },
        "fields": {
          "type": [
            "粤菜"
          ]
        },
        "sort": [
          5
        ],
        "inner_hits": {
          "top_rated": {
            "hits": {
              "total": 1,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYW_OA-dG63Txse",
                  "_score": null,
                  "_source": {
                    "name": "广式鲫鱼汤",
                    "rating": 5,
                    "type": "粤菜"
                  },
                  "sort": [
                    5
                  ]
                }
              ]
            }
          }
        }
      }
    ]
  }
}

好了,字段折叠介绍就到这里。

© 著作权归作者所有

Medcl

Medcl

粉丝 66
博文 9
码字总数 14146
作品 0
长沙
私信 提问
加载中

评论(4)

刘小宝
刘小宝

引用来自“刘小宝”的评论

补充提问,排重后的结果排序再分页可以实现吗,比如按菜系排重后,再按每个菜系评论最高的倒序排序返回前3个菜

引用来自“Medcl”的评论

可以的
💪
Medcl
Medcl 博主

引用来自“刘小宝”的评论

补充提问,排重后的结果排序再分页可以实现吗,比如按菜系排重后,再按每个菜系评论最高的倒序排序返回前3个菜
可以的
刘小宝
刘小宝
补充提问,排重后的结果排序再分页可以实现吗,比如按菜系排重后,再按每个菜系评论最高的倒序排序返回前3个菜
刘小宝
刘小宝
好像不错
Elasticsearch 6.0.0 正式发布,带来大量新特性

在 Elasticsearch 5.0.0 发布之后,Elasticsearch 在333个 commite、2236 个合并请求下,发布了基于 Lucene 7.0.1 的 Elasticsearch 6.0.0 正式版。 Elasticsearch 6.0.0 下载地址 Elastics...

王练
2017/11/15
7.8K
21
ElasticSearch 版本选择及分布式环境搭建

版本问题 因为ElasticSearch 是ELK 组合中的一部分,之前的ELK 中的各个中间件的版本不一致, 2016 年秋季,为了方便各中间件方便配合使用,ElasticSearch 直接从2.x 升级到了5.x,保持了和各...

Java搬砖工程师
2018/12/18
202
0
elasticsearch 5.x常见问题整理

总结一下elasticsearch java api开发过程中遇到的一些问题。 elasticsearch版本:5.1.2 PreBuiltTransportClient找不到 当我们按elasticsearch老版本2.x一样引用maven文件时,如下: 在我们创...

Airship
03/25
41
0
[译]ElasticSearch数据类型--string类型已死, 字符串数据永生

原文链接: https://www.elastic.co/blog/s... Text vs. keyword 随着ElasticSearch 5.0的到来, 同时也迎来了该版本的重大特性之一: 移除了类型. 这个变动的根本原因是类型会给我们带来很多困...

牧曦之晨
09/29
0
0
Elasticsearch Rest Client bboss v5.5.8 发布

The best Elasticsearch Highlevel Rest Client API-----bboss v5.5.8 发布。 主要功能特色 ElasticSearch兼容性:1.x,2.x,5.x,6.x,+ JDK兼容性: jdk 1.7+ Spring boot兼容性:1.x,2.x ORM和D......

bboss
04/04
876
0

没有更多内容

加载失败,请刷新页面

加载更多

Docker 常用命令速查手册

记录一下docker的日常使用命令,本文主要针对linux + mac操作系统而言,window是否适用不太确定,谨慎使用 <!-- more --> 1. docker进程 docker进程启动、停止、重启,常见的三种case # 启动...

小灰灰Blog
10分钟前
2
0
主流移动端账号登录方式的原理及设计思路

1、引言 在即时通讯网经常能看到各种高大上的高并发、分布式、高性能架构设计方面的文章,平时大家参加的众多开发者大会,主题也都是各种高大上的话题——什么5G啦、AI人工智能啦、什么阿里双...

imtech
18分钟前
3
0
如何递归计算目录中的所有代码行?

我们有一个PHP应用程序,并希望计算特定目录及其子目录下的所有代码行。 我们不需要忽略评论,因为我们只是想弄清楚。 wc -l *.php 该命令在给定目录中运行良好,但忽略子目录。 我当时认为...

技术盛宴
47分钟前
4
0
使用 try-with-resources 优雅关闭资源

我们知道,在 Java 编程过程中,如果打开了外部资源(文件、数据库连接、网络连接等、redis),我们必须在这些外部资源使用完毕后,手动关闭它们。 因为外部资源不由 JVM 管理,无法享用 JVM ...

七弦桐
54分钟前
4
0
04.深入浅出索引(上)

简单来说,索引的出现就是为了提高数据查询效率,就像书的目录一样。 索引的常见模型 索引实现的方式有很多种,所以这里就引入了索引模型的概念,可以用于提高读写效率的数据结构很多,比较常...

scgaopan
57分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部