ES全文搜索-match query

match query是ES中常见的一种全文搜索,该查询可以接受文本、数字、日期或者布尔类型,不过它在做查询之前会对查询的文本进行分词。

所谓的分词就是将文本拆分成一个个的词,对于英文而言通常通过空格符号拆分就行。但是对于中文而言,显然不是很好的处理方式。如果对这方面有兴趣,可以关注公众号,在我之前的文章中有相关内容介绍。

简单示例

在介绍简单使用的时候首先构建一些测试数据,示例代码如下:

POST _bulk
{ "index" : { "_index" : "doc"} }
{ "content":"what is java?"}
{ "index" : { "_index" : "doc"} }
{ "content":"Java is a programming language and computing platform first released by Sun Microsystems in 1995"}
{ "index" : { "_index" : "doc"} }
{"content":"java is programming language"}
{ "index" : { "_index" : "doc"} }
{"content":"Go is programming language"}

下面我们实现一个最简单的搜索,示例代码如下:

GET doc/_search
{
"query": {
"match": {
"content": "java programming language"
}
}
}

最后结果如下:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 1.2698221,
    "hits" : [
      {
        "_index" : "doc",
        "_type" : "_doc",
        "_id" : "PtPKxoQB5b0eqQ47tMmg",
        "_score" : 1.2698221,
        "_source" : {
          "content" : "java is programming language"
        }
      },
      {
        "_index" : "doc",
        "_type" : "_doc",
        "_id" : "P9PKxoQB5b0eqQ47tMmg",
        "_score" : 0.8465481,
        "_source" : {
          "content" : "Go is programming language"
        }
      },
      {
        "_index" : "doc",
        "_type" : "_doc",
        "_id" : "PdPKxoQB5b0eqQ47tMmg",
        "_score" : 0.6971004,
        "_source" : {
          "content" : "Java is a programming language and computing platform first released by Sun Microsystems in 1995"
        }
      },
      {
        "_index" : "doc",
        "_type" : "_doc",
        "_id" : "PNPKxoQB5b0eqQ47tMmg",
        "_score" : 0.45743963,
        "_source" : {
          "content" : "what is java?"
        }
      }
    ]
  }
}

从结果可以看出,所有的文档都被搜索出来了。在执行该查询时,match query会将搜索的文本进行分词,在我们的示例中,它会把文本拆分为javaprogramminglanguage三个词。同时match query的查询类型是boolean,你可以理解为该查询中有三个子查询,每个子查询对应的搜索内容就是拆分之后的词。默认情况下,子查询之间的关系为OR,也就是只要文档匹配到任意一个子查询,文档都能被搜索出来。

参数详解

上面的示例只是一个最简单的查询,实际上match query还支持许多参数,通过修改这些参数可以实现更复杂的功能。

analyzer

分析器参数。在创建索引时我们可以在mapping中指定字段的类型和分析器。该分词器包括索引时分析器和搜索时分析器,其分别由analyzersearch_analyzer控制,默认情况下不指定使用的是standard。而在搜索文本的同时,我们也可以指定分析器。该分析器是对搜索的文本进行分词。如果在我们没有指定的情况下,该分析器保持跟查询字段一致。大多数情况下,该值不需要设置使用默认值即可。如果设置成不一致,可能会导致查询有问题。

示例中使用的ik分析器需要安装,它不是ES中内置的分析器。

