如何对 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 raw
即 not_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
查询)。
查看三个文档在每个字段中生成的术语很有用,因为这是您的搜索查询所针对的(在本例中 raw
和 lowercase
相同的标记,但这只是因为所有输入已经 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
这可能是一个很常见的问题,但是到目前为止我得到的答案并不令人满意。
问题:
我有一个由将近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 raw
即 not_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
查询)。
查看三个文档在每个字段中生成的术语很有用,因为这是您的搜索查询所针对的(在本例中 raw
和 lowercase
相同的标记,但这只是因为所有输入已经 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