Home Articles

如何在Elasticsearch中对分析的字段执行完全匹配查询?

Asked
Viewed 426 times
2

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

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

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

我查看了与此问题相关的其他帖子,此帖子包含最接近的解决方案:Elasticsearch exact matches on analyzed fields

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

第二个是一个更容易的解决方案,但同样,我必须保持更多(几乎)冗余数据,我不认为使用 term 查询将解决我的问题 .

我认为最后一个有问题,它不会阻止 super dupersuper duper cool pizza 匹配,这不是我想要的输出 .

那么有什么其他方法可以实现这个目标吗?如果需要清除问题,我可以发布一些样本映射 . 我已经保留了源代码(如果可以使用的话) . 请随时提出任何改进建议 .

提前致谢 .

[ UPDATE ]

最后,我使用 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:这些是生成的查询,这就是为什么有一些冗余块,因为会有许多其他字段连接到查询中 .

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

1 Answer

  • 4

    我认为这会做你想要的(或者至少尽可能接近),使用keyword tokenizerlowercase 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 设置为由标准分析器进行分析,然后是 raw 的子字段 raw (您可能不需要这个,我只是将其添加用于比较),另一个子字段 lowercase 创建令牌恰好是与输入文本相同,除了它们是小写的(但不是在空格上分割) . 所以 match 查询会返回您的预期:

    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" 将与搜索 "Super Duper COOL PIzza" 具有完全相同的效果(如果您想要完全匹配,您仍然可以使用 term 查询) .

    查看三个文档在每个字段中生成的术语非常有用,因为这是您的搜索查询将要反对的内容(在这种情况下 rawlowercase 具有相同的标记,但这只是因为所有输入都较低 - 已经):

    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

    如果你还想做部分单词匹配,我建议你看看ngrams . 我在这里写了一篇关于Qbox的介绍:

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

Related