PUT test_doc
{
"mappings": {
"properties": {
"content":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}

POST _bulk
{ "index" : { "_index" : "test_doc"} }
{ "content":"今天天气不错"}
{ "index" : { "_index" : "test_doc"} }
{ "content":"天气预报说明天是阴天"}

上面示例我们创建了一个新的索引并且为其设置了分析器为ik_max_word,另外插入了部分测试数据,下面使用match query搜索明天天气信息,查询示例如下:

GET test_doc/_search
{
"query": {
"match": {
"content":{
"query": "明天天气",
"analyzer": "ik_max_word"
}
}
}
}

查询结果如下:

{
  "took" : 17,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.9395274,
    "hits" : [
      {
        "_index" : "test_doc",
        "_type" : "_doc",
        "_id" : "CdPhxoQB5b0eqQ47K8qN",
        "_score" : 0.9395274,
        "_source" : {
          "content" : "今天天气不错"
        }
      },
      {
        "_index" : "test_doc",
        "_type" : "_doc",
        "_id" : "CtPhxoQB5b0eqQ47K8qN",
        "_score" : 0.8195878,
        "_source" : {
          "content" : "天气预报说明天是阴天"
        }
      }
    ]
  }
}

可以发现文档都能被搜索到,现在我们修改搜索时的分析器改成standard,再次执行搜索如下:

GET test_doc/_search
{
"query": {
"match": {
"content":{
"query": "明天天气",
"analyzer": "standard"
}
}
}
}

执行该查询发现一个文档都没搜索到。之所以搜索不到,是因为ik_max_wordstandard分析器分词结果不一致导致的。使用_analyzeAPI对搜索条件分词对比如下:

POST test_doc/_analyze
{
"text":"明天天气",
"analyzer": "ik_max_word"
}


{
"tokens" : [
{
"token" : "明天",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "天天",
"start_offset" : 1,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "天气",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 2
}
]
}
POST test_doc/_analyze
{
"text":"明天天气",
"analyzer": "standard"
}


{
"tokens" : [
{
"token" : "明",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "天",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "天",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "气",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 3
}
]
}

从分词结果可以看出,它们的分词结果不一致所以导致搜索不到对应的文档。所以大多数情况下该值最好保持一致。

auto_generate_synonyms_phrase_query

是否为近义词生成短语查询,该值默认为true

了解该属性之前需要先了解近义词查询相关内容。

首先构建测试数据,示例代码如下:

PUT /test_doc1
{
"settings": {
"analysis": {
"filter": {
"my_synonym":{
"type":"synonym_graph",
"expand":true,
"synonyms":["ny,new york"]
}
},
"analyzer": {
"my_synonym_analyzer":{
"tokenizer":"standard",
"filter":["my_synonym"]
}
}
}
},
"mappings": {
"properties": {
"content":{
"type":"text",
"analyzer": "standard"
}
}
}
}


POST _bulk
{ "index" : { "_index" : "test_doc1"} }
{ "content":"ny in usa"}
{ "index" : { "_index" : "test_doc1"} }
{ "content":"new time is york"}
{ "index" : { "_index" : "test_doc1"} }
{ "content":"new york in usa"}

上面示例代码中创建了一个新的索引tes_doc1,并且该索引中还自定义了一个分析器用来实现近义词搜索,最后插入了部分测试数据。

GET test_doc1/_search
{
"query": {
"match": {
"content": {
"query": "ny",
"analyzer": "my_synonym_analyzer",
"auto_generate_synonyms_phrase_query": "true"
}
}
}
}

该查询结果如下:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0596458,
    "hits" : [
      {
        "_index" : "test_doc1",
        "_type" : "_doc",
        "_id" : "R9M9x4QB5b0eqQ47gc3d",
        "_score" : 1.0596458,
        "_source" : {
          "content" : "ny in usa"
        }
      },
      {
        "_index" : "test_doc1",
        "_type" : "_doc",
        "_id" : "SdM9x4QB5b0eqQ47gc3d",
        "_score" : 0.90630186,
        "_source" : {
          "content" : "new york in usa"
        }
      }
    ]
  }
}

因为nynew york的简称,所以文档1和3被查询出来了。现在修改auto_generate_synonyms_phrase_queryfalse,再次执行结果如下:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0596458,
    "hits" : [
      {
        "_index" : "test_doc1",
        "_type" : "_doc",
        "_id" : "R9M9x4QB5b0eqQ47gc3d",
        "_score" : 1.0596458,
        "_source" : {
          "content" : "ny in usa"
        }
      },
      {
        "_index" : "test_doc1",
        "_type" : "_doc",
        "_id" : "SNM9x4QB5b0eqQ47gc3d",
        "_score" : 0.90630186,
        "_source" : {
          "content" : "new time is york"
        }
      },
      {
        "_index" : "test_doc1",
        "_type" : "_doc",
        "_id" : "SdM9x4QB5b0eqQ47gc3d",
        "_score" : 0.90630186,
        "_source" : {
          "content" : "new york in usa"
        }
      }
    ]
  }
}

从结果可以发现文档2被查询出来了,这是因为文档2中同时包含了newyork这两个词。之前之所以没查出来是因为new york是执行了一个短语查询,这就要求这两个词必须是挨着的,中间不能带有别的词。

所以在实际业务开发中,该属性不推荐修改,修改之后可能导致召回率太高,但是搜索结果不太准确。

fuzziness

了解该参数前需要了解什么是模糊查询,例如我们在搜索java时不小心打成了jave,但是我们想搜索到java相关内容,此时就需要模糊查询了,我们只需要编辑一次即把e改成a就可以完成了。而fuzziness就是用来控制最大编辑距离的,默认情况下没有开启模糊查询,相当于fuzzuness=0

