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

背景

在针对线上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,可以从单元测试代码入手,编写更加复杂的测试代码,进行调试,可以快速定位出问题出现的原因并进行修复。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,374评论 0 5
  • 应用间通信 应用程式只能间接与设备上的其他应用进行通信。您可以使用AirDrop与其他应用程序共享文件和数据。您还...
    nicedayCoco阅读 735评论 0 1
  • 一、简历准备 1、个人技能 (1)自定义控件、UI设计、常用动画特效 自定义控件 ①为什么要自定义控件? Andr...
    lucas777阅读 5,190评论 2 54
  • A application [ˌæplɪ'keɪʃ(ə)n]应用程式 应用、应用程序application fra...
    朱晓晓的技术博客阅读 971评论 0 2
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32