跨字段查询

在ES查询DSL中,我们了解了multi_match,知道它的查询方式有best_fieldsmost_fields两种以及基于这两种的扩展。most_fields致力于返回所有的相关文档,而best_fields则致力于返回尽可能精确的文档。他们会为每个字段都生成一个查询,而best_fields取子查询中算分最高的最为最终算分,most_fields则取所有子查询的算分的和做为最终算分。这两种方式因为都是以字段为中心的,我们可以叫做以字段为中心的查询方式。详细参考ElasticSearch查询DSL之全文检索中multi_match一节对于这两种方式的介绍以及ElasticSearch查询DSL之组合查询中对于dis_max的介绍。

今天这里我们在看这样一种场景,假设我们有一个索引,存储的是地址信息,其数据如下。我们发现城市、省份、街道等信息都散落在不同的字段,一个完整的地址需要用多个字段来唯一标识。

"hits" : {
    "total" : {
      "value" : 5,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "address",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "provice" : "Gansu",
          "city" : "Lanzhou",
          "street" : "Anning"
        }
      },
      {
        "_index" : "address",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "provice" : "Gansu",
          "city" : "Lanzhou",
          "street" : "Chenguan"
        }
      },
      {
        "_index" : "address",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "provice" : "Gansu",
          "city" : "Lanzhou",
          "street" : "Qilihe"
        }
      },
      {
        "_index" : "address",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "provice" : "Shanxi",
          "city" : "Xian",
          "street" : "Yanta"
        }
      },
      {
        "_index" : "address",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 1.0,
        "_source" : {
          "provice" : "Shanxi",
          "city" : "Anning",
          "street" : "Anning"
        }
      }
    ]
  }

那么对于这种场景,我们通过most_fields方式查询看看有什么结果?我们发现ID为1的这条数据本应该是最符合条件的,但是他的算分反而低,被排在了ID为5的数据的后面,而ID为5的数据因为Anning这个单词出现了两次,被排在了前面。还有一个问题是,这里我是想找Gansu Lanzhou Anning这个确定的地址,要么找到,要么找不到,但是他却给我们返回了一堆无用的文档,我们把这个叫做长尾效应

POST address/_search
{
  "query": {
    "multi_match": {
      "query": "Gansu Lanzhou Anning",
      "type": "most_fields",
      "fields": ["provice", "city", "street"]
    }
  }
}

"hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 2.261763,
    "hits" : [
      {
        "_index" : "address",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 2.261763,
        "_source" : {
          "provice" : "Shanxi",
          "city" : "Anning",
          "street" : "Anning"
        }
      },
      {
        "_index" : "address",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.9534616,
        "_source" : {
          "provice" : "Gansu",
          "city" : "Lanzhou",
          "street" : "Anning"
        }
      },
      {
          // ... 这里还有其他结果,为了空间不罗列了
      }
    ]
  }

因此,通过这个例子,我们发现这个需求用most_fields这种方法解决的弊端,那我们一起来找找原因。

  1. ID为5的文档之所以排在前面,是因为它是以字段为中心的查询方式,他在查询的时候会为每个字段生成一个子查询,最终将算分加起来作为总的算分,ID为5的文档中,关键字anning出现了两次,因此算分最高,排列最靠前。
  2. 存在长尾效应。对于这个问题,我们可能会想到通过设置operateand或者限制minimum_should_match来保证尽可能的精确,但是and操作符要求所有的词都必须要出现在同一个字段,如果我们用了它,会导致什么也查不到。
  3. 除了上述问题,还有另一个问题,词频的问题。在算分的时候,ES是有一个公式的,这个公式中,有两个很重的变量TFIDFTF叫做词频,一个文档在某一个文章中出现的频率越高,那么相关度就越高。但是像is这种词本身出现的频率就很高,我们能说明他相关度高吗?答案是不能,那么这个时候有了另一个概念:IDF叫做逆向文档频率,如果一个词在所有文档中出现的频率越高,那么这个词的相关度就越低。举个例子,比如北京这个地名可能经常出现,所以导致他有一个很高的IDF指,而环县也作为一个地名,却有着比较低的IDF值。这样最终可能导致在算分的时候出现一些意料之外的结果。

存在这些问题的根源就是因为我们将数据分散在多个字段,那么如果将他们组合成单个字段,问题自然而然就不存在了,比如下面这样的格式。但是这样会带来的问题是,数据的冗余,而且这样看起来并不优雅。好在ES为我们提供了一种解决方案:cross_fileds 跨字段查询

{
    "provice" : "Gansu",
	"city" : "Lanzhou",
	"street" : "Chenguan",
    "address": "Gansu Lanzhou Chenguan"
}

cross_fileds这种方式会分对查询的关键字进行分词,然后从所有字段中搜索每个词,解决了以字段为中心这种方式的问题,我们把它叫做以词为中心的方式。而且他通过混合不同字段逆向索引文档频率的方式解决了词频的问题,但是这个需要为每个字段使用相同的分析器。

POST address/_search
{
  "query": {
    "multi_match": {
      "query": "Gansu Lanzhou Anning",
      "type": "cross_fields",
      "operator": "and", 
      "fields": ["provice", "city", "street"]
    }
  }
}

# 查询结果
"hits" : [
      {
        "_index" : "address",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.9534616,
        "_source" : {
          "provice" : "Gansu",
          "city" : "Lanzhou",
          "street" : "Anning"
        }
      }
    ]
  }