3.3 检索进阶

Elasticsearch提供了一个基JSON格式的完整的Query DSL来定义查询。Query DSL被视为查询的抽象语法树,由简单查询、复合查询两种类型的语句组成。查询子句的作用互不相同,具体取决于它们是在查询上下文还是过滤器上下文中被使用。这一节主要对Query DSL进行介绍,涉及全文检索、词项检索、复合查询、跨度查询、特殊查询等。

3.3.1 全文检索

全文检索通常用于在全文字段(例如电子邮件正文)上运行查询。程序可以自动分析查询的字段,并在执行之前将每个字段的analyzer或search_analyzer应用于查询字符串。这一节将对全文检索中的match、match_phrase、match_phrase_prefix、multi_match、simple_query_string等查询进行介绍。

1.match查询

match查询子句可接受文字、数字和日期等类型的数据,match子句是查询指定索引下的所有文档(相当于SQL语句的“select content from某数据表”,如代码段3.9所示)。也可以利用size指定返回结果集的大小(如果没有指定size的值,相当于SQL语句的“select top10 content from某数据表”)。

    //代码段3.9: match子句相当于 select content from某数据表    
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
        "match": {
          "content": "程序员"
        }
      }
    }'

代码段3.10给出了一次match_all查询并且返回第11~15个文档的代码实现。

    //代码段3.10:匹配所有文档且返回 top11-top15的记录集   
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
        "query": {
            "match_all": {}
        },
        "from":10,
        "size":5
    }'

代码段3.11给出match_all查询并且指定字段(publish Time)的升序排序的实现方法,返回满足条件的3条记录(在关系型数据库管理系统中,相当于SQL语句“selecttop3*from某数据表order by publish Time”。

    //代码段3.11:匹配所有文档且检索结果按指定的字段排序,返回前3个记录集   
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
        "match_all": {}
      },
      "sort": {
        "publishTime": {         //指定的排序字段
          "order": "asc"         //排序策略
        }
      },
      "size":3                   //返回的结果集大小
    }'

2.match_phrase查询

match_phrase查询对文本进行分析,并在分析过的文本中构建一个短语查询。其中的slop参数定义了在查询文本的词项之间应间隔多少个未知单词才视为短语匹配成功。代码实现见代码段3.12。

    //代码段3.12: match_phrase查询   
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "match_phrase": {
          "title": {
            "query": "中国and日本",
            "slop":2
          }
        }
      },
      "_source": [
        "title"
      ]
    }'

3.match_phrase_prefix查询

match_phrase_prefix与match_phrase基本相同,唯一不同的是这里的查询信息以短语形式出现,查询时考虑的是最后一个词的前缀匹配。其中的参数max_expansions表示短语中最后一个词将与多少候选词进行匹配,默认值为50,代码段3.13使用“开发过程”进行查询,并将max_expansion设置为10,其含义是Elasticsearch为输入的短语中最后一个词项准备候选匹配词的数量。需要注意的是,这里不排除候选词中未出现用户需要的词的可能性。

    //代码段3.13: match_phrase_prefix查询  
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
        "match_phrase_prefix": {
          "content": {
            "query": "开发过程",       //注意要找一个存在的短语
            "max_expansions":10
          }
        }
      }
    }'

4.multi_match查询

Elasticsearch支持使用multi_match子句进行跨字段检索(只需写明需要检索的多个目的字段即可),并同时从多个字段中返回包含指定检索词的内容。下面的代码段3.14实现了在title、content两个字段中,对关键词“中国”的查询。

    //代码段3.14:利用 multi_match在多个字段中检索   
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "multi_match": {
          "query": "中国",
          "fields": [         //指定在哪些字段中进行检索
            "title",  "content"
          ]
        }
      }
    }'

5.simple_query_string查询

与其他的查询类型相比,simple_query_string查询支持Lucene所有的查询语法。对于给定的内容,simple_query_string查询使用查询解析器来构造实际的查询。示例代码如代码段3.15所示[Rafal,2015]

    //代码段3.15: simple_query_string查询   
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "simple_query_string": {
          "query": "title:中国^2+title:日本 -content:美国",
                                                  //注意这里的加号和减号前空格不能省略
          "flags": "ALL"       //设置查询中支持的Lucene查询符号,如+表示AND、-表示NOT
        }
      }
    }'

