如何对 Elasticsearch 中的分析字段执行精确匹配查询?

How to perform an exact match query on an analyzed field in Elasticsearch?

这可能是一个很常见的问题,但是到目前为止我得到的答案并不令人满意。

问题: 我有一个由将近100个字段组成的es索引。大多数字段都是 string 类型并设置为 analyzed。但是,查询可以是部分的 (match) 或精确的(更像是 term)。因此,如果我的索引包含一个值为 super duper cool pizza 的字符串字段,则可以有像 duper super 这样的部分查询并将与文档匹配,但是,可以有像 cool pizza 这样的精确查询,它应该与文件不符。另一方面,Super Duper COOL PIzza 又应该与该文档匹配。

到目前为止,部分匹配部分很简单,我在 match 查询中使用了 AND 运算符。但是无法完成其他类型。

我调查了与此问题相关的其他 post,这个 post 包含最接近的解决方案:

在三个解决方案中,第一个感觉非常复杂,因为我有很多字段而且我不使用 REST api,我正在使用 QueryBuilders 和来自他们的 NativeSearchQueryBuilder 动态创建查询 Java api。它还会生成很多可能的模式,我认为这些模式会导致性能问题。

第二个是一个更简单的解决方案,但同样,我必须维护更多(几乎)冗余数据,而且我认为使用 term 查询永远无法解决我的问题。

最后一个我觉得有问题,不会阻止super duper匹配到super duper cool pizza,这不是我想要的输出

那么我还有其他方法可以实现这个目标吗?如果需要进一步解决问题,我可以 post 一些示例映射。我也已经保留了源代码(以防万一)。请随时提出任何改进建议。

提前致谢。

[更新]

最后,我使用了 multi_field,为精确查询保留一个原始字段。当我插入时,我对数据使用了一些自定义修改,而在搜索过程中,我对输入文本使用了相同的修改例程。这部分不由 Elasticsearch 处理。如果你想这样做,你也必须设计合适的分析器。

索引设置和映射查询:

PUT test_index

POST test_index/_close

PUT test_index/_settings
{
  "index": {
    "analysis": {
      "analyzer": {
        "standard_uppercase": {
          "type": "custom",
          "char_filter": ["html_strip"],
          "tokenizer": "keyword",
          "filter": ["uppercase"]
        }
      }
    }
  }
}

PUT test_index/doc/_mapping
{
  "doc": {
     "properties": {
        "text_field": {
           "type": "string",
           "fields": {
              "raw": {
                 "type": "string",
                 "analyzer": "standard_uppercase"
              }
           }
        }
     }
  }
}

POST test_index/_open

正在插入一些示例数据:

POST test_index/doc/_bulk
{"index":{"_id":1}}
{"text_field":"super duper cool pizza"}
{"index":{"_id":2}}
{"text_field":"some other text"}
{"index":{"_id":3}}
{"text_field":"pizza"}

精确查询:

GET test_index/doc/_search
{
  "query": {
    "bool": {
      "must": {
        "bool": {
          "should": {
            "term": {
             "text_field.raw": "PIZZA"
            }
          }
        }
      }
    }
  }
}

回复:

{
   "took": 1,
   "timed_out": false,
   "_shards": {
      "total": 1,
      "successful": 1,
      "failed": 0
   },
   "hits": {
      "total": 1,
      "max_score": 1.4054651,
      "hits": [
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "3",
            "_score": 1.4054651,
            "_source": {
               "text_field": "pizza"
            }
         }
      ]
   }
}

部分查询:

GET test_index/doc/_search
{
  "query": {
    "bool": {
      "must": {
        "bool": {
          "should": {
            "match": {
              "text_field": {
                "query": "pizza",
                "operator": "AND",
                "type": "boolean"
              }
            }
          }
        }
      }
    }
  }
}

回复:

{
   "took": 1,
   "timed_out": false,
   "_shards": {
      "total": 1,
      "successful": 1,
      "failed": 0
   },
   "hits": {
      "total": 2,
      "max_score": 1,
      "hits": [
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "3",
            "_score": 1,
            "_source": {
               "text_field": "pizza"
            }
         },
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "1",
            "_score": 0.5,
            "_source": {
               "text_field": "super duper cool pizza"
            }
         }
      ]
   }
}

PS:这些是生成的查询,这就是为什么会有一些冗余块,因为会有许多其他字段连接到查询中。

