Elasticsearch:了解 Elasticsearch combined fields 和 multi match 查询

2022年1月13日   |   by mebius

原文:Understanding Elasticsearch combined fields and multi match queries

这篇博文深入探讨了 Elasticsearch 7.13 中添加的新的 combined_fields 查询以及为什么它是一个非常好的补充,尤其是对于电子商务用例。 然而,为了更好地理解这个查询,我们还将花一些时间使用 multi_match 查询。 当然,你会在搜索中了解德语的复杂性 – 因为谁不呢?

问题是什么?

让我们看一下电子商务产品的非常简化的 JSON 表示。

{
  "name" : "Gestreiftes Kleid / Abendkleid",
  "color" : "rot",
  "brand" : "Esprit",
  "size" : "L",
  "price" : "32.49"
}

为了简化,在这里有意忽略了产品的种类、库存数据、尺寸/单位标准化等。

让我们快速索引五个文档进行测试

PUT products/_bulk?refresh
{ "index" : { "_id" : 1} }
{"name":"Gestreiftes Kleid","color":["gelb","blau"]}
{ "index" : { "_id" : 2} }
{"name":"Gestreiftes Kleid / Abendkleid","color":"rot","brand":"Esprit","size":"L","price":"32.49"}
{ "index" : { "_id" : 3} }
{"name":"Gestreiftes Kleid","color":["creme rose"]}
{ "index" : { "_id" : 4} }
{"name":"Gestreiftes Kleid 3000","color":["rot"]}
{ "index" : { "_id" : 5} }
{"name":"Hoodie Faster Runner","brand":"nike","size":"XL","color":"black"}

现在常见的用户搜索可能是 kleidrot – 它在两个不同的字段中有两个不同的术语,但每个字段中只有一个匹配项。我们的目的是找出在所有文档中同时含有这两个词的文档。 and 查询很难做到这一点。 以下不返回任何结果:

GET products/_search
{
  "query": {
    "multi_match": {
      "query": "kleid rot",
      "fields": ["name", "brand", "size", "color"],
      "operator": "and"
    }
  }
}

将运算符从 and更改为 or返回任何包含 kleid 或者 rot 的内容。

GET products/_search
{
  "query": {
    "multi_match": {
      "query": "kleid rot",
      "fields": ["name", "brand", "size", "color"],
      "operator": "or"
    }
  }
}

使用这个特定的查询,我们不能使用 minimum_should_match,因为只有两个 terms 构成查询。

深入研究的一种方法是在 multi_match 查询中使用 most_fields 类型:

GET products/_search
{
  "query": {
    "multi_match": {
      "query": "kleid rot",
      "fields": ["name", "brand", "size", "color"],
      "operator": "or",
      "type": "most_fields"
    }
  }
}

这把所有字段的分数进行相加,并表明颜色有 rot 且名称中有 kleid 的文档得分最高。 然而,第三个文档不包含 rot,因此你最终可能会得到很多不需要的文档 —这在电子商务设置中仍然可以。

我们可以做一个最后的改变来只得到正确的匹配,并使用带有 and 运算符的 cross_fields。

GET products/_search?filter_path=**hits
{
  "query": {
    "multi_match": {
      "query": "kleid rot",
      "fields": ["name", "brand", "size", "color"],
      "operator": "and",
      "type": "cross_fields"
    }
  }
}

这仅返回两个文档:

{
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.2619879,
    "hits" : [
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.2619879,
        "_source" : {
          "name" : "Gestreiftes Kleid / Abendkleid",
          "color" : "rot",
          "brand" : "Esprit",
          "size" : "L",
          "price" : "32.49"
        }
      },
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.2619879,
        "_source" : {
          "name" : "Gestreiftes Kleid 3000",
          "color" : [
            "rot"
          ]
    tgcode    }
      }
    ]
  }
}

我们也可以像这样使用 minimum_should_match 和 or 查询:

GET products/_search
{
  "query": {
    "multi_match": {
      "query": "kleid rot esprit",
      "fields": ["name", "brand", "size", "color"],
      "operator": "or",
      "minimum_should_match": 2, 
      "type": "cross_fields"
    }
  }
}

这仍然返回两个文件 – 红色 esprit 连衣裙排名更高。 此外,你可以搜索未包含在索引中的术语并仍返回数据,例如 esprit 之类的。

到目前为止,一切都那么完美,那么为什么我们会提到 combine_fields 查询呢?

从高层次的角度来看,cross_fields 类型和新的 combine_fields 查询非常相似。

GET products/_search?filter_path=**hits
{
  "query": {
    "combined_fields": {
      "query": "kleid rot esprit",
      "fields": [ "name", "brand", "size", "color" ],
      "operator": "or",
      "minimum_should_match": 2
    }
  }
}