Tips:代码段3.15中出现的查询是典型的Lucene查询模式,其中,“title:中国^2”是指在title字段中要包含“中国”字符且其权重为2; “+title:日本”是指在title字段中还要同时包含“日本”字符串,只不过该字符串的权重为1,这些权重会影响到最终结果排序;“-content:美国”是说在content字段中不能含有字符串“美国”。

3.3.2 词项检索

虽然全文检索会在执行之前分析查询字符串,但是词项检索要对存储在倒排索引中的确切词语进行操作。这些查询通常用于结构化数据(如数字、日期和枚举等)而不是全文本的内容。这一节将对词项检索中的term、terms、range、prefix、wildcard、regexp等进行介绍。

1.term查询

term查询仅匹配在给定字段有某个词项的文档,如代码段3.16所示即是term查询,term查询中词项不再被解析。如果希望提升该term的重要性,可以在代码中增加boost属性以便提升其重要性。

    //代码段3.16:含有 boost term查询   
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "term": {                   //term查询
          "title": {                //查询字段,给定值及boost
            "value": "中国",
            "boost":10
          }
        }
      }
    }'

2.terms查询

terms查询允许匹配包含某些词项的文档。例如,如果想查询在baidu/baike文档的title字段中包含字符串“中国”或“日本”的文档,可以采用类似代码段3.17中的方法。

    //代码段3.17: terms查询    
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "terms": {
          "title": [
            "中国",
            "日本"
          ]
        }
      }
    }'

3.range查询

range查询是范围查询,一般只作用在单个字段上,并且查询的参数要封装在字段名称中,它也支持如下参数:

· gte:greater-than or equal to,即大于或等于,表示范围下界。

· gt:greater-than,即大于,表示范围下界。

·lte:less-than or equal to,即小于或等于,表示范围上界。

·lt:less-than,即小于,表示范围上界。

· boost:查询的权重,默认值1.0。

有关range查询的用法见代码段3.18。

    //代码段3.18: range查询,其中 lastModifyTime字段是指抓取网页时间,参见前述对 baike类型文件的说明   
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "range": {     //range查询
          "lastModifyTime": {     //指定查询字段及其范围
            "gte": "2016-10-26",
            "lte": "2016-11-16",
            "boost":2
          }
        }
      }
    }'

下面的代码段3.19展示了在类型文件baike中,对日期型或时间型字段(这里是对gatherTime字段)进行范围检索的方法,它表示查找的是从现在开始12个小时之前的查询结果集(注:这里还能改为"now-3d",意为从现在开始往前3天算起),到当前时间的筛选数据子集。

    //代码段3.19: range子句中设定 lastModifyTime的上下限    
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "range": {
          "lastModifyTime": {
            "gt": "now-12h",
            "lt": "now"
          }
        }
      }
    }'

Tips:这里提到的h和d属于时间单位,Elasticsearch支持的时间单位有y(年)、M(月)、w(周)、d(日)、h(12时制小时)、H(24时制小时)、m(分钟)、s(秒)。

4.prefix查询

prefix查询能够找到某个字段以给定前缀开头的文档。同样,这里也支持boost属性影响其排序结果。基于prefix查询实现的方法见代码段3.20。

    //代码段3.20: prefix查询    
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "prefix": {         //prefix查询
          "title": {        //在title中查询,可指定值和权重
            "value": "中华",
            "boost":2
          }
        }
      },
      "_source": [
        "title"
      ]
    }'

5.wildcard查询

wildcard通配符查询允许在要查询的内容中使用通配符*和?(通配符*表示任意多个任意字符;通配符?表示一个任意字符)。除此之外,它和term查询相似,如代码段3.21所示,只需把term查询中的“term”换为“wildcard”,在查询字段中根据需要嵌入*或?即可。

    //代码段3.21:含有 boost wildcard查询   
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "wildcard": {               //wildcard查询
          "content": {              //查询字段,给定值及boost
            "value": "*豆",
            "boost":10
          }
        }
      }
    }'

