jjzjj

优化 es 中 should 加 matchPhraseQuery 查询性能

贪吃小龙 2023-09-01 原文

先说下优化的背景

我们后端的所有接口有一个质量属性的要求,就是保证我们的接口响应时长不能超过 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. ruby - ECONNRESET (Whois::ConnectionError) - 尝试在 Ruby 中查询 Whois 时出错 - 2

    我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.

  2. ruby-on-rails - rspec should have_select ('cars' , :options => ['volvo' , 'saab' ] 不工作 - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion在首页我有:汽车:VolvoSaabMercedesAudistatic_pages_spec.rb中的测试代码:it"shouldhavetherightselect"dovisithome_pathit{shouldhave_select('cars',:options=>['volvo','saab','mercedes','audi'])}end响应是rspec./spec/request

  3. ruby-on-rails - 在 Rails 和 ActiveRecord 中查询时忽略某些字段 - 2

    我知道我可以指定某些字段来使用pluck查询数据库。ids=Item.where('due_at但是我想知道,是否有一种方法可以指定我想避免从数据库查询的某些字段。某种反拔?posts=Post.where(published:true).do_not_lookup(:enormous_field) 最佳答案 Model#attribute_names应该返回列/属性数组。您可以排除其中一些并传递给pluck或select方法。像这样:posts=Post.where(published:true).select(Post.attr

  4. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  5. ES基础入门 - 2

    ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear

  6. sql - 查询忽略时间戳日期的时间范围 - 2

    我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时

  7. Ruby 的数字方法性能 - 2

    我正在使用Ruby解决一些ProjectEuler问题,特别是这里我要讨论的问题25(Fibonacci数列中包含1000位数字的第一项的索引是多少?)。起初,我使用的是Ruby2.2.3,我将问题编码为:number=3a=1b=2whileb.to_s.length但后来我发现2.4.2版本有一个名为digits的方法,这正是我需要的。我转换为代码:whileb.digits.length当我比较这两种方法时,digits慢得多。时间./025/problem025.rb0.13s用户0.02s系统80%cpu0.190总计./025/problem025.rb2.19s用户0.0

  8. ruby-on-rails - solr 清理查询 - 2

    我在Rails上使用带有ruby​​的solr。一切正常,我只需要知道是否有任何现有代码来清理用户输入,比如以?开头的查询。或* 最佳答案 我不知道执行此操作的任何代码,但理论上可以通过查看parsingcodeinLucene来完成并搜索thrownewParseException(只有16个匹配!)。在实践中,我认为您最好只捕获代码中的任何solr异常并显示“无效查询”消息或类似信息。编辑:这里有几个“sanitizer”:http://pivotallabs.com/users/zach/blog/articles/937-s

  9. ruby - Ruby 性能中的计时器 - 2

    我正在寻找一个用ruby​​演示计时器的在线示例,并发现了下面的代码。它按预期工作,但这个简单的程序使用30Mo内存(如Windows任务管理器中所示)和太多CPU有意义吗?非常感谢deftime_blockstart_time=Time.nowThread.new{yield}Time.now-start_timeenddefrepeat_every(seconds)whiletruedotime_spent=time_block{yield}#Tohandle-vesleepinteravalsleep(seconds-time_spent)iftime_spent

  10. ruby-on-rails - Rails 3 在一个查询中包含多个表 - 2

    我正在为锦标赛开发一个Rails应用程序。我在这个查询中使用了三个模型:classPlayertruehas_and_belongs_to_many:tournamentsclassTournament:destroyclassPlayerMatch"Player",:foreign_key=>"player_one"belongs_to:player_two,:class_name=>"Player",:foreign_key=>"player_two"在tournaments_controller的显示操作中,我调用以下查询:Tournament.where(:id=>params

随机推荐