先说下优化的背景

我们后端的所有接口有一个质量属性的要求,就是保证我们的接口响应时长不能超过 1s, 而这个根据用户名称查询用户昵称是很多其他接口的依赖,首先这个查询的过程,没法做缓存,因为客户那边需要实时看到用户更新的动态,如果将用户名称和昵称做缓存,会出现延迟响应的过程,因此只能考虑怎么更快的查询出一批用户名称对应的用户昵称。

代码存在的问题

代码中其他的查询条件都还 ok, 就是有一个地方,会因为批量查询的用户名称越多而导致响应时间变慢,下面是一部分原始代码,由于涉及到业务,只贴出关键性的代码

BoolQueryBuilder userNameShouldBuilder = QueryBuilders.boolQuery();
for (String userName : userNames) {userNameShouldBuilder.should(QueryBuilders.matchPhraseQuery("user_name",userName));
}
boolQueryBuilder.must(userNameShouldBuilder);

为啥要用 should + matchPhraseQuery 的方式查询了?
其实这样看 es 索引的 mapping 字段长啥样

"mappings" : {...,"user_name" : {"type" : "text","analyzer" : "analyzer_1_20"}...,
}

user_name 字段定义了一个自定义的分词器,我们可以通过 analyzer api 看下分词效果

GET index_name/_analyze
{"analyzer": "analyzer_1_20","text": "qq1234"
}

它的结果如下

{"tokens" : [{"token" : "q","start_offset" : 0,"end_offset" : 1,"type" : "word","position" : 0},{"token" : "qq","start_offset" : 0,"end_offset" : 2,"type" : "word","position" : 1},{"token" : "qq1","start_offset" : 0,"end_offset" : 3,"type" : "word","position" : 2},{"token" : "qq12","start_offset" : 0,"end_offset" : 4,"type" : "word","position" : 3},{"token" : "qq123","start_offset" : 0,"end_offset" : 5,"type" : "word","position" : 4},{"token" : "qq1234","start_offset" : 0,"end_offset" : 6,"type" : "word","position" : 5},{"token" : "q","start_offset" : 1,"end_offset" : 2,"type" : "word","position" : 6},{"token" : "q1","start_offset" : 1,"end_offset" : 3,"type" : "word","position" : 7},{"token" : "q12","start_offset" : 1,"end_offset" : 4,"type" : "word","position" : 8},{"token" : "q123","start_offset" : 1,"end_offset" : 5,"type" : "word","position" : 9},{"token" : "q1234","start_offset" : 1,"end_offset" : 6,"type" : "word","position" : 10},{"token" : "1","start_offset" : 2,"end_offset" : 3,"type" : "word","position" : 11},{"token" : "12","start_offset" : 2,"end_offset" : 4,"type" : "word","position" : 12},{"token" : "123","start_offset" : 2,"end_offset" : 5,"type" : "word","position" : 13},{"token" : "1234","start_offset" : 2,"end_offset" : 6,"type" : "word","position" : 14},{"token" : "2","start_offset" : 3,"end_offset" : 4,"type" : "word","position" : 15},{"token" : "23","start_offset" : 3,"end_offset" : 5,"type" : "word","position" : 16},{"token" : "234","start_offset" : 3,"end_offset" : 6,"type" : "word","position" : 17},{"token" : "3","start_offset" : 4,"end_offset" : 5,"type" : "word","position" : 18},{"token" : "34","start_offset" : 4,"end_offset" : 6,"type" : "word","position" : 19},{"token" : "4","start_offset" : 5,"end_offset" : 6,"type" : "word","position" : 20}]
}

通过分词效果很容易看出来,text 类型,会根据自定义的 analyzer 进行分词处理,建立索引的作者的初衷是考虑到我们这个用户名称需要支持模糊查询,同时也支持准确查询。 但我们这里的需求是需要的聚合查询,而 text 类型是不支持完全相等查询的。因此要精确查询就只能用 matchPhraseQuery。

matchPhraseQuery 是短语精确查询,它会保证所有分词的顺序以及分词组合都完全一致的才会查询出来,但其实这里还存在另外一个问题,如果用户名称中包含一些标点符号,可能会出现查询结果不准确的问题, 因为实际的需求是要找到完全相等的用户名称对应的用户昵称。