可悲的是,现在我需要再次重写整个映射:(

我认为这会做你想做的(或者至少尽可能接近),使用 keyword tokenizer and lowercase token filter:

PUT /test_index
{
   "settings": {
      "analysis": {
         "analyzer": {
            "lowercase_analyzer": {
               "type": "custom",
               "tokenizer": "keyword",
               "filter": ["lowercase_token_filter"]
            }
         },
         "filter": {
            "lowercase_token_filter": {
               "type": "lowercase"
            }
         }
      }
   },
   "mappings": {
      "doc": {
         "properties": {
            "text_field": {
               "type": "string",
               "fields": {
                  "raw": {
                     "type": "string",
                     "index": "not_analyzed"
                  },
                  "lowercase": {
                     "type": "string",
                     "analyzer": "lowercase_analyzer"
                  }
               }
            }
         }
      }
   }
}

我添加了几个文档用于测试:

POST /test_index/doc/_bulk
{"index":{"_id":1}}
{"text_field":"super duper cool pizza"}
{"index":{"_id":2}}
{"text_field":"some other text"}
{"index":{"_id":3}}
{"text_field":"pizza"}

请注意,我们将外部 text_field 设置为由标准分析器分析,然后是 sub-field rawnot_analyzed(您可能不想要这个,我只是添加它用于比较),另一个 sub-field lowercase 创建与输入文本完全相同的标记,只是它们已被小写(但未按空格拆分)。所以这个 match 查询 returns 你所期望的:

POST /test_index/_search
{
    "query": {
        "match": {
           "text_field.lowercase": "Super Duper COOL PIzza"
        }
    }
}
...
{
   "took": 3,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "failed": 0
   },
   "hits": {
      "total": 1,
      "max_score": 0.30685282,
      "hits": [
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "1",
            "_score": 0.30685282,
            "_source": {
               "text_field": "super duper cool pizza"
            }
         }
      ]
   }
}

请记住,match 查询也将针对搜索短语使用该字段的分析器,因此在这种情况下搜索 "super duper cool pizza" 与搜索 [=21= 具有完全相同的效果](如果您想要完全匹配,您仍然可以使用 term 查询)。

查看三个文档在每个字段中生成的术语很有用,因为这是您的搜索查询所针对的(在本例中 rawlowercase相同的标记,但这只是因为所有输入已经 lower-case):

POST /test_index/_search
{
   "size": 0,
   "aggs": {
      "text_field_standard": {
         "terms": {
            "field": "text_field"
         }
      },
      "text_field_raw": {
         "terms": {
            "field": "text_field.raw"
         }
      },
      "text_field_lowercase": {
         "terms": {
            "field": "text_field.lowercase"
         }
      }
   }
}
...{
   "took": 26,
   "timed_out": false,
   "_shards": {
      "total": 5,
      "successful": 5,
      "failed": 0
   },
   "hits": {
      "total": 3,
      "max_score": 0,
      "hits": []
   },
   "aggregations": {
      "text_field_raw": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "pizza",
               "doc_count": 1
            },
            {
               "key": "some other text",
               "doc_count": 1
            },
            {
               "key": "super duper cool pizza",
               "doc_count": 1
            }
         ]
      },
      "text_field_lowercase": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "pizza",
               "doc_count": 1
            },
            {
               "key": "some other text",
               "doc_count": 1
            },
            {
               "key": "super duper cool pizza",
               "doc_count": 1
            }
         ]
      },
      "text_field_standard": {
         "doc_count_error_upper_bound": 0,
         "sum_other_doc_count": 0,
         "buckets": [
            {
               "key": "pizza",
               "doc_count": 2
            },
            {
               "key": "cool",
               "doc_count": 1
            },
            {
               "key": "duper",
               "doc_count": 1
            },
            {
               "key": "other",
               "doc_count": 1
            },
            {
               "key": "some",
               "doc_count": 1
            },
            {
               "key": "super",
               "doc_count": 1
            },
            {
               "key": "text",
               "doc_count": 1
            }
         ]
      }
   }
}

这是我用来测试的代码:

http://sense.qbox.io/gist/cc7564464cec88dd7f9e6d9d7cfccca2f564fde1

如果您还想进行部分 word 匹配,我建议您看一下 ngrams。我在这里写了Qbox的介绍:

https://qbox.io/blog/an-introduction-to-ngrams-in-elasticsearch