ELK7真功夫: Elasticsearch分析与检索功能实操

虽然通过文档_id可以获取到文档,但_id字段一般都是一个无意义的值,在实际应用中更多地是使用文档其它有意义字段做检索。Elasticsearch提供一个专门用于检索的_search接口,这个接口可以根据指定的查询条件检索文档,Elasticsearch强大的检索能力都体现在对这个接口的应用上。除了本文介绍的文档检索基于_search接口,《Elastic Stack应用宝典》这本书中第6章介绍的聚集查询也是基于这个接口,只是使用的参数及格式不同而已。
Elasticsearch可用于文档检索的接口除了_search以外,还包括_count、_msearch、_scripts等。此外,还有一组辅助文档检索的接口可供使用。它们可以查看检索执行情况,为性能调优提供依据,包括_validate、_explain、_field_caps、_search_shards等。
由于_search接口比较重要,本文会以先介绍这个接口的使用方法,然后再介绍接口在检索文档时可用的一些重要参数,其余接口将统一放在最后介绍。本文所有示例都将使用Kibana样例数据,请读者在学习之前确保这些数据已经导入。

_search接口

_search接口可以使用GET或POST方法请求,在请求路径中可以指定一个或多个索引,还可以使用_all或者星号“*”匹配所有索引。如果不指定索引名称,实际上也是匹配所有索引。Elasticsearch为使用这个接口定义了一种查询语言DSL(Domain Specific Language)。DSL是一套基于JSON的查询语言,这种只在某一领域使用的语言通常称为领域特定语言,而它们英文单词首字母简写就是DSL。《Elastic Stack应用宝典》这本书的后续章节都将简称这种语言为DSL,由于DSL内容非常庞杂,将在书中第5章单独介绍。
_search接口有两种请求方式,一种是基于URI的请求方式,另一种则是基于请求体的请求方式。无论是哪一种,它们执行的语法根基都是DSL,只是在使用形式上不同而已。

基于URI

_search接口基于URI的请求方式比较简单,DSL查询条件以请求参数q传递给接口。使用_search接口的最简形式就是不挂任何参数直接调用,可以在路径中添加索引名称,也可以不添加。所以示例1中的请求都是正确的:

GET _search
POST _search
GET kibana_sample_data_logs/_search?q=message:chrome firefox

示例1 基于URI的_search接口调用

在最后一个请求中,参数q定义的内容叫查询字符串(Query String),它的含义是检索message字段值中包含chrome或firefox的文档。查询字符串不仅可以在基于URI的检索中使用,也可以在基于请求体的检索中使用,是DSL定义的一种检索方法。查询字符串属于全文检索,这意味着查询字符串在检索前会被分析器解析为一系列词项和运算符。以示例1中的请求为例,“chrome firefox”会被解析为chrome和firefox两个词项,然后再与message字段的词项索引做匹配。只要message字段中包含chrome或firefox,这个文档就满足查询条件。

查询字符串

查询字符串的基本格式为“<字段名>:<查询值>”,其中字段名可以指定,也可以不指定。如果没有指定字段名,要匹配的字段由index.query.default_field参数设置。这个参数的默认值为*.*,即在所有字段中查询。此外,还可以使用参数df(Default Field)指定要查询的字段名,它与参数q一样是可以用在URI中的参数。如果指定了字段名,查询将在指定字段中匹配词项。除了直接指定字段名以外,还可使用通配符等形式匹配字段,例如:

GET kibana_sample_data_logs/_search?q=geo.\*:CN US
GET kibana_sample_data_logs/_search?q=_exists_:geo

示例2 特殊格式的字段名

在示例2中,查询字符串“geo.*:CN US”将在geo的子字段中匹配CN或US。第二个查询字符串中的_exists_不是一个具体的字段名,而是代表所有非空的title字段。

下面再来看看查询字符串中的查询值。查询值会在检索前通过分析器拆分为词项,在检索时只要字段中包含任意一个词项就视为满足条件。在实现上,这其实是使用了DSL语言中定义的match查询。如果使用双引号将它们括起来,_search接口将使用DSL的match_phrase做短语匹配。从效果上看就类似于用整个短语做检索,而不是使用单个词项做检索。查询值中除了包含词项本身以外,还可以包含操作符OR和AND,注意它们必须大写否则将被识别为词项。例如,“(tom smith) AND jhon”代表的含义是同时包含tom、jhon或smith、jhon的字段。除了可以包含词项、操作符以外,查询字符串的查询值中还可以包含通配符、正则表达式等。表1给出了一些可能的用法:

请求参数

基于URI调用_search接口时可以使用的参数,除了前述的q和df以外还有很多。例如,_source参数可以用来设置在返回结果中是否包含_source字段,还可以使用_source_include或_source_exclude参数包含或排除源文档的字段。这样的参数还有很多,它们大多数与基于请求体的参数具有相同的名称和含义。不仅如此,部分参数对于其它接口也可使用,所以对于参数的介绍将在本文下一阶段统一讲解。表2先将这些参数总结出来供参考:

基于请求体

基于请求体的接口调用,可以在请求体中传递DSL检索条件。尽管可以GET或POST方法请求_search接口,但由于一些客户端不支持使用GET方法发送请求体,所以最好使用POST方法请求基于请求体的_search接口。使用请求体检索时,DSL检索条件通过请求体的query参数设置。例如检索目的地为中国的航班:

GET kibana_sample_data_flights/_search
{
  "query": {
    "term": {
      "DestCountry": "CN"
    }
  }
}

示例3 基于请求体的检索

在示例3的检索中,采用了DSL基于词项(Term)的查询,检索条件是name为tom。DSL中最简单的查询关键字是match_all和match_none,它们分别代表匹配所有和都不匹配。例如:

GET kibana_sample_data_logs/_search
{
  "query": {
    "match_all": {}
  }
}
GET kibana_sample_data_flights/_search
{
  "query": {
    "match_none": {}
  }
}
示例4 match_all和match_none

除了这两种查询以外,DSL还定义了多种多样的查询语法,有关DSL的具体语法将在《Elastic Stack应用宝典》这本书的第5章做全面介绍。在请求体中可以使用的参数除了query以外还有很多,它们很多与表2中的URI参数名称和含义都是相同的,接下来将对一些重要的参数做介绍。

分页与排序

在查询大量数据时必须要做分页,一方面是便于用户浏览,但更重要的是防止一次加载数据量过大而导致内存溢出。_search接口提供了一组参数可用于检索结果分页,但它们有各自不同的应用场景,需要区别对待。

from/size参数

_search接口提供的from和size两个参数可以实现分页,其中from参数代表检索文档的起始位置,默认值为0;而size参数则代表每次检索文档的总量,默认值为10。form和size即可以在URI参数中使用,也可以在请求体中使用。例如示例5中的两个请求都是从第100条文档开始,一共取20条文档:

GET kibana_sample_data_flights/_search?q=DestCountry:CN&from=100&size=20
GET kibana_sample_data_flights/_search
{
  "from": 100, 
  "size": 20, 
  "query": {
    "term": {
      "DestCountry": "CN"
    }
  }
}

示例5 from/size参数

from与size的和不能超过index.max_result_window这个索引配置项设置的值。默认情况下这个配置项的值为10000,所以如果要查询10000条以后的文档,就必须要增加这个配置值。例如,要检索第10000条开始的200条数据,这个参数的值必须要大于10200,否则将会抛出类似“Result window is too large”的异常。由此可见,Elasticsearch在使用from和size处理分页问题时会将所有数据全部取出来,然后再截取用户指定范围的数据返回。所以在查询非常靠后的数据时,即使使用了from和size定义的分页机制依然有内存溢出的可能,而index.max_result_window设置的10000条则是对Elasticsearch的一种保护机制。
那么Elasticsearch为什么要这么设计呢?首先,在互联网时代的数据检索应该通过相似度算法,提高检索结果与用户期望的附和度,而不应该让用户在检索结果中自己挑选满意的数据。以互联网搜索为例,用户在浏览搜索结果时很少会看到第3页以后的内容。假如用户在翻到第10000条数据时还没有找到需要的结果,那么他对这个搜索引擎一定会非常失望。其次,如果真的需要遍历所有数据,不能单纯使用from和size,应该结合scroll接口使用。

scroll参数