更极端的问题是,代码中可能出现内存爆满的问题,或者查询结果超时等问题,举个例子来说比如有一个用户名称为 nownow_ 的用户, 那么它会匹配所有去掉停用词的之后只有 nownow 的用户,举个例子来说, idontknownow, snownow, knownow 等都会匹配到,能想象一下如果一次性拿出所有匹配到这些数据,它查出来的结果会有多大。幸好的事我们的代码中加了查询个数限制,是因为当时预发环境测试的时候就发现查询数据量很大的问题,才加的这个限制,只是当时没有对这个问题引起足够的重视,因为测试人员他测试的用户名称很少,且对 es 来说比较好区分的那种。

因此解决这个问题,需要增加一个字段,支持完全相等的查询。

keyword 类型不会分词,是直接建立索引的,支持完全相等的查询。

同时由于我们同时要支持一次查询一批用户的昵称,所以需要配合should 进行查询

总结问题:

  1. matchPhraseQuery + should 查询性能低,批量查询的用户名称越多,性能越差
  2. 现有的实现未满足需求要求,分词时会移除标点符号等无意义的词,可能造成查询结果不准确

解决存在的问题

  1. 既然需要一个不分词的字段,自然考虑到添加一个 keyword 的字段, text 下面天然支持 keyword, 通过下面的方式给已有的字段添加一个 keyword 字段
PUT /index_name/_mapping
{"properties": {"user_name": {"type": "text","analyzer" : "analyzer_1_20","fields": {"keyword": {"type": "keyword","ignore_above": 256}}}}
}

现在直接去查这个 keyword 字段是没有数据的, 需要重新索引下数据。

  1. 通过 _update_by_query 重新索引下数据,这样 keyword 中就有数据了,这里可以根据业务条件,限定下数据量, 注意在 _update_by_query 之前最好看一下查询条件对不对, 比如我这里只考虑有 nick_name 字段的数据
GET index_name/_count
{"query": {"bool": {"must": [{"exists": {"field": "nick_name"}}]}}
}

确认没问题之后,就可以重建下索引了

POST index_name/_update_by_query?conflicts=proceed&slices=4
{"query": {"bool": {"must": [{"exists": {"field": "nick_name"}}]}}
}

注意: conflicts=proceed , _update_by_query 在开始执行的时候获取一个快照, 类似 scroll 查询,此时的数据会控制一个内部版本号, 如果快照的数据在 update 的时候,已经有过更新处理,那么就会出现版本号冲突,导致更新中断,抛出更新冲突异常。conflicts=proceed 就是在遇到版本冲突的时候,不会中断 update 操作, 只是做简单的冲突计数,我们这里只是给 keyword 字段建立一个索引,所以不用考虑文档更新冲突问题。

如果数据量很大,_update_by_query 可能会执行很长的时间, 怎么看它的进度, 可以直接查已经建立 keyword 索引字段的数据量来计算大概的进度

GET index_name\_count
{"query": {"bool": {"must": [{"exists": {"field": "user_name.keyword"}}]}}
}

这里可能有的小伙伴不理解为啥,不直接建个索引,然后 reindex 下就行,搞这么麻烦, 首先这里有前提条件,一个索引数据量很大,上亿,但真正要重建索引的数据很少,另外一点就是这个索引在实时用的,迁移的过程当中很难保证重建之后的索引数据状态,需要做许多额外的工作,可能比这个过程更加复杂,更加漫长。

  1. keyword 字段建立索引之后,就需要修改代码了,来优化查询

    boolQueryBuilder.must(QueryBuilders.termsQuery("user_name.keyword",userNames));
    
  2. 修改完代码,可以在预发环境测试下,查询性能

