zoukankan      html  css  js  c++  java
  • ElasticSearch深入搜索

    一、 结构化搜索

    结构化搜索(Structured search) 是指有关探询那些具有内在结构数据的过程。比如日期、时间和数字都是结构化的:它们有精确的格式,我们可以对这些格式进行逻辑操作。比较常见的操作包括比较数字或时间的范围,或判定两个值的大小。

    在结构化查询中,我们得到的结果 总是 非是即否,要么存于集合之中,要么存在集合之外。结构化查询不关心文件的相关度或评分;它简单的对文档包括或排除处理。

    1、精确值查找

    当进行精确值查找时, 我们会使用过滤器(filters)。过滤器很重要,因为它们执行速度非常快,不会计算相关度(直接跳过了整个评分阶段)而且很容易被缓存。请尽可能多的使用过滤式查询。

    term 查询数字

    我们首先来看最为常用的 term 查询, 可以用它处理数字(numbers)、布尔值(Booleans)、日期(dates)以及文本(text)

    首先,让我们以下面的例子开始介绍,创建并索引一些表示产品的文档,文档里有字段 `price` 和 `productID` ( `价格` 和 `产品ID` ):

    POST /my_store/products/_bulk
    { "index": { "_id": 1 }}
    { "price" : 10, "productID" : "XHDK-A-1293-#fJ3" }
    { "index": { "_id": 2 }}
    { "price" : 20, "productID" : "KDKE-B-9947-#kL5" }
    { "index": { "_id": 3 }}
    { "price" : 30, "productID" : "JODL-X-1937-#pV7" }
    { "index": { "_id": 4 }}
    { "price" : 30, "productID" : "QQPX-R-3956-#aD8" }

    在 Elasticsearch 的查询表达式(query DSL)中,我们可以使用 term 查询达到相同的目的。 term 查询会查找我们指定的精确值。作为其本身, term 查询是简单的。它接受一个字段名以及我们希望查找的数值:

    {
        "term" : {
            "price" : 20
        }
    }

    通常当查找一个精确值的时候,我们不希望对查询进行评分计算。只希望对文档进行包括或排除的计算,所以我们会使用 constant_score 查询以非评分模式来执行 term 查询并以一作为统一评分。

    最终组合的结果是一个 constant_score 查询,它包含一个 term 查询:

    GET /my_store/products/_search
    {
        "query" : {
            "constant_score" : { 
                "filter" : {
                    "term" : { 
                        "price" : 20
                    }
                }
            }
        }
    }

    注:term查询文本时,要想查询其精确值,要将其设置成 not_analyzed 无需分析的属性。

    2、布尔过滤器

    一个 bool 过滤器由三部分组成:

    {
       "bool" : {
          "must" :     [],
          "should" :   [],
          "must_not" : [],
       }
    }

    must

      所有的语句都 必须(must) 匹配,与 AND 等价。

    must_not

      所有的语句都 不能(must not) 匹配,与 NOT 等价。

    should

      至少有一个语句要匹配,与 OR 等价

    下面例子是查询price为20或productID为XHDK-A-1293-#fJ3,且price不等于30的查询,

    用SQL语句表示如下:

    SELECT product
    FROM   products
    WHERE  (price = 20 OR productID = "XHDK-A-1293-#fJ3")
      AND  (price != 30)

    用 Elasticsearch 语句如下:

    GET /my_store/products/_search
    {
       "query" : {
          "filtered" : { 
             "filter" : {
                "bool" : {
                  "should" : [
                     { "term" : {"price" : 20}}, 
                     { "term" : {"productID" : "XHDK-A-1293-#fJ3"}} 
                  ],
                  "must_not" : {
                     "term" : {"price" : 30} 
                  }
               }
             }
          }
       }
    }

     3、嵌套布尔过滤器

    对于以下这个 SQL 语句:

    SELECT document
    FROM   products
    WHERE  productID      = "KDKE-B-9947-#kL5"
      OR (     productID = "JODL-X-1937-#pV7"
           AND price     = 30 )

    我们将其转换成一组嵌套的 bool 过滤器:

    GET /my_store/products/_search
    {
       "query" : {
          "filtered" : {
             "filter" : {
                "bool" : {
                  "should" : [
                    { "term" : {"productID" : "KDKE-B-9947-#kL5"}}, 
                    { "bool" : { 
                      "must" : [
                        { "term" : {"productID" : "JODL-X-1937-#pV7"}}, 
                        { "term" : {"price" : 30}} 
                      ]
                    }}
                  ]
               }
             }
          }
       }
    }

     、全文索引

    全文搜索两个最重要的方面是:

    相关性(Relevance)
    它是评价查询与其结果间的相关程度,并根据这种相关程度对结果排名的一种能力,这种计算方式可以是 TF/IDF 方法(参见 相关性的介绍)、地理位置邻近、模糊相似,或其他的某些算法。
    分析(Analysis)
      它是将文本块转换为有区别的、规范化的 token 的一个过程,(参见 分析的介绍) 目的是为了(a)创建倒排索引以及(b)查询倒排索引
    1、基于词项与基于全文

    所有查询会或多或少的执行相关度计算,但不是所有查询都有分析阶段。 和一些特殊的完全不会对文本进行操作的查询(如 bool 或 function_score )不同,文本查询可以划分成两大家族:

    基于词项的查询

      如 term 或 fuzzy 这样的底层查询不需要分析阶段,它们对单个词项进行操作。用 term 查询词项Foo 只要在倒排索引中查找 准确词项 ,并且用 TF/IDF 算法为每个包含该词项的文档计算相关度评分_score 。

      term 查询只对倒排索引的词项精确匹配,这点很重要,它不会对词的多样性进行处理(如, foo或 FOO )。这里,无须考虑词项是如何存入索引的。如果是将 ["Foo","Bar"] 索引存入一个不分析的( not_analyzed )包含精确值的字段。

    基于全文的查询

    像 match 或 query_string 这样的查询是高层查询,它们了解字段映射的信息:

    • 如果查询 日期(date) 或 整数(integer) 字段,它们会将查询字符串分别作为日期或整数对待。
    • 如果查询一个( not_analyzed )未分析的精确值字符串字段, 它们会将整个查询字符串作为单个词项对待。
    • 但如果要查询一个( analyzed )已分析的全文字段, 它们会先将查询字符串传递到一个合适的分析器,然后生成一个供查询的词项列表。

    一旦组成了词项列表,这个查询会对每个词项逐一执行底层的查询,再将结果合并,然后为每个文档生成一个最终的相关度评分。

     2、查询语句提升权重

    假设想要查询关于 “full-text search(全文搜索)” 的文档, 但我们希望为提及 “Elasticsearch” 或 “Lucene” 的文档给予更高的 权重 ,这里 更高权重 是指如果文档中出现 “Elasticsearch” 或 “Lucene” ,它们会比没有的出现这些词的文档获得更高的相关度评分 _score ,也就是说,它们会出现在结果集的更上面。

    一个简单的 bool 查询 允许我们写出如下这种非常复杂的逻辑:content 字段必须包含 full 、 text 和 search 所有三个词。如果 content 字段也包含 Elasticsearch 或 Lucene ,文档会获得更高的评分 _score

    GET /_search
    {
        "query": {
            "bool": {
                "must": {
                    "match": {
                        "content": { 
                            "query":    "full text search",
                            "operator": "and"
                        }
                    }
                },
                "should": [ 
                    { "match": { "content": "Elasticsearch" }},
                    { "match": { "content": "Lucene"        }}
                ]
            }
        }
    }

     但是如果我们想让包含 Lucene和 Elasticsearch的有更高的权重,并且包含 Elasticsearch 的语句比 Lucene 的权重更高,该如何处理?

    我们可以通过指定 boost 来控制任何查询语句的相对的权重, boost 的默认值为 1 ,大于 1 会提升一个语句的相对权重。所以下面重写之前的查询:

    GET /_search
    {
        "query": {
            "bool": {
                "must": {
                    "match": {  
                        "content": {
                            "query":    "full text search",
                            "operator": "and"
                        }
                    }
                },
                "should": [
                    { "match": {
                        "content": {
                            "query": "Elasticsearch",
                            "boost": 3 
                        }
                    }},
                    { "match": {
                        "content": {
                            "query": "Lucene",
                            "boost": 2 
                        }
                    }}
                ]
            }
        }
    }

    boost 参数被用来提升一个语句的相对权重( boost 值大于 1 )或降低相对权重( boost值处于 0 到 1 之间),但是这种提升或降低并不是线性的,换句话说,如果一个 boost 值为 2 ,并不能获得两倍的评分 _score 。

     3、多字符串查询评分计算方式

     如果我们知道 War and Peace 是标题,Leo Tolstoy 是作者,translator是译者,语句如下:

    GET /_search
    {
      "query": {
        "bool": {
          "should": [
            { "match": { "title":  "War and Peace" }},
            { "match": { "author": "Leo Tolstoy"   }},
            { "bool":  {
              "should": [
                { "match": { "translator": "Constance Garnett" }},
                { "match": { "translator": "Louise Maude"      }}
              ]
            }}
          ]
        }
      }
    }

    为什么将译者条件语句放入另一个独立的 bool 查询中呢?所有的四个 match 查询都是 should 语句,所以为什么不将 translator 语句与其他如 title 、 author 这样的语句放在同一层呢?

    答案在于评分的计算方式。 bool 查询运行每个 match 查询,再把评分加在一起,然后将结果与所有匹配的语句数量相乘,最后除以所有的语句数量。处于同一层的每条语句具有相同的权重。在上面这个例子中,包含 translator 语句的 bool 查询,只占总评分的三分之一。如果将 translator 语句与 title 和 author 两条语句放入同一层,那么 title 和 author 语句只贡献四分之一评分。

     三、多字段搜索

    1、单字符查询

    bool 查询是多语句查询的主干。 它的适用场景很多,特别是当需要将不同查询字符串映射到不同字段的时候。

    问题在于,目前有些用户期望将所有的搜索项堆积到单个字段中,并期望应用程序能为他们提供正确的结果。有意思的是多字段搜索的表单通常被称为 高级查询 (Advanced Search) —— 只是因为它对用户而言是高级的,而多字段搜索的实现却非常简单。

    对于多词(multiword)、多字段(multifield)查询来说,不存在简单的 万能 方案。为了获得最好结果,需要 了解我们的数据 ,并了解如何使用合适的工具。

    当用户输入了单个字符串查询的时候,通常会遇到以下三种情形:

    最佳字段
    当搜索词语具体概念的时候,比如 “brown fox” ,词组比各自独立的单词更有意义。像 title 和 body 这样的字段,尽管它们之间是相关的,但同时又彼此相互竞争。文档在 相同字段 中包含的词越多越好,评分也来自于 最匹配字段 。
    多数字段

    为了对相关度进行微调,常用的一个技术就是将相同的数据索引到不同的字段,它们各自具有独立的分析链。

    主字段可能包括它们的词源、同义词以及 变音词 或口音词,被用来匹配尽可能多的文档。

    相同的文本被索引到其他字段,以提供更精确的匹配。一个字段可以包括未经词干提取过的原词,另一个字段包括其他词源、口音,还有一个字段可以提供 词语相似性 信息的瓦片词(shingles)。

    其他字段是作为匹配每个文档时提高相关度评分的 信号 , 匹配字段越多 则越好。

    混合字段

    对于某些实体,我们需要在多个字段中确定其信息,单个字段都只能作为整体的一部分:

    • Person: first_name 和 last_name (人:名和姓)
    • Book: title 、 author 和 description (书:标题、作者、描述)
    • Address: street 、 city 、 country 和 postcode (地址:街道、市、国家和邮政编码)

    在这种情况下,我们希望在 任何 这些列出的字段中找到尽可能多的词,这有如在一个大字段中进行搜索,这个大字段包括了所有列出的字段。

    上述所有都是多词、多字段查询,但每个具体查询都要求使用不同策略。本章后面的部分,我们会依次介绍每个策略。

     1、最佳字段

    假设有个网站允许用户搜索博客的内容, 以下面两篇博客内容文档为例:

    PUT /my_index/my_type/1
    {
        "title": "Quick brown rabbits",
        "body":  "Brown rabbits are commonly seen."
    }
    
    PUT /my_index/my_type/2
    {
        "title": "Keeping pets healthy",
        "body":  "My quick brown fox eats rabbits on a regular basis."
    }

    用户输入词组 “Brown fox” 然后点击搜索按钮。事先,我们并不知道用户的搜索项是会在 title 还是在body 字段中被找到,但是,用户很有可能是想搜索相关的词组。用肉眼判断,文档 2 的匹配度更高,因为它同时包括要查找的两个词:

    现在运行以下 bool 查询:

    {
        "query": {
            "bool": {
                "should": [
                    { "match": { "title": "Brown fox" }},
                    { "match": { "body":  "Brown fox" }}
                ]
            }
        }
    }

    但是我们发现查询的结果是文档 1 的评分更高:

    {
      "hits": [
         {
            "_id":      "1",
            "_score":   0.14809652,
            "_source": {
               "title": "Quick brown rabbits",
               "body":  "Brown rabbits are commonly seen."
            }
         },
         {
            "_id":      "2",
            "_score":   0.09256032,
            "_source": {
               "title": "Keeping pets healthy",
               "body":  "My quick brown fox eats rabbits on a regular basis."
            }
         }
      ]
    }

    为了理解导致这样的原因, 需要回想一下 bool 是如何计算评分的:

    1. 它会执行 should 语句中的两个查询。
    2. 加和两个查询的评分。
    3. 乘以匹配语句的总数。
    4. 除以所有语句总数(这里为:2)。

    文档 1 的两个字段都包含 brown 这个词,所以两个 match 语句都能成功匹配并且有一个评分。文档 2 的body 字段同时包含 brown 和 fox 这两个词,但 title 字段没有包含任何词。这样, body 查询结果中的高分,加上 title 查询中的 0 分,然后乘以二分之一,就得到比文档 1 更低的整体评分。

    在本例中, title 和 body 字段是相互竞争的关系,所以就需要找到单个 最佳匹配 的字段。

    如果不是简单将每个字段的评分结果加在一起,而是将 最佳匹配 字段的评分作为查询的整体评分,结果会怎样?这样返回的结果可能是: 同时 包含 brown 和 fox 的单个字段比反复出现相同词语的多个不同字段有更高的相关度。

    dis_max 查询

    不使用 bool 查询,可以使用 dis_max 即分离 最大化查询(Disjunction Max Query) 。分离(Disjunction)的意思是 或(or) ,这与可以把结合(conjunction)理解成 与(and) 相对应。分离最大化查询(Disjunction Max Query)指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回 :

    {
        "query": {
            "dis_max": {
                "queries": [
                    { "match": { "title": "Brown fox" }},
                    { "match": { "body":  "Brown fox" }}
                ]
            }
        }
    }

    得到我们想要的结果为:

    {
      "hits": [
         {
            "_id":      "2",
            "_score":   0.21509302,
            "_source": {
               "title": "Keeping pets healthy",
               "body":  "My quick brown fox eats rabbits on a regular basis."
            }
         },
         {
            "_id":      "1",
            "_score":   0.12713557,
            "_source": {
               "title": "Quick brown rabbits",
               "body":  "Brown rabbits are commonly seen."
            }
         }
      ]
    }

     2、多数字段

    全文搜索被称作是 召回率(Recall) 与 精确率(Precision) 的战场: 召回率 ——返回所有的相关文档;精确率 ——不返回无关文档。目的是在结果的第一页中为用户呈现最为相关的文档。

    为了提高召回率的效果,我们扩大搜索范围 ——不仅返回与用户搜索词精确匹配的文档,还会返回我们认为与查询相关的所有文档。如果一个用户搜索 “quick brown box” ,一个包含词语 fast foxes 的文档被认为是非常合理的返回结果。

    提高全文相关性精度的常用方式是为同一文本建立多种方式的索引, 每种方式都提供了一个不同的相关度信号 signal 。主字段会以尽可能多的形式的去匹配尽可能多的文档。举个例子,我们可以进行以下操作:

    • 使用词干提取来索引 jumps 、 jumping 和 jumped 样的词,将 jump 作为它们的词根形式。这样即使用户搜索 jumped ,也还是能找到包含 jumping 的匹配的文档。
    • 将同义词包括其中,如 jump 、 leap 和 hop 。
    • 移除变音或口音词:如 ésta 、 está 和 esta 都会以无变音形式 esta 来索引。

    尽管如此,如果我们有两个文档,其中一个包含词 jumped ,另一个包含词 jumping ,用户很可能期望前者能排的更高,因为它正好与输入的搜索条件一致。

    为了达到目的,我们可以将相同的文本索引到其他字段从而提供更为精确的匹配。一个字段可能是为词干未提取过的版本,另一个字段可能是变音过的原始词,第三个可能使用 shingles 提供 词语相似性 信息。这些附加的字段可以看成提高每个文档的相关度评分的信号 signals ,能匹配字段的越多越好。

    一个文档如果与广度匹配的主字段相匹配,那么它会出现在结果列表中。如果文档同时又与 signal 信号字段匹配,那么它会获得额外加分,系统会提升它在结果列表中的位置。

    跨字段实体搜索

    现在讨论一种普遍的搜索模式:跨字段实体搜索(cross-fields entity search)。 在如 person 、 product或 address (人、产品或地址)这样的实体中,需要使用多个字段来唯一标识它的信息。 person 实体可能是这样索引的:

    {
        "firstname":  "Peter",
        "lastname":   "Smith"
    }

    或地址:

    {
        "street":   "5 Poland Street",
        "city":     "London",
        "country":  "United Kingdom",
        "postcode": "W1V 3DG"
    }

    我们的用户可能想搜索 “Peter Smith” 这个人,或 “Poland Street W1V” 这个地址,这些词出现在不同的字段中,所以如果使用 dis_max 或 best_fields 查询去查找 单个 最佳匹配字段显然是个错误的方式。

    简单的方式

    依次查询每个字段并将每个字段的匹配评分结果相加,听起来真像是 bool 查询:

    {
      "query": {
        "bool": {
          "should": [
            { "match": { "street":    "Poland Street W1V" }},
            { "match": { "city":      "Poland Street W1V" }},
            { "match": { "country":   "Poland Street W1V" }},
            { "match": { "postcode":  "Poland Street W1V" }}
          ]
        }
      }
    }

    为每个字段重复查询字符串会使查询瞬间变得冗长,可以采用 multi_match 查询, 将 type 设置成most_fields 然后告诉 Elasticsearch 合并所有匹配字段的评分:

    {
      "query": {
        "multi_match": {
          "query":       "Poland Street W1V",
          "type":        "most_fields",
          "fields":      [ "street", "city", "country", "postcode" ]
        }
      }
    }

    most_fields 方式的问题

    用 most_fields 这种方式搜索也存在某些问题,这些问题并不会马上显现:

    • 它是为多数字段匹配 任意 词设计的,而不是在 所有字段 中找到最匹配的。
    • 它不能使用 operator 或 minimum_should_match 参数来降低次相关结果造成的长尾效应。
    • 词频对于每个字段是不一样的,而且它们之间的相互影响会导致不好的排序结果。

    四、近似匹配

    1、短语匹配

    什么是短语

    一个被认定为和短语 quick brown fox 匹配的文档,必须满足以下这些要求:

    • quick 、 brown 和 fox 需要全部出现在域中。
    • brown 的位置应该比 quick 的位置大 1 。
    • fox 的位置应该比 quick 的位置大 2 。

    如果以上任何一个选项不成立,则该文档不能认定为匹配。

    像 match 查询对于标准全文检索是一种最常用的查询一样,当你想找到彼此邻近搜索词的查询方法时,就会想到 match_phrase 查询:

    GET /my_index/my_type/_search
    {
        "query": {
            "match_phrase": {
                "title": "quick brown fox"
            }
        }
    }

     2、越近越好

    鉴于一个短语查询仅仅排除了不包含确切查询短语的文档, 而 邻近查询 — 一个 slop 大于 0— 的短语查询将查询词条的邻近度考虑到最终相关度 _score 中。 通过设置一个像 50 或者 100 这样的高 slop 值, 你能够排除单词距离太远的文档, 但是也给予了那些单词临近的的文档更高的分数。

    下列对 quick dog 的邻近查询匹配了同时包含 quick 和 dog 的文档, 但是也给了与 quick 和 dog 更加临近的文档更高的分数 

    POST /my_index/my_type/_search
    {
       "query": {
          "match_phrase": {
             "title": {
                "query": "quick dog",
                "slop":  50 
             }
          }
       }
    }

     注意高 slop 值:分数较高因为 quick 和 dog 很接近,分数较低因为 quick 和 dog 分开较远

    {
      "hits": [
         {
            "_id":      "3",
            "_score":   0.75, 
            "_source": {
               "title": "The quick brown fox jumps over the quick dog"
            }
         },
         {
            "_id":      "2",
            "_score":   0.28347334, 
            "_source": {
               "title": "The quick brown fox jumps over the lazy dog"
            }
         }
      ]
    }

    3、使用邻近度提高相关度

    我们可以将一个简单的 match 查询作为一个 must 子句。 这个查询将决定哪些文档需要被包含到结果集中。 我们可以用 minimum_should_match 参数去除长尾。 然后我们可以以 should 子句的形式添加更多特定查询。 每一个匹配成功的都会增加匹配文档的相关度。

    GET /my_index/my_type/_search
    {
      "query": {
        "bool": {
          "must": {
            "match": { 
              "title": {
                "query":                "quick brown fox",
                "minimum_should_match": "30%"
              }
            }
          },
          "should": {
            "match_phrase": { 
              "title": {
                "query": "quick brown fox",
                "slop":  50
              }
            }
          }
        }
      }
    }

    must 子句从结果集中包含或者排除文档。 should 子句增加了匹配到文档的相关度评分。

     

  • 相关阅读:
    node.js+mysql接口入门
    input边写边验证?正则表达式写在属性里?小技巧
    创建vue,react项目
    jquery在网页中加载本地json文件
    OpenFeigin服务接口调用
    Ribbon负载均衡服务调用
    Consul服务注册与发现
    Eureka服务注册与发现
    springboot项目在idea实现热部署
    设计模式——单例模式
  • 原文地址:https://www.cnblogs.com/liuzhongchao/p/8989870.html
Copyright © 2011-2022 走看看