几个搜索的相关话题

作者: 饿虎嗷呜 | 来源:发表于2020-03-29 17:06 被阅读0次

    结构化搜索与全文搜索

    ES在搜索时有两种类型,即全文搜索与结构化搜索。其相对应于"term"系列的查询 和 "match"系列的查询。

    这两种类型的查询的区别在于,使用match查询时,ES会对输入的字符串先进行分词,然后进行查询,而term查询不会进行分词。

    在ES中,分词对应于Analyzer这个功能,有很多内置的分词器,同时用户也可以自定义分词器。一个完整的分词器会包含3个部分:

    charactor filter: 对文本进行预处理,比如去除html标签之类的工作,会影响Tokenizer的pos和offset信息。

    tokenizer:对文本进行切分,划分成一个一个词。

    token filter:对切分出来的结果进行过滤,过滤掉停用词等。

    用户可以在设置索引配置时,为索引设置自定义analyzer,然后为索引的字段设置上这个自定义的analyzer。

    PUT products
    {
      "settings": {
        "number_of_shards": 1,
        "analysis": {
          "analyzer": {
            "my_analyzer": {
              "type": "custom",
              "tokenizer": "standard",
              "filter": [
                "lowercase"
              ]
            }
          }
        }
      },
      "mappings": {
        "properties": {
          "my_text": {
            "type": "text",
            "analyzer": "my_analyzer"
          }
        }
      }
    }
    

    不仅仅analyzer可以自定义,analyzer的3个组成部分char_filtertokenizer, filter,用户都可以自定义。

    由于文档在写入时,构造倒排索引会对原文进行转小写,去除停用词,加入同义词,变换时态等操作,因此如果在查询时对text类型的字段使用term搜索,可能得不到想要的结果。

    PUT /my_book/_doc/1
    {
      "title": "Hello, World"
    }
    
    GET /my_book/_search
    {
      "query": {
        "term": {
          "title": {
            "value": "Hello, World"
          }
        }
      }
    }
    
    {
      "took" : 1,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 0,
          "relation" : "eq"
        },
        "max_score" : null,
        "hits" : [ ]
      }
    }
    

    上面这个查询,由于使用standard分词器,对文本进行了分词,对文本进行了小写处理。在搜索时,搜索大写的”Hello,World!“是搜索不到结果的。如果想搜到结果由两种方法。

    在动态mapping中,ES会为text类型的字段添加一个keyword类型的子字段:

    GET /my_book/_mapping
    
    {
      "my_book" : {
        "mappings" : {
          "properties" : {
            "title" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            }
          }
        }
      }
    }
    
    

    我看可以针对这个keyword类型的子字段进行'term'查询,就可以获得我们想要的结果。

    另外一种方法,我们可以使用全文搜索的方式,使用这种方式,在search发生时,ES对输入text类型同样会做分词转换,这样我们就可以搜索到相关的结果了。

    GET /my_book/_search
    {
      "query": {
        "term": {
          "title.keyword": {
            "value": "Hello, World"
          }
        }
      }
    }
    
    GET /my_book/_search
    {
      "query": {
        "match": {
          "title": "Hello, World"
        }
      }
    }
    
    

    如上两种搜索方式,都可以获得结果。

    TF-IDF 算法与相关性算分

    ES在5.x版本之前使用的是相关性算分算法是TF-IDF。

    TF,是Term Frequency的缩写,代指词频。算法是用该词在一篇文档中出现的次数除以该文档的总词数。

    tf = num_of_searched_terms/num_of_terms_in_doc
    

    根据这个公式来看,这个词在一篇文档中出现频率越高,其词频即TF就越高

    IDF,是Inverse Document Frequency,代表的是该词在所有文档中的频率。算法是

    idf = log(总文档数/包含该词的文档数)
    

    可以看到,包含改词的文档数越大,总文档数/包含该词的文档数的取值约接近1,idf取值约接近于0。

    TF-IDF 就是将TF和IDF进行了加权和。

    比如说我们进行如下搜索:

    GET article/_search
    {
      "query": {
        "match": {
          "title": "beautiful world"
        }
      }
    }
    

    搜索时会将搜索项,"beautiful world"拆分成beautiful和world两个词,对搜索的每篇文档都会对这两个词进行相关性算分,然后将其相加,得到对应文档的相关性算分(tf(beautiful)*idf(beautiful) + tf(world)*idf(world))。

    Lucene中的tf-idf简化版

    score(q,d) = coord(q,d) * queryNorm(q) * cumulate(tf(t in d) * idf(t)^2 * boost(t) * norm(t,d))
    

    其中boost指的是,在搜索时我们可以为某个term提高其算分。norm(t,d)则是表示某个文档长度越短,其贡献的算分越高。

    这个相关性算分会存储在返回结果的_score这个字段里面,并以此进行结果排序。需要注意的是,如果在搜索过程中,指定了排序,那么返回结果不会包含算分。即"_score" : null

    在现在的版本中(5.x之后),默认的相关性算法改成了BM25,与TF-IDF相比,该算法会在一个最高值处收敛,而不是TD-IDF算法的发散式结果。

    匹配一个,匹配两个,顺序匹配

    上文我们已经讨论过,在进行全文搜索时,ES会对搜索字段进行分词,针对分词分别在每个文档计算tf和idf,然后进行累加获得一个算分。然后根据算分对结果进行排序。这样的分析逻辑就会带来一个结果。比如,我搜索”Hello World",ES会分别正对“Hello”和“World”进行算分。如果文档中只含有“hello”或者“world”,也会进入到搜索的结果中,只是算分要低一些。

    如果想获得匹配两个词项的结果,我们可以通过为match设置参数来达成。

    GET /my_book/_search
    {
      "query": {
        "match": {
          "title": {
            "query": "Hello, World",
            "operator": "and"
          }
        }
      }
    }
    

    比如说,match的默认operator其实是“or”,只要命中词项中的任意一个就算命中。我们可以在搜索时显式地把参数“operator”设为“and”,这样只有搜索结果中包含所有词项的情况下,结果才算命中。

    另外一个方法是设置最小命中词项数。

    GET /my_book/_search
    {
      "query": {
        "match": {
          "title": {
            "query": "Hello, World",
            "minimum_should_match": 2
          }
        }
      }
    }
    

    但是这样还是无法保证返回结果的顺序,”hello world“和”world hello“会拥有相同的算分。如果要保证匹配的顺序,需要使用match_phrase搜索

    GET /my_book/_search
    {
      "query": {
        "match_phrase": {
          "title": {
            "query": "Hello World"
          }
        }
      }
    }
    

    不过由于进行了分词,”Hello, World“与”Hello World“以及”hello world“都会有相同的算分。这个时候可以自定义分词器,或者使用term search来实现。

    PUT my_book/_doc/1
    {
        "title": "Hello, My Girl",
        "body":  "This World is Beautiful"
    }
    
    PUT my_book/_doc/2
    {
        "title": "hahaha, It is very delicious",
        "body":  "Hello, World!"
    }
    
    GET my_book/_search
    {
      "query": {
        "bool": {
          "should": [
            {
              "match": {
                "title": "hello, World"
              }
            },
            {
              "match": {
                "body": "hello, World"
              }
            }
          ]
        }
      }
    }
    

    以上面为例,由于should查询的算分是每个field分别算分再相加。因此虽然2号文档有更符合的组合。但是返回结果算分最高的是第一个结果。

    这种情况,可以使用"dis_max"搜索来解决。

    GET my_book/_search
    {
      "query": {
        "dis_max": {
            "tie_breaker": 0, 
          "queries": [
            {
              "match": {
                "title": "hello, World"
              }
            },
            {
              "match": {
                "body": "hello, World"
              }
            }
            ]
        }
      }
    }
    

    dismax搜索主要会依靠最佳匹配的结果,对其他结果会使用一个明明tie_breaker的参数,调整期取值,该参数默认为0。

    最佳字段,多数字段和混合字段

    上一节的情况同样可以使用multi-match的方法来实现,下面这段和上文dis_max的搜索方式等价:

    GET my_book/_search
    {
      "query": {
        "multi_match": {
          "type": "best_fields", 
          "query": "hello, World",
          "tie_breaker": 0,
          "fields": ["title", "body"]
        }
      }
    }
    

    其中需要注意的是type字段,该字段默认值为"best_fields",即搜索结果以最佳匹配的field结果为主,其他fields上的算分通过tie_breaker进行控制。

    这个字段另外还可以取值”most_fields“和”cross_fields“,

    "most_fields"的效果和上面的bool 查询的效果相似,会对所有fields上面的算分进行累加得到一个结果作为算分。

    而"cross_fields"则可以设置一个"operator: "and"",这样只有所有词项都出现的结果才会返回。与上面match搜索中设置”operator“的效果是一致的。

    GET my_book/_search
    {
      "query": {
        "multi_match": {
          "type": "cross_fields", 
          "query": "hahaha world",
          "operator": "and", 
          "fields": ["title", "body"]
        }
      }
    }
    

    比如这个搜索,最终只会返回一个结果:

        "hits" : [
          {
            "_index" : "my_book",
            "_type" : "_doc",
            "_id" : "2",
            "_score" : 0.83994377,
            "_source" : {
              "title" : "hahaha, It is very delicious",
              "body" : "Hello, World!"
            }
          }
        ]
    

    query_string与simple_query_string

    string_query实际上和URI query的功能是类似的,比如上文的搜索就可以写成:

    GET my_book/_search
    {
      "query": {
        "query_string": {
          "fields": ["title", "body"],
          "query": "hahaha AND world"
        }
      }
    }
    

    可以指定要搜索的字段,和内容的组合。

    simple_query_string功能类似,但是不支持在query中使用"AND OR NOT",但是可以使用:+代表AND,|代表OR,-代表NOT,这三个符合在query_string中同样可用。同时会忽略错误的语法。

    GET my_book/_search
    {
      "query": {
        "simple_query_string": {
          "fields": ["title", "body"],
          "query": "-hahaha +world",
          "default_operator": "AND"
        }
      }
    }
    

    相关文章

      网友评论

        本文标题:几个搜索的相关话题

        本文链接:https://www.haomeiwen.com/subject/bqeuuhtx.html