scroll即是_search接口的参数也是接口,它提供了一种类似数据库游标的文档遍历机制,一般用于非实时性的海量文档处理需求。例如,将一个索引中的文档导入到另一个索引中,或者将索引中的文档导入到MySQL中。使用scroll机制有两个步骤,第一步是创建游标,第二步则是对游标遍历。这两个步骤基于_search接口执行,例如:

POST kibana_sample_data_logs/_search?scroll=2m&size=1000
{
  "query": {
    "term": {
      "message": "chrome"
    }
  }
}

示例6 创建游标

其中,scroll参数只能URI中使用,而不能出现中请求体中。它定义了检索生成的游标需要保留多长时间,比如2m代表2分钟,1h代表1小时。scroll保留时长不是处理完所有数据所需要的时长,而是处理单次遍历所需要的时间。从性能角度来看,保留时间越短,空间利用率就越高,所以应该根据单次处理能力设置这个值。size参数可以放在请求体中,也可以挂在地址后面,代表了每次遍历时返回的文档数量。size只能在初始查询时指定在遍历时不能更改,请求体中还可以包含其它_search接口的合法参数。在添加了scroll参数后,返回的结果中将包含一个名为_scroll_id的字段,它惟一的代表了一个scroll查询的结果。接下来,根据这个_scroll_id就可以对结果进行遍历了。例如:

POST _search/scroll
{
  "scroll":"2m",
  "scroll_id":"DXF1ZXJ5QW5kRmV0Y2gBAA............FpUZw=="
}

示例7 遍历游标

在遍历游标时,不需要指明索引或映射类型,反复调用_search/scroll接口就可实现对结果的遍历了。请求体中的scroll参数相当于延长了游标的存活时长,而scroll_id则是在初始查询时返回的_scroll_id值。在遍历过程中将根据初始查询时设置的size值返回相应数量的文档,但在遍历过程中不能重新修改size值。每次调用scroll都会自动向后遍历,直到所有文档全部遍历结束。在遍历过程中,每次返回的结果中还是会包含_scroll_id字段,通常来说它的值会保持不变。

scroll在超时后将自动删除,但Elasticsearch也为用户提供了主动删除scroll的接口。可以通过请求体发送要删除的游标,例如:

DELETE _search/scroll
{
  "scroll_id": "id1"
}
DELETE /_search/scroll/_all
DELETE /_search/scroll/id2,id3

示例8 删除游标

对于海量文档的遍历,Elasticsearch还支持对scroll再做片段分割,每一个分割后的片段又可以被独立使用。例如:

POST kibana_sample_data_flights/_search?scroll=1m
{
  "slice":{
    "id":0,
    "max":2
  }
}

示例9 游标分段

其中,max定义了分割片段的总量为2,而id则定义了当前请求返回哪一个片段。所以,上面的请求将会把游标分为两个片段,当前请求返回第一个片段。id值从0开始,所以它的值应该小于max。在返回的结果中,同样也会包含_scroll_id字段。每一个游标片段都是独立的,可以使用多线程并发处理。从物理角度来看,Elasticsearch会让游标片段分配到不同的索引分片上以提升遍历速度。所以,游标片段数量不应该大于索引分片数量,否则游标分段的性能将受到影响。正因如此,游标片段数量也有上限,默认为1024,由index.max_slices_per_scroll参数设置。

search_after参数

在前面介绍分页时提到了两种机制,一种是使用from/size,一种是使用scroll。这两种机制都会将数据整体加载进来,不同的是from/size机制下每一次请求都会加载,而scroll则只在初始时加载。所以,scroll实际上比较适合对同一结果集做多次迭代,但在数据量比较大时依然对性能有影响。为此,Elasticsearch提供了另外一种机制search after,它使用search_after参数定义检索应该在文档某些字段的值之后查询其它文档,所以需要预先以这些字段排序。例如:

POST /kibana_sample_data_flights/_search
{
  "sort":["DestCountry","FlightNum"],
  "search_after":[
    "AE",
    "AR9OTDM"
  ]
}

示例10 search_after

