intro: 对于布尔类型字段的处理,Elasticsearch 曾犯了一个错,直到数年后 发布6.0版本才修正过来,这个设计或多或少会遇到,只是没留意,但是查询的时候结果还是让人困惑的。
问题或现象
前几天刚到公司,同事抛出一个问题,就是发现前一天某个搜索查询条件没有结果,但是第二天却出来结果,不过这个出来的结果是不对的,即搜索result=90时,出现了result=91的结果。
于是给我发了链接,我点过去就是下图这样子:
由于忙于其他问题,所以随口回复了让他使用 result.keyword=90 查询,显然满足条件了。不过一会儿对方又问了个问题“这个字段和其他有什么特殊吗,为什么就要用keyword”,我想了想,的确是个问题,这类索引没有使用特别的分词也没有用特制的打分策略,确实不应该匹配的。
但是为什么呢?
为什么
好在Elasticsearch(以下可能简称es)提供了一些辅查询相关的助接口,如分词有疑问可使用_analyze理解,打分有疑问可使用_explain, 应早在1.7版本前已经存在了,虽然es的版本有段时间跳跃。
当我们无法理解一个document为什么会被匹配时,就可以试试用explain查询那条记录,看看es为何会匹配,于是有下面结果(我简化了下查询):
|
|
我们期待的得分是0,即应该有一条是不满足的条件,但上述结果返回的还是得分1.0068661,匹配了,explain接口值得后面再写文章讨论下,这里不展开,如果这里你看不出什么,可以试试下面。
可以再查询下 昨日今日,即aa-2019.08.13/14的mapping配置,于是我得到了这样的结果
系统的索引是一天创建一个当天日期后缀的索引,没有特别对字段的mapping配置。
那么结论也就出来了,08.13那天的索引里,result类型是boolean,所以当查询条件 result为90或91的时候,他们都是都会被解析为true,也就是匹配索引里的boolean类型的字段的那条记录,所以搜索 result=90时,result=91也就出现在结果里了。
How
解决方法不难,有几种。
先看根原因,由于写es会根据字段 biz=A 聚合到同一索引下,多个服务又会共用 biz=A 的属性,并且由于他们可能使用了同名的字段 act,但是(act在各个服务里的)类型是不同的,比如上文result 有的是boolean,有的是String类型,所以每天凌晨第一条数据(先发出事件的服务)决定该字段在当天该索引的类型了。
所以,根本的办法是要求各应用规范统一。
但也可以在这里修改es不修改服务,统一设置该类索引的mapping,强制将该字段弱化为 string 类型,这样实现elasticsearch层面的统一。
More
我好奇的是,这是es的bug吗?
于是尝试下载最新版的Elasticsearch,发现该问题已经不存在的了
这里报了个json解析异常,这看起来有点有趣。
我们知道elasticsearch底层其实也用到Jackson的jsonparser去解析json类型内容的,于是我看了下7.2.1的Jackson-core这个jar包,确实升级了个版本。
那么这个bug是谁解决的呢?是Elasticsearch团队解决的,还是他们不经意间升级Jackson组件解决的?
后者有趣,是软件开发里的信任链问题了。
如果对Jackson了解的话,或许已经有答案了,不过我还是希望可以通过搜索到相关主题,更快速些。
遗憾的是通过elasticsearch/boolean/BooleanFieldMapper/number等关键字N种组合尝试都没有找到相关主题。
于是我猜测了几个可能的改动文件,就先从 BooleanFieldMapper.java 开始,从github的历史版本里查找,至少二分法查找能找到在哪个版本里有git变更吧。(需要说明的是elasticsearch源码比较能折腾,7.0后代码组织结构大变更,从之前的core分到server目录,module变更等)。巧合的是打开6.0版本就发现BooleanFieldMapper.java的历史变更记录里有一个主题关于 strict boolean ,点开发现和我的问题很相似。
看了下,虽然主题下帖子较多,但是互动人数不多,看评论似乎还未意识到这是个很明显的“看起来合理”的错误,而不是喜好问题。
More
该PR涉及几个改动,这里列下和本文问题最相关的改动点(以下讨论时基于JsonXContentParser):
1)es的解析原理中,对于document的解析是在org.elasticsearch.index.mapper.DocumentParser里通过 parseObjectOrField 方法完成对各个字段的解析的(index/store是后续逻辑了,无关本文)。
2)parseObjectOrField将解析代理给 org.elasticsearch.index.mapper.FieldMapper, 由于我们已经知道该字段是boolean类型的,所以就是通过 BooleanFieldMapper 解析的,对应的入口就是在org.elasticsearch.index.mapper.BooleanFieldMapper.parseCreateField 方法内处理field的。
3)5.2.2和 7.2.1 在处理boolean类型field的区别就是下面代码所示:
Elasticsearch 5.2.2 的部分代码
Elasticsearch 7.2.1 的部分代码
区别在于 7.2.1中对boolean的解析去掉了 token == Token.VALUE_NUMBER 部分的逻辑(同时对0/1作为布尔类型也不再支持了),而是先判断 VALUE_STRING 这种情况,通过 parseBoolean 处理, 即仅支持“true/false/null”,其他任何都是报 IllegalArgumentException(包括不支持on/off/True/False/yes/no) ,此外的就交给 doBooleanValue 处理了,即通过Jackson的 JsonParser.getBooleanValue处理。这里其实只是对Jackson有些依赖的。
需要指出是对于True/False是另外一处代码,但最终和 JsonParser.getBooleanValue 类似。