上面查询的结果为:

{
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 3.684941,
    "hits" : [
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 3.684941,
        "_source" : {
          "name" : "Gestreiftes Kleid / Abendkleid",
          "color" : "rot",
          "brand" : "Esprit",
          "size" : "L",
          "price" : "32.49"
        }
      },
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.6346233,
        "_source" : {
          "name" : "Gestreiftes Kleid 3000",
          "color" : [
            "rot"
          ]
        }
      }
    ]
  }
}

和 multi match 结果也相似,但得分不同。这就是我们必须深入了解细节的地方。

那么,这是一个仅用于评分的新查询吗?是的,我将解释为什么这是有道理的。首先,cross field 类型可能会创建破碎的分数或在这里cross fields 也使用它自己的评分公式,这让用户感到困惑

此问题的解决方案是 BM25F,从而在评分时采用更稳健的方法。这个想法类似于 cross fields,其中查询以术语为中心(term-centric),将查询分析为单个术语,然后在任何字段中查找每个术语,基本上将所有字段视为一个大字段。然而,BM25F 结合了文档统计数据,因此 BM25 的使用及其 TF 饱和度得以保留 – 作为副作用,上述评分问题不会发生(不是真正的副作用,是吗?)。

如果你想阅读有关 BM25F 的更多信息,请查看在 combine_fields中的PDF链接中有关概率相关性框架:BM25 及 以后。

深层发掘

如果没有任何限制,那不会很有趣,对吧? 现在我们终于明白了,为什么我选择了德国的例子。 德语不仅是复合词的语言,因此需要分解词。 在英语中,a dress has stripes andisstriped。 所以这两个词 striped 和 striped 都含有共同的词干。 在德语中,这是不同的:a dress hasStreifen, but it isgestreift。 我不知道确切的翻译,但我认为这是过去分词的形容词 – 不是现在分词。 在德语中,这将是一个 partizipatives Adjektiv。

长话短说,如果你想搜索 kleid gestreift 或 rotes kleid,你需要进行一些额外的处理。 让我们来看看德语分析器,它会考虑德语停用词并进行某种词干提取。 让我们来看看:

GET _analyze?filter_path=**.token
{
  "analyzer": "german",
  "text": ["rotes Kleid"]
}

上述结果是:

{
  "tokens" : [
    {
      "token" : "rot"
    },
    {
      "token" : "kleid"
    }
  ]
}

好的,所以这有效。然而,针对kleid gestreift,我们仍然不走运。搜索 gestreiftes Kleid 会将其简化为 gestreift kleid,因此会进行一些词干提取。 streifen 也将被分词为streif。尽管你可以使用更多的词干技巧并使用 hunspell token filter,下载字典并改进它,但目前还没有办法从 gestreift 到 streifen 或 streif – 这宁愿需要词形还原,这反过来意味着,你还需要应用词性标记,以找出单词的上下文。这在电子商务中更难,因为用户很少写完整的句子,所以任何模型都很难。

另一种无法推广但可能对某些术语有价值的方法可能是同义词的愚蠢使用。你可以使用 nltk 或 HanTa 之类的工具包来创建引理,然后使用引理化版本作为同义词。因此,对于 gestreift,这可能是 streifen。让我们做一些索引更改来试试这个

POST products/_close

PUT products/_settings
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_search_analyzer": {
            "tokenizer": "standard",
            "filter": [
              "lowercase",
              "german_stop",
              "german_stemmer",
              "search_synonym_filter"
            ]
          }
        },
        "filter": {
          "search_synonym_filter": {
            "type": "synonym_graph",
            "synonyms": [
              "gestreift => streifen"
            ]
          },
          "german_stop": {
            "type": "stop",
            "stopwords": "_german_"
          },
          "german_stemmer": {
            "type": "stemmer",
            "language": "light_german"
          }
        }
      }
    }
  }
}

POST products/_open

GET products/_analyze?filter_path=**.token
{
  "text": "rotes kleid gestreift",
  "analyzer": "synonym_search_analyzer"
}

上面的最后一个命令的返回结果为:

{
  "tokens" : [
    {
      "token" : "rot"
    },
    {
      "token" : "kleid"
    },
    {
      "token" : "streif"
    }
  ]
}

这看起来不错!

现在,为了将其纳入我们的索引,让我们使用正确的设置重新创建和重新索引数据

DELETE products

