说说Elasticsearch那点事

几个搜索的相关话题

2020-03-29  本文已影响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"
    }
  }
}
上一篇下一篇

猜你喜欢

热点阅读