6.regexp查询

在对程序员论坛数据集it-home的检索过程中,可能要设置相应的from、size、boost等属性。比如使用regexp(正则表达式)查询的方式,从数据集中的“category”字段搜索关键字,为了提升category字段的重要性,在代码中可提升其boost属性值,也可以同时指定多个字段的属性值。代码段3.22中利用正则表达式查询,指定搜索结果的category字段必须为“Web前端”,或为“数据库学习”,并获取其前10条结果,如图3.4所示。

    //代码段3.22:含有 boost wildcard查询    
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "from":0,
      "size":10,
      "query": {
        "regexp": {
          "category": {
            "value": "[Web前端|数据库学习]",     //Web前端或数据库学习
            "boost":10
          }
        }
      }
    }'

图3.4 正则表达式查询

3.3.3 复合查询

复合查询语句包含了其他复合查询或简单查询,能够结合其查询结果和分值以改变其表现形式,或从查询转换为过滤器上下文。这一节将对符合查询中的bool、boosting等查询进行介绍。

1.bool查询

bool查询是根据对结果的必要程度,将不同查询子句组合的查询方式。在bool查询中可以同时使用must、filter、should、must_not等子句,之后可与基本检索方法里的match和term查询结合使用。代码段3.23是在程序员论坛数据集it-home中检索“category”字段中含有“开发”“java”且不含“.net”字符串的结果集,其中对“java”的查询在filter子句中,不考虑权重。查询结果如图3.5所示。

    //代码段3.23:查询“category”字段中含有开发”“java”且不含有“.net”的结果    
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
        "bool": {
          "must": {
            "match": {"category": "开发"}
          },
          "filter": {
            "term": {"category": "java"}
          },
          "must_not": {
            "term": {"category": ".net"}
          }
        }
      }
    }'

图3.5 Bool复合查询的实现

2.boosting查询

boosting查询可将匹配的结果降级处理。与bool查询中的not子句不同,查询结果中仍然会包含不符合预期的此项,但其分值会降低。代码段3.24实现了对有关“java”的分类中,内容与“json”不相关信息的查询。关键词“json”处于negative子句中,其结果的分值将会根据negative_boost的值被相应降低。

    //代码段3.24: boosting查询   
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
        "boosting": {
          "positive": {           //预期相关的信息
            "term": {
              "category": "java"
            }
          },
          "negative": {           //预期不相关的信息
            "term": {
              "content": "json"
            }
          },
          "negative_boost":0.2   //不相关信息要降低的分值
        }
      }
    }'

3.3.4 跨度查询

跨度查询是一种低级别、基于词项相对位置的查询,可以提供对指定项的顺序和接近度的控制。这些通常用于实施对法律文件或专利的非常具体的查询。除span_multi查询之外,跨度查询不能与非跨度查询复合使用。本节将对span_term、span_or、span_containing、span_within等查询进行介绍。

1.span_term查询

span_term查询可以查询包含有查询词项的一段文本,这一功能相当于Lucene中的SpanTermQuery。代码段3.25实现了对含有词项“java”的文本的查询。

    //代码段3.25: span_term查询    
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
    "span_term": {
          "content": {
            "value": "java",
            "boost":2
          }
        }
      }
    }'

2.span_or查询

span_or匹配其span_term子句查询结果的并集。这一功能相当于Lucene中的SpanOrQuery。代码段3.26实现了对含有词项“java”“json”或“jquery”的文本的查询。

    //代码段3.26:查询含有词项“java”“json”“jquery”的文本    
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
        "span_or": {
          "clauses": [
            {"span_term": {"content": "java"}},
            {"span_term": {"content": "json"}},
            {"span_term": {"content": "jquery"}}
          ]
        }
      }
    }'

3.span_containing查询

