美文网首页腾讯云Elasticsearch ServiceElasticsearch实践与分析@IT·大数据
记一次向Elasticsearch开源社区贡献代码的经历

记一次向Elasticsearch开源社区贡献代码的经历

作者: bellengao | 来源:发表于2019-12-05 10:30 被阅读0次

    背景

    在针对线上ES集群进行运维值班的过程中,有用户反馈使用自建的最新的7.4.2版本的ES集群,索引的normalizer配置无法使用了,怎么配置都无法生效,而之前的6.8版本还是可以正常使用的。根据用户提供的索引配置进行了复现,发现确实如此。通过搜索发现github上有人已经针对这个问题提了issue: #48650, 并且已经有社区成员把这个issue标记为了bug, 但是没有进一步的讨论了,所以我就深入研究了源码,最终找到了bug产生的原因,在github上提交了PR:#48866,最终被merge到了master分支,在7.6版本会进行发布。

    何为normalizer

    normaizer 实际上是和analyzer类似,都是对字符串类型的数据进行分析和处理的工具,它们之间的区别是:

    1. normalizer只对keyword类型的字段有效
    2. normalizer处理后的结果只有一个token
    3. normalizer只有char_filter和filter,没有tokenizer,也即不会对字符串进行分词处理
    

    如下是一个简单的normalizer定义,并且把字段foo配置了normalizer:

    PUT index
    {
      "settings": {
        "analysis": {
          "char_filter": {
            "quote": {
              "type": "mapping",
              "mappings": [
                "« => \"",
                "» => \""
              ]
            }
          },
          "normalizer": {
            "my_normalizer": {
              "type": "custom",
              "char_filter": ["quote"],
              "filter": ["lowercase", "asciifolding"]
            }
          }
        }
      },
      "mappings": {
        "properties": {
          "foo": {
            "type": "keyword",
            "normalizer": "my_normalizer"
          }
        }
      }
    }
    

    情景复现

    首先定义了一个名为my_normalizer的normalizer, 处理逻辑是把该字符串中的大写字母转换为小写:

    {
      "settings": {
        "analysis": {
          "normalizer": {
            "my_normalizer": {
              "filter": [
                "lowercase"
              ],
              "type": "custom"
            }
          }
        }
      }}
    

    通过使用_analyze api测试my_normalizer:

    GET {index}/_analyze
    {
      "text": "Wi-fi",
      "normalizer": "my_normalizer"
    }
    

    期望最终生成的token只有一个,为:"wi-fi", 但是实际上生成了如下的结果:

    {
      "tokens" : [
        {
          "token" : "wi",
          "start_offset" : 0,
          "end_offset" : 2,
          "type" : "<ALPHANUM>",
          "position" : 0
        },
        {
          "token" : "fi",
          "start_offset" : 3,
          "end_offset" : 5,
          "type" : "<ALPHANUM>",
          "position" : 1
        }
      ]
    }
    

    也就是生成了两个token: wi和fi,这就和前面介绍的normalizer的作用不一致了:normalizer只会生成一个token,不会对原始字符串进行分词处理。

    为什么会出现这个bug

    通过在6.8版本的ES上进行测试,发现并没有复现,通过对比_analyze api的在6.8和7.4版本的底层实现逻辑,最终发现7.0版本之后,_analyze api内部的代码逻辑进行了重构,把执行该api的入口方法TransportAnalyzeAction.anaylze()方法的逻辑有些问题:

    public static AnalyzeAction.Response analyze(AnalyzeAction.Request request, AnalysisRegistry analysisRegistry,
                                              IndexService indexService, int maxTokenCount) throws IOException {
    
            IndexSettings settings = indexService == null ? null : indexService.getIndexSettings();
    
            // First, we check to see if the request requires a custom analyzer.  If so, then we
            // need to build it and then close it after use.
            try (Analyzer analyzer = buildCustomAnalyzer(request, analysisRegistry, settings)) {
                if (analyzer != null) {
                    return analyze(request, analyzer, maxTokenCount);
                }
            }
    
            // Otherwise we use a built-in analyzer, which should not be closed
            return analyze(request, getAnalyzer(request, analysisRegistry, indexService), maxTokenCount);
        }
    

    analyze方法的主要逻辑为:先判断请求参数request对象中是否包含自定义的tokenizer, token filter以及char filter, 如果有的话就构建出analyzer或者normalizer, 然后使用构建出的analyzer或者normalizer对字符串进行处理;如果请求参数request对象没有自定义的tokenizer, token filter以及char filter方法,则使用已经在索引settings中配置好的自定义的analyzer或normalizer,或者使用内置的analyzer对字符串进行进行分析和处理。

    我们复现的场景中,请求参数request中使用了在索引settings中配置好的normalizer,所以buildCustomAnalyzer方法返回空, 紧接着执行了getAnalyzer方法用于获取自定义的normalizer, 看一下getAnalyzer方法的逻辑:

    private static Analyzer getAnalyzer(AnalyzeAction.Request request, AnalysisRegistry analysisRegistry, IndexService indexService) throws IOException {
            if (request.analyzer() != null) {
                    ...
                 return analyzer;
                }
            }
            if (request.normalizer() != null) {
                // Get normalizer from indexAnalyzers
                if (indexService == null) {
                    throw new IllegalArgumentException("analysis based on a normalizer requires an index");
                }
                Analyzer analyzer = indexService.getIndexAnalyzers().getNormalizer(request.normalizer());
                if (analyzer == null) {
                    throw new IllegalArgumentException("failed to find normalizer under [" + request.normalizer() + "]");
                }
            }
            if (request.field() != null) {
                ...
            }
            if (indexService == null) {
                return analysisRegistry.getAnalyzer("standard");
            } else {
                return indexService.getIndexAnalyzers().getDefaultIndexAnalyzer();
            }
    

    上述逻辑用于获取已经定义好的analyzer或者normalizer, 但是问题就出在与当request.analyzer()不为空时,正常返回了定义好的analyzer, 但是request.normalizer()不为空时,却没有返回,导致程序最终走到了最后一句return, 返回了默认的standard analyzer.

    所以最终的结果就可以解释了,即使自定义的有normalizer, getAnalyer()始终返回了默认的standard analyzer, 导致最终对字符串进行解析时始终使用的是standard analyzer, 对"Wi-fi"的处理结果正是"wi"和"fi"。

    单元测试没有测试到吗

    通过查找TransportAnalyzeActionTests.java类中的testNormalizerWithIndex方法,发现对normalizer的测试用例太简单了:

    public void testNormalizerWithIndex() throws IOException {
            AnalyzeAction.Request request = new AnalyzeAction.Request("index");
            request.normalizer("my_normalizer");
            request.text("ABc");
            AnalyzeAction.Response analyze
                = TransportAnalyzeAction.analyze(request, registry, mockIndexService(), maxTokenCount);
            List<AnalyzeAction.AnalyzeToken> tokens = analyze.getTokens();
    
            assertEquals(1, tokens.size());
            assertEquals("abc", tokens.get(0).getTerm());
        }
    

    对字符串"ABc"进行测试,使用自定义的my_normalizer和使用standard analyzer的测试结果是一样的,所以这个测试用例通过了,导致这个bug没有及时没发现。

    提交PR

    在确认了问题的原因后,我提交了PR:#48866, 主要的改动点有:

    1. TransportAnalyzeAction.getAnalyzer()方法判断normalizer不为空时返回该normalizer
    2. TransportAnalyzeActionTests.testNormalizerWithIndex()测试用例中把用于测试的字符串修改我"Wi-fi", 确保自定义的normalizer能够生效。

    改动的并不多,社区的成员在确认这个bug之后,和我经过了一轮沟通,认为应当对测试用例生成的结果增加注释说明,在增加了说明之后,社区成员进行了merge, 并表示会在7.6版本中发布这个PR。

    总结

    本次提交bug修复的PR,过程还是比较顺利的,改动点也不大,总结的经验是遇到新版本引入的bug,可以从单元测试代码入手,编写更加复杂的测试代码,进行调试,可以快速定位出问题出现的原因并进行修复。

    相关文章

      网友评论

        本文标题:记一次向Elasticsearch开源社区贡献代码的经历

        本文链接:https://www.haomeiwen.com/subject/qzqdgctx.html