它的值可以是数字,例如012,注意只能是这三个数字,超过之后将不支持。同时还支持AUTO模式。AUTO模式的写法如下:

AUTO:lowDistance,highDistance

例如当fuzziness的值为AUTO:3,5时,它代表的意思如下:

  • 长度小于3的词可编辑次数为0次

  • 长度大于等于3且小于5时允许的编辑次数为1次

  • 长度大于等于5时可编辑次数为2次

例如下面的查询

GET doc/_search
{
"query": {
"match": {
"content": {
"query": "it",
"fuzziness": "AUTO:3,5"
}
}
}
}

该查询没有结果返回,因为it的长度为2所以编辑次数为0,所以无法找到含有is的文档内容。

GET doc/_search
{
"query": {
"match": {
"content": {
"query": "jave",
"fuzziness": "AUTO:3,5"
}
}
}
}

搜索jave时可以搜索出所有包含java的文档,因为jave只需要修改一次就可以变成java

max_expansions

查询扩展的最大字词数。该参数可能不太好理解,它的意思是我们使用fuzziness时可能会有很多词被匹配到。这个参数就是用来控制最大fuzziness匹配到的最大词数。可能语言表示不太好理解,直接看使用示例。

POST _bulk
{ "index" : { "_index" : "test_doc2"} }
{ "content":"abaa"}
{ "index" : { "_index" : "test_doc2"} }
{ "content":"abbb"}
{ "index" : { "_index" : "test_doc2"} }
{ "content":"abcc"}
{ "index" : { "_index" : "test_doc2"} }
{ "content":"abdd"}

上面我们构建了4个ab开头的单词,现在使用fuzziness参数进行查询,示例代码如下:

GET test_doc2/_search
{
"query": {
"match": {
"content": {
"query": "abss",
"fuzziness": 2
}
}
}
}

该查询可以将所有文档全部查出来,现在我们增加max_expansions为2的设置,查询如下:

GET test_doc2/_search
{
"query": {
"match": {
"content": {
"query": "abss",
"fuzziness": 2,
"max_expansions": 2
}
}
}
}

最后响应的结果如下:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.6019864,
    "hits" : [
      {
        "_index" : "test_doc2",
        "_type" : "_doc",
        "_id" : "utOiyIQB5b0eqQ47-dlO",
        "_score" : 0.6019864,
        "_source" : {
          "content" : "abaa"
        }
      },
      {
        "_index" : "test_doc2",
        "_type" : "_doc",
        "_id" : "u9OiyIQB5b0eqQ47-dlO",
        "_score" : 0.6019864,
        "_source" : {
          "content" : "abbb"
        }
      }
    ]
  }
}

原本未设置之前可以搜索到全部文档,现在只能搜索到2个,这就是max_expansions的作用。默认情况下该值为50,我这里的试验环境shard数量为1,如果在多shard的情况下,查询的结果并不能用来验证该值是否生效,因为每个shard中term可能不太一样。

prefix_length

模糊匹配的开头多少个字符数保持不变。例如我们在搜索java的时候,大多数情况下开头一般不会出错,出错的一般是后面的内容。这个参数就是用来确认开头多少个字不参与fuzziness的。例如下面示例:

GET test_doc2/_search
{
"query": {
"match": {
"content": {
"query": "aaaa",
"fuzziness": 2,
"prefix_length": 0
}
}
}
}

上面查询的结果可以找到如下文档:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.9029796,
    "hits" : [
      {
        "_index" : "test_doc2",
        "_type" : "_doc",
        "_id" : "utOiyIQB5b0eqQ47-dlO",
        "_score" : 0.9029796,
        "_source" : {
          "content" : "abaa"
        }
      }
    ]
  }
}

现在修改prefix_length=2,再次执行查询将找不到任何文档。

fuzzy_transpositions

该值是用来处理交换位置的。例如ab变成ba,如果按照编辑次数算法编辑次数为2,但是如果设置为true,就认为编辑次数为1。默认情况下,该值为true

GET test_doc2/_search
{
"query": {
"match": {
"content": {
"query": "baaa",
"fuzziness": 1
}
}
}
}

结果如下:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.9029796,
    "hits" : [
      {
        "_index" : "test_doc2",
        "_type" : "_doc",
        "_id" : "utOiyIQB5b0eqQ47-dlO",
        "_score" : 0.9029796,
        "_source" : {
          "content" : "abaa"
        }
      }
    ]
  }
}

如果修改fuzzy_transpositionsfalse,示例如下:

GET test_doc2/_search
{
"query": {
"match": {
"content": {
"query": "baaa",
"fuzziness": 1,
"fuzzy_transpositions": "false"
}
}
}
}

该查询将无法查找到abaa,因为此时编辑距离为2。

fuzzy_rewrite

用于重写查询方法。如果fuzziness参数不为0时,则使用默认方法top_terms_blended_freqs_${max_expansions}

lenient

该值默认为false,表示用来在查询时如果数据类型不匹配且无法转换时会报错。如果设置为true则会忽略报错。

operator

match query是一种Boolean类型的查询。例如在前面简单示例中的例子,查询java programming language,只要文档中包含javaprogramminglanguage中的任意一个都能被查询出来,相当于它们之间的关系为OR。现在我们通过设置operatorand示例如下:

GET doc/_search
{
"query": {
"match": {
"content": {
"query": "java programming language",
"operator": "and"
}
}
}
}

该查询结果中what is java?Go is programming language将无法被搜索到,因为他们并没有同时存在这三个词,相当于它们之间的关系为and

minimum_should_match

使用operator太绝对,它只能是全部匹配或者任意匹配一个。而minimum_should_match可以按比例或者固定个数匹配。

GET doc/_search
{
"query": {
"match": {
"content": {
"query": "java programming language",
"minimum_should_match": 2
}
}
}
}

上面示例的意思是,三个词只需要匹配到任意2个即可。所以最后就只有what is java?无法匹配。

zero_terms_query

如果分析器中有停用词处理,搜索文本中只存在停用词,最后分词结果为空。该选项就是用来处理这种情况的,默认情况下该值为none,意味着将没有任何结果返回。如果为all时将相当于match_all查询。

GET doc/_search
{
"query": {
"match": {
"content": {
"query": "is",
"analyzer": "stop",
"zero_terms_query": "none"
}
}
}
}

在英文中is就是我们所说的停用词,这里我们设置搜索时分析器为stop,执行查询结果发现任何文档都搜索不到。如果修改zero_terms_queryall,最后就相当于执行了match_all

cutoff_frequency

对于查询的词按出现频率可以分为低频词和高频次,通常情况下低频词更加重要,而高频词相对来说不是那么重要。例如在数据库相关的内容资料中,database该词可能在每个文档中出现,如果搜索database index文本内容,index匹配出来的文档会更加符合我们的预期条件。

上面所说的database在数据库相关文档中是一个高频词,但是在介绍编程语言的文档中,该词可能又是一个低频词。总结起来就是,高频词和低频词并不是绝对的,而是需要根据实际文档场景来决定。而我们这里的cutoff_frequency就是用来区分高频词和低频词的。在查询的时候,低频词会组成一个查询,而高频词只会用来评分,而不参与匹配过程。这种做的好处就是在查询速度上得到巨大提升。例如对于一个高频词,可能大多数文档都存在,这将导致大多数文档都被匹配出来,最后对大量文档进行相关性打分排序。如果在数据量特别大的情况下查询性能将会特别的糟糕。

cutoff_frequency可以设置一个0到1的小数来表示超过百分之多少则认为是高频词,也可以设置成一个大于或等于1的整型数字,用来表达在多少个文档中存在就表示是高频词。

POST _bulk
{ "index" : { "_index" : "test_doc3"} }
{ "content":"this is content"}
{ "index" : { "_index" : "test_doc3"} }
{ "content":"she is rose"}
{ "index" : { "_index" : "test_doc3"} }
{ "content":"he is jack"}
{ "index" : { "_index" : "test_doc3"} }
{ "content":"I like tom"}

上面是构建测试数据,现在我们查询is tom,并且将cutoff_frequency设置为2,表示在所有文档中出现次数找过2次就认为是高配词。

GET test_doc3/_search
{
"query": {
"match": {
"content": {
"query": "is tom",
"cutoff_frequency": 2
}
}
}
}

最后运行结果如下:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.2039728,
    "hits" : [
      {
        "_index" : "test_doc3",
        "_type" : "_doc",
        "_id" : "2dP7yIQB5b0eqQ47Odyq",
        "_score" : 1.2039728,
        "_source" : {
          "content" : "I like tom"
        }
      }
    ]
  }
}

从结果可以看出,is并没有起到搜索查询的作用,这是因为is在三个文档中出现过,而cutoff_frequency为2,所以只会查询含有tom内容的文档。


ES分析器

ES近义词搜索


原文始发于微信公众号(一只菜鸟程序员):ES全文搜索-match query

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72811.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!