Elasticsearch入门(一)—— 搜索与聚合

Elasticsearch作为分布式搜索引擎可以说应用非常广了,可以用于站内搜索,日志查询等功能。去年在工作中第一次接触到了Elasticsearch, 在此总结一下自己学到的东西。

Elasticsearch 安装

对于初学者来说Elasticsearch的安装建议采用docker的方式。虽然网上有各种教程关于如何安装。但是这种方式的安装是最方便快捷的了。这里推荐极客时间 提供的docker-compose, 里面既包含了Elasticsearch还有Kibana和Cerabro, 可以一键安装到位了。

启动docker之后访问Kibana 地址为http://localhost:5601, 导入Kibana默认提供的三种数据, 然后就可以在Kibana的开发者工具中练习Elasticsearch搜索和聚合的语法了。


搜索

搜索算分

在介绍搜索 DSL (Domain Specific Language) 之前先介绍一下Elasticsearch的搜索算分规则。
在ES5之前默认的相关性算分采用TF-IDF,现在采用BM25。

  • TF-IDF
  1. TF(Term Frequency): 检索词在一篇文档中出现的频率。 检索词出现的次数除以文档的总字数。
  2. IDF (Inverse Document Frequency): 计算方式为 log(全部文档数/检索词出现过的文档总数)

TF-IDF计算公式:

TF(检索词1)* IDF(区块链) + TF(检索词2)* IDF(检索词2)....

本质就是加权求和

  • BM25
    BM25的计算公式如下, 这里不做详细介绍。


    image.png

Term(词项查询)

如果采用如下方式进行查询会发现返回结果为空,这是因为Elasticsearch 在建立索引的时候会默认对customer_first_name字段进行分词, 分词之后Mary变成了mary因此无法完全匹配到。

GET /kibana_sample_data_ecommerce/_search
{
  "query": {
    "term": {
      "customer_first_name": {
        "value": "Mary"
      }
    }
  }
}

如果改成如下语句就能完全匹配到了

GET /kibana_sample_data_ecommerce/_search
{
  "query": {
    "term": {
      "customer_first_name.keyword": {
        "value": "Mary"
      }
    }
  }
}

这是如下图所示, text类型字段ES会默认创建一个keyword字段,通过这个字段去查询就能严格匹配到了。

image.png

Query (全文本查询)

Term查询并不会去做分词处理, 基于全文本的查询会。 基于全文本的查找包括:Match Query / Match Phrase Query / Query String Query。查询的时候会对输入的查询进行分词,每个词逐个进行底层查询,最后将结果进行合并。并且为每个文档生成一个算分。
下面例子中会先对“Low Spherecords”进行分词,比如结果是“low” 和“spherecords”, 然后再分别对这两个单词进行底层搜索。

POST /kibana_sample_data_ecommerce/_search
{
    "query": {
        "match": {
          "manufacturer":{
            "query": "Low Spherecords"
          }
        }
    }
}

Structured Search (结构化搜索)

结构化搜索针对的是日期,布尔类型和数字这些类型。对于文本来说,可能是博客标签,电商网站商品的UPCs码或者其他标识。
以日期格式为例可以通过range进行范围查找

GET /kibana_sample_data_ecommerce/_search
{
  "query": {
    "range": {
      "order_date": {
        "gte": "now-4y"
      }
    }
  }
}

Bool Query (复合查询)

bool 查询是一个或者多个子查询的组合。总有有must,should,must_notfilter四种查询子句。其中前面两种影响算分属于Query Context,后面两个不影响算分,属于Filter Context。
下面是一个bool 查询的例子

GET /kibana_sample_data_ecommerce/_search
{
  "query": {
    "bool": {
      "must": {
        "term": {
          "day_of_week" : "Monday"
          
        }
      },
      "must_not": [
        {
          "range": {
            "taxful_total_price": {
              "lte": 90
            }
          }
        }
      ], 
      "filter": {
        "term": {
          "currency": "EUR"
        }
      },
      "should": [
        {
          "terms": {
            "sku" : ["ZO0549605496", "ZO0299602996"]
          }
        }
      ]
    }
  }
}

子查询可以任意顺序出现,同时可以嵌套多个查询,如果在bool查询中没有must条件,should中必须至少满足一条查询。

单字符串多字段查询

  1. Dis Max Query

Dis max query 可以解决的问题。如下有个例子,分别插入两个文档。

PUT /blogs/_doc/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}

PUT /blogs/_doc/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}

用如下两个语法去查询,采用第一种语法文档1排在文档2的前面,采用第二种语法结果正好相反。

POST /blogs/_search
{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}
POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick fox" }},
                { "match": { "body":  "Quick fox" }}
            ]
        }
    }
}

原因是因为第一种should语法在算分的过程中会考虑整体语句匹配的总数。上述例子的中title和body字段是相互竞争的, 不应将分数简单的叠加,而是找到单个最佳匹配字段的评分。Disjunction Max Query 是将任何与任一查询匹配的文档作为结果返回。采用字段上最匹配的评分返回
当然第二种语法如果没有加上tie_breaker参数就可能出现超预期的效果。比如查询“Quick pets”的时候,因为两个文档中的字段匹配分数的最高都是一样的所以,文档1又出现在了文档2的前面。可以通过如下加上tie_breaker参数解决。加上后,其他匹配语句的评分会与tie_breaker相乘 ,然后在与最佳匹配的语句求和。

POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ],
            "tie_breaker": 0.7
        }
    }
}
  1. Multi-Match
    Multi-Match提供了 best_fields,most_fields, cross_fields 三种查询类型来应对不同的对字段查询场景。
    Multi-Match基本语法如下
GET /_search
{
  "query": {
    "multi_match" : {
      "query":    "this is a test", 
      "fields": [ "subject", "message" ] 
    }
  }
}

下面是most_fields的例子,这个例子中, title字段使用english分词器, 然后插入两个文档。

PUT /titles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "english"
      }
    }
  }
}

两篇文档

POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }

使用下面的语法查询会发现文档1排在前面与期望不符,这是因为english分词器会把词性给抹掉掉了, barking 变成了bark , dogs变成了dog,而文档1语句更短所以排在了前面。

GET titles/_search
{
  "query": {
    "match": {
      "title": "barking dogs"
    }
  }
}

解决方法是修改titles的设置,增加子字段并添加standard分词。在查询的时候使用most_fields类型进行搜索。

PUT /titles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "english",
        "fields": {"std": {"type": "text","analyzer": "standard"}}
      }
    }
GET /titles/_search
{
   "query": {
        "multi_match": {
            "query":  "barking dogs",
            "type":   "most_fields",
            "fields": [ "title", "title.std" ]
        }
    }
}
GET /_search
{
  "query": {
    "multi_match" : {
      "query":      "Will Smith",
      "type":       "best_fields",
      "fields":     [ "first_name", "last_name" ],
      "operator":   "and" 
    }
  }
}

上面采用best_fields并不适合做跨字段的搜索。因为它的执行逻辑如下,是采用对每个field做operator的匹配。
(+first_name:will +first_name:smith)
| (+last_name:will +last_name:smith)
所有的term必须在一个field中都匹配到
cross_field可用于跨字段搜索

GET /_search
{
  "query": {
    "multi_match" : {
      "query":      "Will Smith",
      "type":       "cross_fields",
      "fields":     [ "first_name", "last_name" ],
      "operator":   "and"
    }
  }
}

它的执行逻辑如下
+(first_name:will last_name:will)
+(first_name:smith last_name:smith)
所有的term都至少在一个field中匹配到

聚合

Aggregation作为Search的一部分语法如下:


image.png

Metric Aggregation

Metric Aggregation可以用来做一些单值或者多值分析。单值分析比如min, max avg, sum , cardinality。 多值分析比如stats, extended stats, percentile, top hits。 下面是单值分析的例子:

GET /kibana_sample_data_ecommerce/_search
{
  "size": 0,
  "aggs": {
    "max_tax_total_price": {
      "max": {
        "field": "taxful_total_price"
      }
    }
  }
}

Bucket Aggregation

Bucket aggregation 是按照一定规则把文档分配到不同的桶中,达到分类的目的。
Terms Aggregation需要打开fieldata。keyword默认支持, text类型需要在mapping中打开然后才会按照分词之后的结果进行分类。
如下这个例子中通过打开category的fieldata从而实现针对category做聚合。

PUT kibana_sample_data_ecommerce/_mapping
{
  "properties" : {
    "category":{
       "type":     "text",
       "fielddata": true
    }
  }
}

GET /kibana_sample_data_ecommerce/_search
{
  "size": 0,
  "aggs": {
    "category": {
      "terms": {
        "field": "category"
      }
    }
  }
}

下面是嵌套聚合的例子,先根据星期进行分类,然后再根据total_quantity进行降序排列取前三个。

GET /kibana_sample_data_ecommerce/_search
{
  "size": 0,
  "aggs": {
    "categories": {
      "terms": {
        "field": "day_of_week"
      },
      "aggs":{
      "total_quantity":{
        "top_hits": {
          "size": 3,
          "sort": [{"total_quantity":"desc"}]
        }
      }
    }
    }
  }
}

Pipeline Aggregation

Pipeline就是在一次聚合分析的基础之上再做一次聚合分析。比如下面的语法就是找出平均total_quantity最少的那个星期。buckets_path指定聚合路径,然后再去做一次min_bucket的计算。

GET /kibana_sample_data_ecommerce/_search
{
  "size": 0,
  "aggs": {
    "categories": {
      "terms": {
        "field": "day_of_week"
      },
        "aggs":{
        "avg_total_quantity":{
          "avg": {
            "field": "total_quantity"
          }
        }
      }
    },
     "min_quantity":{
      "min_bucket":{
        "buckets_path":"categories>avg_total_quantity"
      }
    }
  }
}

根据Pipeline的分析结果输出到原结果中的位置,可将Pipeline分为两大类:

  • Sibling (结果和现有分析结果同级)
    • Max, min, Avg & Sum Bucket
    • Stats, Extended Status Bucket
    • Percentiles Buckets
  • Parent (结果内嵌到现有的聚合分析结果中)
    • Derivative
    • Cumultive Sum
    • Moving Function (滑动窗口)

当数据分散在不同primary shards上的时候,会出现聚合不准确的情况。可以通过添加show_term_doc_count_error对结果进行分析,同时通过增加shard_size的大小来提高精准度。

GET my_flights/_search
{
  "size": 0,
  "aggs": {
    "weather": {
      "terms": {
        "field":"OriginWeather",
        "size":1,
        "shard_size":1,
        "show_term_doc_count_error":true
      }
    }
  }
}

以上就是我对Elasticsearch搜索和聚合查询的总结,会有些遗漏的知识点,具体可以通过ES的官网查阅。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容