优化 es 中 should 加 matchPhraseQuery 查询性能相关推荐

  1. ES中如何实现随机抽样查询

    一.场景说明 索引中有几千万的数据,现在需要每次查询随机抽样返回10条数据,怎么实现? 二.实现方式 DSL语句执行如下: GET myIndex/_search {"from": ...

  2. ES中如何实现对查询结果的二次排序

    一.场景说明 比如我们在CSDN中根据输入的关键词搜索博客文章,需要先根据关键词的相似度匹配排序,然后根据博客热度进行二次排序,保证热度比较高的博客文章优先被搜索到,提高用户的搜索体验. 那么,如何在 ...

  3. es中 term多字段查询

    es中 must 多字段查询可以如下操作 {"query": {"bool": {"must": [{"term":{& ...

  4. es分页查询重复数据_ES优化 - 巨量数据如何提高查询性能

    问题:如果数据量特别大,如何优化ES的查询性能? 可以从以下几个方面进行思考: File Cache可用的内存: ES的查询严重依赖OS的File Cache,所以说内存分配的内存肯定是越多越好.最理 ...

  5. Apache Iceberg 中引入索引提升查询性能

    动手点关注 干货不迷路 ‍ ‍Apache Iceberg 是一种开源数据 Lakehouse 表格式,提供强大的功能和开放的生态系统,如:Time travel,ACID 事务,partition ...

  6. 26、ES中使用mget批量查询api(学习笔记,来自课程资料 + 自己整理)

    1.批量查询的好处 一条一条的查询,比如说要查询100条数据,那么就要发送100次网络请求,这个开销还是很大的,如果批量查询的话,查询100条数据,就只要发送1次网络请求,网络请求的性能开销缩减100 ...

  7. 24.ES中什么是match查询?match查询可以做什么?如何使用match查询?嘻哈的简写笔记——Elastic Search

    1.什么是match查询?match查询可以做什么? 我们知道了term查询是不可以进行分词查询的,那么如何进行分词查询呢?就可以使用match查询: match查询属于高层查询,他会根据你查询的不同 ...

  8. 解决JQuery.Treeview在CI中无法加载查询函数的方法

    项目结构如下: UAS为IOIS项目下的一个子项目,由于CI对文件的访问都是相对于项目中的index.php的,所以URL的访问方式有两种: 1.直接使用CI的默认访问方式:url:"htt ...

  9. ES中如何实现like模糊查询

    问题描述: 我们都知道ES针对复杂的多添加组合查询非常强大,也知道通过match可以实现全文检索查询(分词查询),但是如果现在我只需要实现类似mysql中的like全匹配模糊查询,该怎么实现呢? 业务 ...

最新文章

  1. js 打开窗口window.open
  2. Embedding external files using [Embed] (转载:学习如何嵌入外部文件)
  3. 刘晓艳2021英语语法句型结构总结1之简单句型结构
  4. 不同php怎么传递参数,php – 将所有参数传递给另一个函数
  5. http服务详解(1)——一次完整的http服务请求处理过程
  6. 抽象类中不能有private的成员_【java基础】-- java接口和抽象类的异同分析
  7. Struts2中带参数的结果集
  8. Atitit.软件开发的几大规则,法则,与原则。。。attilax总结
  9. arm+linux书籍
  10. 点击链接时触发php文件,php点击链接直接下载文件写法
  11. 手机投屏到电视上怎么操作?
  12. 网络小说海外“走红”的启示
  13. 解决方案:rabbitmq使用场景-超时未支付订单处理
  14. 网页游戏对java的技术要求_网页制作谈谈什么技术是Java开发网页游戏的必要条件呢?怎样在微信公众平台上制作5级游戏?...
  15. springboot集成canal,实现缓存实时刷新,驼峰问题
  16. Arduino:设置ADC参考电压
  17. 基于vue3+ts+scss的后台管理系统(二)----excel的导入导出
  18. springboot+FreeMarker制作word模板
  19. Mac文件编码格式转换
  20. 数据结构-平衡二叉树(AVL树)

热门文章

  1. java-生成印章swing
  2. 准确率、召回率、F-measure值
  3. 图解Git分支和命令
  4. IOS开发--icon图标设置
  5. WORD点击索引目录提示错误信息
  6. arm linux死机不是崩溃,用sysrq-trigger实现ARM Linux一键内核崩溃、一键关机、一键dump信息等...
  7. Win10磁盘占用100%解决方法
  8. 常见日志框架介绍和对比(log4j,logback,log4j2)
  9. 2 OPENVINO : What is Video, what is computer vision, how do we accelerate it on modern computer
  10. AMOS实验——方差估计与假设检验