在上面的请求中,kibana_sample_data_flights将按DestCountry和FlightNum字段排序,但只返回DestCountry为AE并且FlightNum为AR9OTDM之后的10条文档(size默认为10)。所以这种机制本质上是通过匹配字段,动态决定第一条文档是哪一个,所以在这种情况下from必须设置为0或-1。不仅如此,参与排序的字段值需要保证惟一。虽然这种惟一性保证并非必须,但如果不惟一则在查询将导致歧义,有可能返回不正确的结果。
需要特别注意的是,这种机制在匹配字段时并非使用精确匹配,而是只要部分满足即可。在上面的例子中,如果含有一个FlightNum为AR9OTDMXXX,也是满足匹配条件的。但由于AR9OTDMXXX会排在AR9OTDM之后,所以还是不会出现问题的。
排序后的检索结果中,都会在最后附带一个排序字段的值,例如示例10检索结果最后会包含如下内容:

"sort" : [
  "AE",
  "IX9M6YB"
]

示例11 排序结果

这个内容正好与当前检索结果中的DestCountry和FlightNum字段值相同,可以为下一次search after使用。讲到这里就涉及到检索的另外一个重要内容-排序。

sort参数

排序是文档检索中另一个重要的话题,在很多应用中排序都是一个必不可少的功能。例如,按商品售价、销量排序以搜索出那些物美价廉的商品。例如在示例10中,由于使用search after机制已经使用到了排序。Elasticsearch提供的排序可以依照文档一个或多个字段排序,包括两个虚拟字段_score和_doc。按_score排序就是按文档相似度得分排序,而_doc则是按索引次序排序。例如:

POST /kibana_sample_data_flights/_search
{
  "sort": [
    "AvgTicketPrice",
    {"FlightDelayMin":"asc"},
    {"DistanceKilometers": {"order": "desc"}},
    "_score"
  ]
}

示例12 排序

在示例12中给出了几种排序方法,将会依次按AvgTicketPrice、FlightDelayMin字段升序排列,再按DistanceKilometers、_score字段降序排列。排序执行的顺序与它们在sort数组中的次序一致。与SQL语言类似,asc代表升序,desc代表降序。默认情况下,除_score按降序排列,其余字段都按升序排列。Elasticsearch支持使用数组类型或多值类型字段做排序,但需要定义如何使用数组中的数据。这包括min、max、avg、sum、median等几种情况,分别代表取最小值、最大值、平均值、总和或中值参与排序,可通过参数mode来定义。例如在下面的示例中,将按products.base_price字段的最大值做升序排列:

POST kibana_sample_data_ecommerce/_search
{
  "sort": [
    {
      "products.base_price": {
        "order": "desc",
        "mode": "max"
      }
    }
  ]
}

示例13 数组排序

默认情况下,查询结果会按_score字段降序排列,_score字段是文档与查询条件的相似度得分。也就是说,越是靠前的结果与查询条件的相似度越高,这与人们的使用习惯相符。相似度问题在全文检索中是一个非常重要的话题,将在《Elastic Stack应用宝典》这本书的第7章中专门讨论。
此外,由于排序算法需要知道所有参与排序的值才可能做运算,所以参与排序字段在文档中的值都需要加载到内存中来。这一方面对节点分配的内存提出了更高要求,另一方面也要求参与排序的字段必须支持文档值(Doc Value)或fielddata机制。这是因为倒排索引保存的是词项到文档的对应关系,适用于通过词项检索文档。但在排序时需要的是通过文档找到字段值参与排序,所以必须能够通过文档找到字段值。在默认情况下,文档值机制对于非text类型的字段都是开启的,而text类型则只能通过开启fielddata机制才可能支持排序。这两种机制在本书第2章第2.2.2节有过介绍,如果不记得了可以回去查看。

字段投影

投影(Projection)的概念源于关系型数据库,是指从一个关系中选取若干个属性形成一个新的关系。简单来说,就是在查询表时不将所有字段返回,而只返回其中的部分字段。Elasticsearch并没有直接引入投影的概念,但支持类似投影的操作。这主要体现在对查询结果的_source字段和fields字段的定制上。

_source参数

Elasticsearch文档查询结果中会包含_source元字段,这个字段存储了文档的最原始数据。_search接口提供了_source参数,可以定制源文档中哪些字段出现中_source中。这个参数可以在URI中使用,也可以在请求体中使用。例如在示例14中,_source将只包含DestCountry字段的值:

POST /kibana_sample_data_flights/_search
{
  "_source":"DestCountry",
  "query":{
    "match_all":{}
  }
}

示例14 _source参数

如果需要返回多个字段,可以使用数组设置_source,并且可以使用通配符星号“*”。例如下面两个请求,都将返回OriginCountry和DestCountry字段:

POST /kibana_sample_data_flights/_search
{
  "_source":["OriginCountry","DestCountry"]
}
POST /kibana_sample_data_flights/_search
{
  "_source":"*Country"
}

示例15 使用数组和星号

当然,使用星号匹配的范围更大一些,如果索引中包含其它以Country结尾的字段,它们也将出现在返回结果中。类似地,_source也可以设置为false,这将禁止在返回的结果中包含_source源文档内容,而只包含元字段。除此之外,还可以在_source字段中添加includes和excludes字段,以明确包含和排除字段。例如在示例16中,将所有包含lon、lat等经纬度信息的字段包含进来,而又排除了DestLocation的子字段,所以在返回的结果中应该只包含OriginLocation:

POST /kibana_sample_data_flights/_search
{
  "_source":{
    "includes":["*.lon","*.lat"],
    "excludes":"DestLocation.*"
  }
}

示例16 includes和excludes

stored_fields参数

除了使用_source字段过滤可以出现在源文档中的字段以外,还可以使用stored_fields字段指定哪些被存储的字段出现在结果中。当然这些字段的store属性要设置为true,否则即使在stored_fields中设置了它们,也会被忽略。例如,在示例17中,author字段的store参数为true而title设置为false,则在查询的结果中将忽略title:

PUT articles
{
  "mappings": {
    "properties": {
      "author": {
        "type": "keyword",
        "store": true
      },
      "title": {
        "type": "keyword"
      },
      "content": {
        "type": "text"
      }
    }
  }
}
POST /articles/_search
{
  "stored_fields":["author", "title"]
}

示例17 stored_fields

在返回结果中会增加一个fields字段,其中包含了stored_fields中配置的字段值。此外,在使用stored_fields之后,_source字段默认将不会出现在结果中,但可通过将_source参数设置为true让它返回。字段的store参数在《Elastic Stack应用宝典》这本书第2章第2.2.2节也有过介绍,当文档某字段单独使用的频率比较高而其它字段值占用空间又非常大时,就可以把这种常用的字段单独保存起来使用。

docvalue_fields参数

docvalue_fields也是_search接口的参数,它用于将文档字段以文档值机制保存的值返回。文档值机制是非text类型字段支持的一种在硬盘中保存字段原始值的机制,可通过字段的doc_value参数设置开启或关闭。这种机制也是在本书第2章第2.2.2节讲解,详细介绍请参考该章节。

POST kibana_sample_data_flights/_search
{
  "_source": "timestamp", 
  "docvalue_fields": [
    {
      "field":"timestamp",
      "format":"epoch_millis"
    }
  ]
}

示例18 docvalue_fields

在示例18中,docvalue_fields接收的对象有两个属性,field定义字段名称,而format则定义数值和日期的格式。在示例中使用了日期格式epoch_millis,所以返回结果将以毫秒数显示timestamp字段。format可以使用use_field_mapping关键字,它代表的含义是使用字段在索引映射中定义的格式。类似于stored_fields,docvalue_fields查询的返回结果中也会增加一个fields字段,其中包含了在docvalue_fields中声明的字段及其文档值。与stored_fields不同的是,在返回结果中默认会包含_source字段。所以在示例18中使用_source参数过滤了返回结果以保证_source中也只包含timestamp字段。

script_fields参数

script_fields同样是_search接口的参数,它可以通过脚本向检索结果中添加字段。与stored_fields和docvalue_fileds类似,通过脚本添加的字段也会出现在结果的fields字段中。默认情况下,使用了script_fields参数后,_source字段也不会出现在返回结果中,但可使用_source参数配置开启。例如在示例19中向返回结果添加了price_per_km字段,它通过AvgTicketPrice字段和DistanceKilometers字段相除而得,反映了机票每公里的平均票价:

POST kibana_sample_data_flights/_search
{
  "script_fields": {
    "price_per_km": {
      "script": {
        "source": "doc['AvgTicketPrice'].value/doc['DistanceKilometers'].value"
      }
    }
  }
}

示例19 script_fields

script_fields中默认使用的脚本也是Painless,可以在这个上下文中使用的变量如表3所示:

(本文节选自《Elastic Stack应用宝典》,机械工业出版社2019年10月出版)



已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页