span_containing查询含有一个little子句和一个big子句,在查询时一旦发现big子句的匹配项中包含了little子句中的匹配项,那么big子句的匹配项将作为查询结果并返回。该查询功能相当于Lucene中的SpanContainingQuery。代码段3.27实现了在词项"gson"和"api"的匹配项中,对含有词项"java"匹配项的查询,结果如图3.6所示,图中的方框标出了词项"gson"和"api"在文中形成的跨度,而词项"java"正处于其中。其中big子句中的span_near查询是一种查找临近词项的跨度查询,后面的slop指定了两个词项之间可以存在多少个不匹配的词项。

    //代码段3.27:查询词项“json”“api”的匹配项中,含有词项“java”匹配项的结果    
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
        "span_containing": {
          "little": {
            "span_term": {"content": "java"}         //跨度中间的词项
          },
          "big": {
            "span_near": {
              "clauses": [
                {"span_term": {"content": "gson"}}, //两个词项确定了一个跨度
                {"span_term": {"content": "api"}}
              ],
              "slop":20,
              "in_order": true
            }
          }
        }
      }
    }'

图3.6 查询中间包含特定词项匹配项的两个词项所在跨度的文本段

4.span_within查询

span_within查询也含有一个little子句和一个big子句,在查询时一旦发现little子句的匹配项包含在big子句中的匹配项中,那么little子句的匹配项将作为查询结果被返回。该查询功能相当于Lucene中的SpanWithinQuery。代码段3.28实现了对含有词项“java”的匹配项中,被包含在词项“json”和“api”跨度中的文本段查询,查询结果同上。

    //代码段3.28:查询含有词项“java”的匹配项中,被包含在词项“json”“api”跨度中的结果    
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
        "span_within": {
          "little": {
            "span_term": {"content": "java"}           //跨度中间的词项
          },
          "big": {
            "span_near": {
              "clauses": [
                {"span_term": {"content": "json"}},    //两个词项确定了一个跨度
                {"span_term": {"content": "api"}}
              ],
              "slop":20,
              "in_order": true
            }
          }
        }
      }
    }'

3.3.5 特殊查询

除上面提到的几类查询方式以外,还有一些查询不属于其中任何一种方式。这一节将对more_like_this查询进行介绍,script查询将在下一节进行介绍。

1.more_like_this查询

more_like_this查询得到与所提供的文本相似的文档。在这里可以使用的部分参数如下[Rafal,2015]:

· fields:查询所作用的字段的数组,默认是_all。

·like_text:指明文档应比较的内容。

· min_term_freq:文档中词项出现的最低频次,低于该值的词项将被忽略,默认是2。

· min_doc_freq:词项应至少在多少个文档中出现才不会被忽视,默认是5。

· min_word_length:指明单个单词的最小长度,低于该值的单词将被忽视,默认是0。

· max_query_terms:指明在生成的查询中查询词项的最大数目,默认是25。

· max_doc_freq:指明出现词项的文档最大数目,以避免词项被忽视,默认是无界0。

· max_word_length:指明单个单词的最大长度,高于该值的单词将被忽视,默认是无界0。

· stop_words:指明忽略词集。

· boost:提升一个查询的权重时使用的权重,默认是1.0。

· analyzer:指明用于分析内容的分词器。

more_like_this查询示例代码如代码段3.29所示,查询与指定的两条存储在Elasticsearch中的信息,以及一条额外的信息相似的结果。其中,在fields子句中指定了title和content两个字段,这是more_like_this查询的执行范围;在like数组中前两个对象分别指定了it-home索引中,posts类型下的某个特定_id中的文档内容,而后面的文本是额外的自定义查询字符串;min_term_freq表示信息若要被查询到,最少要出现的次数;max_query_terms表示要显示匹配结果的最大条数。

    //代码段3.29: more_like_this查询    
    curl-XPOST localhost:9200/it-home/posts/_search-d'{
      "query": {
        "more_like_this": {                //more_like_this查询
          "fields": ["title", "content"],  //查询字段
          "like": [                        //指定应比较的内容
            {
              "_index": "it-home",
              "_type": "posts",
              "_id": "AViVbrJPOQdYmPY46b3B"
            },
            {
              "_index": "it-home",
              "_type": "posts",
              "_id": "AViVb4p2OQdYmPY46b5T"
            },
            "现在我们来写一个测试程序"         //可以直接指定额外的相关内容
          ],
          "min_term_freq":1, //表示最少出现多少次才能被检索出
          "max_query_terms":12            //规定显示结果的最大条数
        }
      }
    }'