PUT products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "search_analyzer": "synonym_search_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "brand": {
        "type": "text",
        "search_analyzer"tgcode: "synonym_search_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "size": {
        "type": "text",
        "search_analyzer": "synonym_search_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "color": {
        "type": "text",
        "search_analyzer": "synonym_search_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      }

    }
  },
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_search_analyzer": {
            "tokenizer": "standard",
            "filter": [
              "lowercase",
              "german_stop",
              "german_stemmer",
              "search_synonym_filter"
            ]
          }
        },
        "filter": {
          "search_synonym_filter": {
            "type": "synonym_graph",
            "synonyms": [
              "gestreift => streifen"
            ]
          },
          "german_stop": {
            "type": "stop",
            "stopwords": "_german_"
          },
          "german_stemmer": {
            "type": "stemmer",
            "language": "light_german"
          }
        }
      }
    }
  }
}

PUT products/_bulk?refresh
{ "index" : { "_id" : 1} }
{"name":"Gestreiftes Kleid","color":["gelb","blau"]}
{ "index" : { "_id" : 2} }
{"name":"Gestreiftes Kleid / Abendkleid","color":"rot","brand":"Esprit","size":"L","price":"32.49"}
{ "index" : { "_id" : 3} }
{"name":"Gestreiftes Kleid","color":["creme rose"]}
{ "index" : { "_id" : 4} }
{"name":"Gestreiftes Kleid 3000","color":["rot"]}
{ "index" : { "_id" : 5} }
{"name":"Hoodie Faster Runner","brand":"nike","size":"XL","color":"black"}

现在我们可以搜索 rotes esprit kleid mit streifen(红色条纹连衣裙)

GET products/_search?filter_path=**hits
{
  "query": {
    "multi_match": {
      "query": "rotes esprit kleid mit streifen",
      "fields": ["name", "brand", "size", "color"],
      "operator": "or",
      "minimum_should_match": 2, 
      "type": "cross_fields"
    }
  }
}

上面的命令的返回结果为:

{
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.955135,
    "hits" : [
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.955135,
        "_source" : {
          "name" : "Gestreiftes Kleid / Abendkleid",
          "color" : "rot",
          "brand" : "Esprit",
          "size" : "L",
          "price" : "32.49"
        }
      },
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.2619879,
        "_source" : {
          "name" : "Gestreiftes Kleid 3000",
          "color" : [
            "rot"
          ]
        }
      }
    ]
  }
}
}
GET products/_search?filter_path=**hits
{
  "quertgcodey": {
    "combined_fields": {
      "query": "rotes esprit kleid mit streifen",
      "fields": [ "name", "brand", "size", "color" ],
      "operator": "or",
      "minimum_should_match": 2
    }
  }
}

上面的命令的返回结果:

{
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 3.684941,
    "hits" : [
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 3.684941,
        "_source" : {
          "name" : "Gestreiftes Kleid / Abendkleid",
          "color" : "rot",
          "brand" : "Esprit",
          "size" : "L",
          "price" : "32.49"
        }
      },
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.6346233,
        "_source" : {
          "name" : "Gestreiftes Kleid 3000",
          "color" : [
            "rot"
          ]
        }
      }
    ]
  }
}

当你比较分数时,你会看到一些差异。然而,还有更多内容,multi_match 查询将允许你在 mapping 使用不同的分词器,而 combine_fields 查询需要相同的分析器。这确实是一个问题,因为在我们的示例中,对 color 字段进行词干以匹配 rotes for rot 可能是有意义的,但对品牌字段进行词干是没有意义的,因为你可能想搜索 nike,它会被词干到 nik,但没有意义,因为品牌 nike 没有词干。此外,combined_fields 查询中不支持模糊性。

仅将同义词用于 name 字段而不用于属于查询一部分的其他字段也可能更有意义。为此,你需要使用 cross_fields 重新使用 multi_match 查询。

cross_fields 类型还有一个不同之处,Mark 在此 github 讨论中概述了这一点。在我们的例子中,产品名称或描述的含义没有改变,但在有名字和姓氏的地址簿的情况下,它确实会改变,因为 Alex 作为姓氏要少得多,因此可能更多或不太重要。

也就是说,如果您能忍受这些限制,那么测试新的组合字段查询是值得一试的。

最后一件事,如果你的规则可能变得比单个同义词更复杂,并且你还想引入更多评分/排名,那么查看 quergy 也可能是一个好主意。

更多阅读:如何使用 Elasticsearch 中的 copy_to 来提高搜索效率

文章来源于互联网:Elasticsearch:了解 Elasticsearch combined fields 和 multi match 查询

相关推荐: Elasticsearch: Python 客户端现在支持异步 I/O

随着支持异步 I/O 的 Python Web 框架(如 FastAPI、Starlette 以及即将在 Django 3.1 中出现)的日益流行,Python Elasticsearch 客户端对原生异步 I/O 支持的需求tgcode不断增长。 异步 I/…

Tags: , ,