elasticsearch慢查询排查记录之wildcard查询

2023-11-12 08:29:37 浏览数 (3)

一.背景

某天用户反馈集群负载很高,CPU资源处于持续被打满的状态,对于elasticsearch集群的业务请求也频繁超时,大量请求失败。

二.排查过程

通过监控,我们发现集群重要任务均为查询任务,通过集群慢日志我们发现集群在大量运行wildcard请求。耗时从数十秒到数分钟不等。在大量请求期间,集群的CPU load,磁盘IO均处于高位运行。

样例语句如下:

代码语言:javascript复制
GET index/_search
{

  "query": {
    "wildcard": {
      "name_zh": {
        "wildcard": "*xx科技*"
      }
    }
  },
  "from": 0,
  "size": 10
}

我们对慢查询语句通过profile解析后发现,耗时主要集中于wildcard模糊匹配阶段。

三.问题原因

wildcard通配符模糊匹配查询需要使用正排索引,类似于关系型数据库中的“like”操作。在wildcard查询对数据进行匹配的过程中需要匹配很多类型的数据,所以整体耗时都会很长。通过匹配字符串的方式对数据进行过滤查询。与elasticsearch使用倒排索引加速查询的理念背道而驰。虽然elasticsearch提供了wildcard这种字符串模糊匹配的能力,但是我们不建议使用该方法对elasticsearch进行查询。

1.使用wildcard查询可能造成的潜在问题

  1. 性能问题:通配符查询需要扫描所有的文档,因此对于大型索引,这可能会导致查询变慢。
  2. 精度问题:由于通配符查询会匹配所有符合条件的结果,因此可能会返回很多不相关的结果。
  3. 内存问题:通配符查询需要在内存中维护正则表达式,如果正则表达式太复杂,可能会导致内存不足。

因此,通配符查询应该谨慎使用,尽量避免在大型索引上使用,并且应该使用更精确的查询方式来提高查询性能和结果的准确性。

代码语言:javascript复制
public WildcardQueryBuilder(String fieldName, String value) {
 if (Strings.isEmpty(fieldName)) {
 throw new IllegalArgumentException("field name is null or empty");
 }
 if (value == null) {
 throw new IllegalArgumentException("value cannot be null");
 }
 this.fieldName = fieldName;
    this.value = value;
}

在该构造函数中判断传入wildcard Query中的字段名与搜索关键字是否为空,如果字段名为空,则抛出 IllegalArgumentException异常。如果值为null ,则抛出IllegalArgumentException异常。否则,将字段名和值赋值给相应的成员变量。 以便于后续构造QueryBuilder。

代码语言:javascript复制
public static WildcardQueryBuilder fromXContent(XContentParser parser) throws IOException {
        String fieldName = null;
        String rewrite = null;
        String value = null;
        float boost = AbstractQueryBuilder.DEFAULT_BOOST;
        boolean caseInsensitive = DEFAULT_CASE_INSENSITIVITY;
        String queryName = null;
        String currentFieldName = null;
        XContentParser.Token token;
        while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
            if (token == XContentParser.Token.FIELD_NAME) {
                currentFieldName = parser.currentName();
            } else if (token == XContentParser.Token.START_OBJECT) {
                throwParsingExceptionOnMultipleFields(NAME, parser.getTokenLocation(), fieldName, currentFieldName);
                fieldName = currentFieldName;
                while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
                    if (token == XContentParser.Token.FIELD_NAME) {
                        currentFieldName = parser.currentName();
                    } else {
                        if (WILDCARD_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                            value = parser.text();
                        } else if (VALUE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                            value = parser.text();
                        } else if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                            boost = parser.floatValue();
                        } else if (REWRITE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                            rewrite = parser.textOrNull();
                        } else if (CASE_INSENSITIVE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                            caseInsensitive = parser.booleanValue();
                        } else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
                            queryName = parser.text();
                        } else {
                            throw new ParsingException(
                                parser.getTokenLocation(),
                                "[wildcard] query does not support ["   currentFieldName   "]"
                            );
                        }
                    }
                }
            } else {
                throwParsingExceptionOnMultipleFields(NAME, parser.getTokenLocation(), fieldName, parser.currentName());
                fieldName = parser.currentName();
                value = parser.text();
            }
        }

在构造WildcardQueryBuilder的这段代码中,从XContentParser解析WildcardQueryBuilder的字段名、值和其他参数,并使用这些参数构建一个WildcardQueryBuilder对象。它根据字段名匹配相应的参数,并将其赋值给相应的变量。最后,将构造的WildcardQueryBuilder对象返回。

代码语言:javascript复制
public WildcardQueryBuilder caseInsensitive(boolean caseInsensitive) {
    this.caseInsensitive = caseInsensitive;
    return this;
}

通过该方法来控制匹配的敏感度,如果传入的参数为true,则表示进行大小写不敏感的匹配;如果传入的参数为false,则表示进行大小写敏感的匹配。

四.使用建议

1.使用match查询语句

代码语言:javascript复制
GET company/_search
{
 "query": {
 "match": {
 "name_zh.word": {
 "query": "xx科技"
 }
 }
 },
 "from": 0,
 "size": 10
}

2.如果需要短语匹配场景建议使用match_phrase短语匹配方式进行查询。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