3.3.6 脚本script

脚本模块可以通过使用script子句来定制表达式,当执行查询时,script子句可以生成一个由外部参数计算出的特定字段,插入到原来的查询语句中执行;或者在查询中为字段求出一个定制化的分值。script子句默认使用的脚本语言是painless,一般情况下子句中包含三条语句,下面给出script子句的一般格式。

    "script": {
        "lang":  "...",           //这里设置使用何种脚本语言
        "inline"|"stored"|"file": "...",     //这里编辑表达式,给出形式参数
        "params": {...}                      //这里给出实际参数
    }

在执行一些检索、聚合或更新操作时,出于安全上的原因,Elasticsearch默认禁止动态脚本的执行。要执行script或其他检索、聚合等操作,需要在Elasticsearch的配置文件elasticsearch.yml中添加以下三行配置信息来启用相关的功能,并重新启动Elasticsearch。

    script.engine.groovy.inline.update: true
    script.inline: true
    script.stored: true

Tips:尽管脚本语言groovy仍然能在Elasticsearch中使用,但Elastic官方已不提供支持,本书中也不再进行介绍。

script查询允许使用脚本来为查询提供数据。在查询中定义参数并填充到查询语句中形成一条完整的查询语句。script可以被编译和缓存以更快地执行。如果在编写查询语句时,多段代码除少量参数不同之外基本相同,最好使用script语句,将不同参数分别传递到语句中即可。代码段3.30实现了对baidu索引下的baike类型中,更新时间在2016年之后的词条信息的查询,结果如图3.7所示。这里需要注意,字段lastModifyTime的数据类型必须设置为date,否则类型不匹配,查询将失败。

    //代码段3.30:使用 script查询2016年及以后的词条    
    curl-XPOST localhost:9200/baidu/baike/_search-d'{
      "query": {
        "bool": {
          "must": {
          "script": {
              "script": {
                "inline": "doc['lastModifyTime'].value>params.param1", //设置形式参数
                "lang": "painless",
                "params": {"param1":2016}    //给出实际参数
              }
            }
          }
        }
      }
    }'

图3.7 查询索引baike中2016年及以后的词条

在执行更新操作时也可以使用script,将参数传递给inline子句中的ctx._source.{字段名}来进行数据更新。代码段3.31实现了对某条记录作者的修改。其中,inline子句中的ctx._source是指访问文档中的_source字段,_source字段是文档中所有字段的集合,例如代码段3.6、3.12和3.20等处均指定了显示_source字段中的一部分;lang语句指定了脚本中使用painless语言;params语句为inline语句中的形式参数params.name给出了实际参数。

    //代码段3.31:使用 script修改某一条记录的作者    
    curl -XPOST localhost:9200/it-home/posts/AViVbpg-OQdYmPY46b20/_update -d '{ //中
    间指定了_id号
    "script": {
        "inline": "ctx._source.user=params.name",
        "lang": "painless",
        "params": {"name": "cy"}
      }
    }'

另外,script还支持在inline语句中使用类似编程语言的方法进行编写。代码段3.32将doc['log_size']作为变量使用,实现了对服务器日志记录中日志长度的计算。在script_fields子句中,"total_size"是为其设置的名称,下面script子句中的inline给出了计算日志长度的代码。

    //代码段3.32:使用 script计算日志长度    
    curl-XPOST localhost:9200/whale/log/_search-d'{
      "query": {
        "match_all": {}
      },
    "script_fields": {
        "total_size": {
          "script": {
            "lang": "painless",
            "inline": "int total=0; for (int i=0; i<doc['log_size'].length; ++i) {
            total+=doc['log_size'][i]; }return total; "
          }
        }
      }
    }'