首页 文章 精选 留言 我的

精选列表

搜索[es],共4396篇文章
优秀的个人博客,低调大师

ES设置字段搜索权重——Query-Time Boosting

Query-Time Boosting InPrioritizing Clauses, we explainedhow you could use theboostparameter at search time to give one query clause more importance than another. For instance: GET /_search { "query": { "bool": { "should": [ { "match": { "title": { "query": "quick brown fox", "boost": 2 } } }, { "match": { "content": "quick brown fox" } } ] } } } Thetitlequery clause is twice as important as thecontentquery clause, because it has been boosted by a factor of2. A query clause without aboostvalue has a neutral boost of1. 转自:https://www.elastic.co/guide/en/elasticsearch/guide/current/query-time-boosting.html 本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/bonelee/p/6475896.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

ES使用org.elasticsearch.client.transport.NoNodeAvailableException: No no...

1) 端口错 client = new TransportClient().addTransportAddress(new InetSocketTransportAddress(ipAddress, 9300)); 这里9300 写成9200的话会No node available 要是你连的不是本机,注意IP有没有正确 2 )jar报引用版本不匹配,开启的服务是什么版本,引用的jar最好匹配(这个我没有去试,反正我的是匹配的) 3) 要是你改了集群名字,还有设置集群名字 Settings settings = ImmutableSettings.settingsBuilder().put("cluster.name", "xxx").build(); client = new TransportClient(settings).addTransportAddress(new InetSocketTransportAddress(ipAddress, 9300)); 本文转自SummerChill博客园博客,原文链接:http://www.cnblogs.com/DreamDrive/p/6710158.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

ES使用脚本进行局部更新的排错记录

初学Elasticsearch,在按照《Elasticsearch服务器开发(第2版)》进行学习的过程中,在P17页中1.4.5 更新文档小节,使用脚本对文档进行局部更新的时候遇到了如下报错: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ~curl-XPOSThttp: //127 .0.0.1:9200 /blog/article/1/_update -d '{"script":"ctx._source.content=\"newcontent\""}' { "error" :{ "root_cause" :[{ "type" : "remote_transport_exception" , "reason" : "[LadyMandarin][127.0.0.1:9300][indices:data/write/update[s]]" } ], "type" : "illegal_argument_exception" , "reason" : "failedtoexecutescript" , "caused_by" :{ "type" : "script_exception" , "reason" : "scriptsoftype[inline],operation[update]andlang[groovy]aredisabled" }}, "status" :400 }% 我根据提示中的原因"scripts of type [inline], operation [update] and lang [groovy] are disabled"进行了查询,查到了官网文档关于对脚本更新的介绍(介绍链接)。 通过粗略查看文档,我发现要解决这个问题,需要在Elasticsearch的配置文件elasticsearch.yml中添加如下配置: script.engine.groovy.inline.update: on 由于Elasticsearch默认使用的是Groovy语言。Groovy语言一个快速且功能丰富的脚本语言,语法类似于Javascript。它在一个沙盒(sandbox)中运行,以防止恶意用户毁坏Elasticsearch或攻击服务器。 由于默认Elasticsearch没有打开Groovy的update权限,因此我们无法通过Groovy进行更新操作,通过上述配置打开权限之后,就可以进行更新操作了。 1 2 3 4 5 6 7 8 9 10 11 12 ~curl-XPOSThttp: //127 .0.0.1:9200 /blog/article/1/_update -d '{"script":"ctx._source.content=\"newcontent\""}' { "_index" : "blog" , "_type" : "article" , "_id" : "1" , "_version" :2, "_shards" :{ "total" :2, "successful" :1, "failed" :0 } }% 本文转自 aaao 51CTO博客,原文链接:http://blog.51cto.com/nolinux/1775232,如需转载请自行联系原作者

优秀的个人博客,低调大师

python统计ES存储空间占用的代码

import os from os.path import join, getsize def get_dir_size(dir, suffix_filter=None): size = 0L if not suffix_filter: for root, dirs, files in os.walk(dir): size += sum([getsize(join(root, name)) for name in files]) else: pos = len(suffix_filter) for root, dirs, files in os.walk(dir): size += sum([getsize(join(root, name)) for name in files if name[-pos:] == suffix_filter]) return size if __name__ == "__main__": suffix = [".doc", ".pos", ".tim", ".tip", ".dvd", ".dvm", ".fdt", ".fdx", ".fnm", ".nvd", ".nvm"] sums = [0]*len(suffix) print "total size,", suffix all_size = 0.0 for i in range(0, 20): dir_name = r'D:\exp\elasticsearch-2.4.1\data\elasticsearch\nodes\metadata-dis-2017.05.17-192.168.10.5-93001\%d' % i total_size = get_dir_size(dir_name) all_size += total_size print total_size, ",", for index,f in enumerate(suffix): filesize = get_dir_size(dir_name, f) print filesize, if index != len(suffix)-1: print ",", sums[index] += filesize print "" print "sum:==>" print all_size, ",", sums print suffix, "==>" print "rate:",["{:6.4f}".format(sums[index]/all_size) for index,f in enumerate(suffix)], "other rate:", "{:6.4f}".format((all_size-sum(sums))/all_size) 结果: total size, ['.doc', '.pos', '.tim', '.tip', '.dvd', '.dvm', '.fdt', '.fdx', '.fnm', '.nvd', '.nvm'] 1317898783 , 151402808 , 49137369 , 301883415 , 4172214 , 141468778 , 2638 , 584778163 , 251283 , 14098 , 84785648 , 766 1322178937 , 151260397 , 49087934 , 301549945 , 4152932 , 141485775 , 2614 , 583980598 , 251572 , 14281 , 90390501 , 786 1319649899 , 151221470 , 49071207 , 301534617 , 4170120 , 140942980 , 2638 , 583509290 , 251019 , 14372 , 88929787 , 796 1314658083 , 151146248 , 49063654 , 301531446 , 4160809 , 140569823 , 2638 , 583241932 , 250572 , 14098 , 84674494 , 766 1320535933 , 151531650 , 49176093 , 302016964 , 4169422 , 141155952 , 2574 , 584516976 , 249055 , 14280 , 87700579 , 786 1320124857 , 151273971 , 49132333 , 301772358 , 4170596 , 141370180 , 2646 , 583164239 , 251026 , 14372 , 88970738 , 796 1316604607 , 151108604 , 49028330 , 301445032 , 4157849 , 140884622 , 2646 , 583674391 , 249567 , 14190 , 86036998 , 776 1318000530 , 151303288 , 49096354 , 301799237 , 4165775 , 141048755 , 2622 , 584203445 , 250668 , 14189 , 86113818 , 776 1319766874 , 151220609 , 49114923 , 301645769 , 4164351 , 141300148 , 2614 , 583120289 , 249195 , 14372 , 88932205 , 796 1316689373 , 151233222 , 49121429 , 301457255 , 4162316 , 141250414 , 2614 , 583262641 , 250656 , 14189 , 85932258 , 776 1318198716 , 151174674 , 49109038 , 301525947 , 4166291 , 140899847 , 2670 , 583598013 , 251472 , 14281 , 87454094 , 786 1320522076 , 151321339 , 49123177 , 301757750 , 4169815 , 141420824 , 2638 , 583468296 , 252283 , 14372 , 88989183 , 796 1319417975 , 151437201 , 49178363 , 301895414 , 4172117 , 140741668 , 2598 , 584117457 , 252773 , 14281 , 87603715 , 786 1317865422 , 151324795 , 49079342 , 301808126 , 4162318 , 141039688 , 2590 , 584047373 , 251114 , 14189 , 86133508 , 776 1319633778 , 151143225 , 49103608 , 301496485 , 4162216 , 140890300 , 2630 , 583678114 , 252802 , 14372 , 88887627 , 796 1317628208 , 151325819 , 49158454 , 301771482 , 4164837 , 141049094 , 2622 , 583762862 , 250401 , 14190 , 86126068 , 776 1318628784 , 151251125 , 49101866 , 301657775 , 4163649 , 140656991 , 2622 , 583999255 , 251506 , 14281 , 87527326 , 786 1316949748 , 151240013 , 49118487 , 301544994 , 4157729 , 140576588 , 2606 , 583907402 , 251325 , 14189 , 86134036 , 776 1316521884 , 151169612 , 49090142 , 301601874 , 4157814 , 140947297 , 2670 , 583231827 , 250762 , 14189 , 86053318 , 776 1319378726 , 151381023 , 49179677 , 301868523 , 4171265 , 140769513 , 2614 , 584110351 , 251400 , 14280 , 87627691 , 786 sum:==> 26370853193.0 , [3025471093L, 982271780L, 6033564408L, 83294435L, 2820469237L, 52504L, 11675372914L, 5020451L, 285065L, 1745003592L, 15660L] ['.doc', '.pos', '.tim', '.tip', '.dvd', '.dvm', '.fdt', '.fdx', '.fnm', '.nvd', '.nvm'] ==> rate: ['0.1147', '0.0372', '0.2288', '0.0032', '0.1070', '0.0000', '0.4427', '0.0002', '0.0000', '0.0662', '0.0000'] other rate: 0.0000 本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/bonelee/p/6932271.html,如需转载请自行联系原作者

优秀的个人博客,低调大师

java api 调用es集群(1.7版本)

public static void main(String[] args) { Settings settings = ImmutableSettings.settingsBuilder() // client.transport.sniff=true // 客户端嗅探整个集群的状态,把集群中其它机器的ip地址自动添加到客户端中,并且自动发现新加入集群的机器 .put("client.transport.sniff", true).put("client", true)// 仅作为客户端连接 .put("data", false).put("cluster.name", "elasticsearch")// 集群名称 .build(); TransportClient client = new TransportClient(settings).addTransportAddress(new InetSocketTransportAddress("192.168.10.140", 9300))// TCP // 连接地址 .addTransportAddress(new InetSocketTransportAddress("192.168.10.124", 9300)); List<String> jsonData = BlogController.initData(); String json = "{" + "\"user\":\"金旭东\"," + "\"postDate\":\"2013-01-30\"," + "\"message\":\"测试elsaticsearch\"" + "}"; for (int i = 0; i < 1; i++) { IndexResponse response = client.prepareIndex("blog", "article").setSource(json).get(); if (response.isCreated()) { System.out.println("创建成功!"); } } client.close(); }

优秀的个人博客,低调大师

《Spring ES实战》第4招 搜索API怎么选

# Elasticsearch 查询三剑客:Repository、Operations 与 NativeQuery 的架构选择 ```text 旧方案:Repository 方法名承载一切查询 → 冲突:方法名爆炸、动态条件无法表达 → 后果:维护成本失控、搜索上下文丢失 ``` 很多团队把 Elasticsearch 当 JPA 用,所有查询都往 Repository 里塞。 一开始确实快,方法名一写,Spring Data 自动生成实现。 可一旦搜索条件从两个变成五个,方法名就长得像一篇文章。 我是老李,见过不少项目在 Repository 方法名超过 80 个字符时才开始反思,但那时调用链已经盘根错节。 为什么 Repository 方法名越来越长就是架构信号? 当稳定契约与动态搜索混在一起时,应该选择 Repository 还是 ElasticsearchOperations? 如果选错了查询入口,搜索上下文丢失后还能不能低成本找回? # 01、故事 ```text 周三 14:30 → 搜索服务组 → 任务:给商城后台加“品牌+类目+价格区间+状态+关键词”组合搜索 → 冲突:Repository 方法名写到一半写不下去 ``` 周三下午两点半,我坐在搜索服务组工位旁。任务是给商城后台加一个高级搜索:按品牌、类目、价格区间、状态、关键词任意组合过滤,还要按销量排序。 同事小周打开 ProductRepository,准备继续加方法。他写了几行,突然停住,屏幕上的方法名已经超过 120 个字符。 他转头问我:“老李,这方法名还能再长吗?再长 Checkstyle 要报警了。”我让他先别急着写,把现有查询方式列出来看看。 > 方法名长如藤蔓爬满墙 > 动态条件在接口里无处安放 > 稳定契约与灵活搜索纠缠成网 > 架构信号在编译警告里轻轻敲响 # 02、问题 ```text 需求变化:从固定两个条件 → 任意五个条件组合 → 冲突:Repository 方法名组合爆炸 → 后果:每加一个条件就要新增大量方法 ``` 小周列完发现,现有 Repository 已经有 23 个查询方法。品牌查询、类目查询、价格区间查询,每个都是独立方法。 现在要支持任意组合,五个条件的笛卡尔积是 31 种非空组合。如果继续用方法名派生,要么写 31 个方法,要么写一个超级长方法名。 更麻烦的是,搜索结果现在只返回 Product 实体。_score、highlight、sort values 这些搜索上下文全被丢弃了。运营想看“为什么这个商品排在前面”,根本拿不到依据。 可验证的验收标准是:同一个组合查询,三种写法返回的文档 ID 集合必须一致,且高级搜索能保留 _score 和排序值。 > 需求一变方法名就疯长 > 组合爆炸让接口失去形状 > 实体返回把搜索证据埋葬 > 排在前面的理由无人能讲 # 03、原理 ```text 核心机制:Repository 是稳定契约代理,ElasticsearchOperations 是查询构造主战场,NativeQuery 是完整能力逃生舱 ``` Spring Data Elasticsearch 有三层查询入口。Repository 基于方法名派生,适合固定条件、固定返回类型的简单 CRUD。 ElasticsearchOperations 提供 CriteriaQuery 和 StringQuery。 CriteriaQuery 用链式 API 构造动态条件,StringQuery 直接写 Elasticsearch DSL JSON。 NativeQuery 是 ElasticsearchOperations 里最接近底层 Java Client 的查询对象,能拿到 SearchHit、SearchHits、SearchPage,保 留 _score、highlight、sort values。 反直觉的是:Repository 底层也是用 ElasticsearchOperations 实现的。你以为 Repository 是另一套引擎,其实它只是 Operations 之上的一个薄代理。 > 三层入口不是三条平行河流 > Repository 只是 Operations 的上游 > 稳定契约适合方法名派生 > 动态搜索必须回到主战场运筹 # 04、架构 ```text 输入:搜索条件集合 + 分页排序参数 → 本模块:查询构造与执行 → 输出:SearchPage(含 _score、highlight、sort values)/ 边界:不负责索引映射与集群运维 ``` 架构上分三层。最上层是 Controller,负责接收 HTTP 参数并做基础校验。中间是 SearchService,负责把业务条件翻译成查询对象。 最下层是查询执行层。稳定查询走 Repository,动态查询走 ElasticsearchOperations,需要完整 DSL 能力时用 NativeQuery。 边界很清楚:本模块不管理索引映射,不处理集群健康状态,只负责查询构造、执行、结果上下文保留。 收益是查询逻辑集中、可测试、可复用。代价是 Service 层会比纯 Repository 方案多一层抽象。适用条件是搜索条件超过两个且存在动态组合。 > 三层架构各守边界 > 稳定与动态分开安放 > 查询上下文不再丢散 > 代价是多一层抽象要扛 # 05、第一版实战 ```text 环境:Spring Boot 3.2 + Spring Data Elasticsearch 5.2 → 配置 → 代码 → 启动 → 验证 → V1 ``` 先搭环境。pom.xml 引入 spring-boot-starter-data-elasticsearch,版本需按当前官方文档核验。 ```xml org.springframework.boot spring-boot-starter-data-elasticsearch ``` application.yml 配置连接: ```yaml spring: elasticsearch: uris: http://localhost:9200 username: elastic password: changeme ``` 定义 Product 实体: ```java @Document(indexName = "products") public class Product { @Id private String id; @Field(type = FieldType.Text) private String name; @Field(type = FieldType.Keyword) private String brand; @Field(type = FieldType.Keyword) private String category; @Field(type = FieldType.Double) private Double price; @Field(type = FieldType.Integer) private Integer status; @Field(type = FieldType.Integer) private Integer sales; // getters and setters } ``` V1 用 Repository 实现按品牌查询: ```java public interface ProductRepository extends ElasticsearchRepository { List findByBrand(String brand); } ``` 启动应用后验证: ```bash curl -X POST "http://localhost:8080/api/products/search" \ -H "Content-Type: application/json" \ -d '{"brand":"华为"}' ``` 示例输出: ```json { "code": 0, "data": [ {"id": "P001", "name": "华为 Mate 60", "brand": "华为", "price": 6999.0} ] } ``` 成功标准:按 brand 精确匹配返回文档列表。V1 完成,但只返回实体,没有 _score。 > 第一版跑通看似风平浪静 > 方法名派生让查询快速成型 > 实体返回却把上下文丢尽 > 隐患在需求变化前已经注定 # 06、排查 ```text 诊断树:动态组合查询失败 → 怀疑方法名派生不支持 → 检查 Spring Data 文档 → 证据:方法名参数超过 4 个可读性急剧下降 → 根因:用错了查询入口 → 修复:引入 ElasticsearchOperations ``` 第一个问题:小周尝试写 findByBrandAndCategoryAndPriceBetweenAndStatus,方法名超过 100 字符,编译通过但可读性极差。 怀疑是方法名派生有长度限制。检查 Spring Data Elasticsearch 官方文档,发现没有硬性长度限制,但参数超过 4 个时解析逻辑变得脆弱。 证据是:当参数顺序写错时,编译不报错,运行时才抛异常。比如把 price 和 status 顺序调换,方法名解析出的查询语义完全错误。 根因是 Repository 方法名派生不适合动态组合查询。修复方案是引入 ElasticsearchOperations,用 CriteriaQuery 构造动态条件。 **错误尝试:** 小周曾尝试用 @Query 注解写死 DSL,但动态条件无法在注解里拼装,每次条件变化都要改代码重新发布。 > 方法名解析像走钢丝 > 参数顺序错乱就摔得结实 > 注解写死 DSL 更是作茧自缚 > 动态查询必须换一条路子 # 07、优化 ```text V1:Repository 方法名派生 → 证据:动态组合无法表达、上下文丢失 → V2:ElasticsearchOperations + CriteriaQuery + NativeQuery ``` V2 引入 ElasticsearchOperations。先注入: ```java @Service public class ProductSearchService { private final ElasticsearchOperations operations; private final ProductRepository repository; public ProductSearchService(ElasticsearchOperations operations, ProductRepository repository) { this.operations = operations; this.repository = repository; } } ``` 用 CriteriaQuery 实现 category + priceRange 动态查询: ```java public SearchHits searchByCategoryAndPrice(String category, Double minPrice, Double maxPrice) { Criteria criteria = new Criteria(); if (category != null && !category.isBlank()) { criteria.and(new Criteria("category").is(category)); } if (minPrice != null && maxPrice != null) { criteria.and(new Criteria("price").between(minPrice, maxPrice)); } Query query = new CriteriaQuery(criteria); return operations.search(query, Product.class); } ``` 改用 SearchHits 返回,_score 和排序值都保留了。这是 V1 到 V2 的关键变化。 > 从方法名派生到链式构造 > 动态条件终于有了安身之道 > SearchHits 把上下文紧紧拥抱 > 搜索证据不再随风飘摇 # 08、演进 ```text V2:CriteriaQuery 动态组合 → 能力:保留 _score、highlight、sort values → 遗留:复杂 DSL 仍需 NativeQuery ``` V2 用 NativeQuery 实现 keyword + brand + category + status + sort 高级搜索: ```java public SearchPage advancedSearch(String keyword, String brand, String category, Integer status, int page, int size) { NativeQuery query = NativeQuery.builder() .withQuery(q -> q.bool(b -> { if (keyword != null && !keyword.isBlank()) { b.must(m -> m.multiMatch(mm -> mm .fields("name", "brand") .query(keyword))); } if (brand != null && !brand.isBlank()) { b.filter(f -> f.term(t -> t.field("brand").value(brand))); } if (category != null && !category.isBlank()) { b.filter(f -> f.term(t -> t.field("category").value(category))); } if (status != null) { b.filter(f -> f.term(t -> t.field("status").value(status))); } return b; })) .withSort(s -> s.field(f -> f.field("sales").order(org.springframework.data.domain.Sort.Order.desc("sales")))) .withPageable(PageRequest.of(page, size)) .build(); return operations.search(query, Product.class); } ``` 用 V1 同一组请求验证:brand=华为 的查询,V1 返回 List,V2 返回 SearchHits,文档 ID 一致,但 V2 多了 _score。 遗留问题:NativeQuery 的 Lambda DSL 虽然完整,但代码量比 CriteriaQuery 大,简单查询用它显得笨重。 > 三把剑各有锋芒 > Repository 守稳定契约的疆场 > CriteriaQuery 在动态组合里游荡 > NativeQuery 是完整能力的最后保障 # 09、洞见 ```text 从现象到判断:方法名长度是架构信号,返回类型决定上下文命运 ``` ## Repository 方法名超过 4 个参数就是架构信号 ```text 方法名长度 → 参数数量 → 查询复杂度 → 架构边界 ``` 方法名派生适合 1 到 3 个固定参数。超过 4 个参数,方法名本身就成了文档负担。这不是代码风格问题,是查询入口选错了。稳定契约和动态搜索必须分开。 ## 返回 Entity 等于主动丢弃搜索证据 ```text SearchHit → Entity → _score 丢失 → 排序理由消失 ``` 反直觉判断:你以为返回 Entity 更简洁,其实你在销毁信息。 _score 是排序依据,highlight 是用户体验依据,sort values 是分页游标依据。 丢弃它们,搜索质量无法度量、无法解释。 ## Repository 底层就是 ElasticsearchOperations ```text Repository → ElasticsearchOperations → Elasticsearch Java Client ``` 反直觉判断:很多人以为 Repository 和 Operations 是两套独立引擎。 实际上 Repository 是 Operations 的代理。 理解了这一点,你就明白为什么复杂查询应该直接用 Operations——少一层代理,多一分控制。 ## 三种查询方式不是互斥,而是分层 ```text 稳定契约 → Repository 动态搜索 → CriteriaQuery 完整能力 → NativeQuery ``` 架构上应该让它们各司其职。Repository 管固定查询,CriteriaQuery 管动态组合,NativeQuery 管复杂 DSL。混用不是问题,混在一个方法里才是问题。 > 方法名是架构的体温计 > 返回类型是上下文的容器 > 代理关系揭开层层面具 > 分层选择才是长久之计 # 10、系统落地 ```text 入口:Controller 接收搜索参数 → 服务:SearchService 路由查询 → 本模块:Repository/CriteriaQuery/NativeQuery 三层查询 → 存储:Elasticsearch 索引 → 未实现:高亮与聚合分析 ``` 原来系统只有 ProductRepository,所有查询都走方法名派生。本篇增加了 ProductSearchService,用 ElasticsearchOperations 承载动态查询。 现在系统能做什么:按品牌精确查询走 Repository,按类目+价格区间动态组合走 CriteriaQuery,高级搜索走 NativeQuery,且都保留搜索上下文。 下一篇缺什么:高亮显示和聚合分析还没实现。SearchHit 里的 highlight 字段已经能拿到,但前端还没消费。聚合需要引入 Aggregation 查询。 官方文档参考:Spring Data Elasticsearch 官方文档主页 https://docs.spring.io/spring-data/elasticsearch/docs/curren t/reference/html/ > 入口接收参数不慌不忙 > 服务路由查询各归其位 > 三层查询守住各自边疆 > 高亮聚合留待下篇开张 # 11、小结 ```text 三问三答:方法名变长是信号,动态搜索选 Operations,选错入口上下文丢失 ``` 为什么 Repository 方法名越来越长就是架构信号?因为方法名派生只适合稳定契约,参数超过 4 个说明查询已经动态化,继续用 Repository 就是硬撑。 稳定契约与动态搜索混在一起时选谁?稳定查询选 Repository,动态搜索选 ElasticsearchOperations。两者不互斥,分层使用。 选错入口的后果?返回 Entity 会丢失 _score、highlight、sort values,搜索质量无法度量,排序理由无法解释,后期补救成本高。 当前状态:V2 已落地,三种查询方式分层使用,SearchHits 和 SearchPage 已接入,搜索上下文完整保留。 > 方法名变长是信号不是偶然 > 动态搜索要回到 Operations 主场 > 选错入口上下文就会丢散 > 分层使用才能行稳致远 # 12、作业 ```text 理解 → 实战 → 排障 → 架构判断 ``` ## 理解题:Repository 方法名派生和 CriteriaQuery 的本质区别是什么? Repository 方法名派生是编译期固定契约,查询条件在方法名里静态定义,适合 1 到 3 个固定参数。 CriteriaQuery 是运行时动态构造,条件可以按需拼装,适合任意组合的动态搜索。 前者是稳定契约代理,后者是查询构造主战场。 ## 实战题:用 NativeQuery 实现一个多字段关键词搜索,要求保留 _score 并高亮 name 字段 使用 NativeQuery.builder() 的 withQuery 构造 multiMatch,withHighlight 配置 name 字段高亮。 返回 SearchHits,从 SearchHit 中取 getHighlightFields() 获取高亮片段,getScore() 获取 _score。 ## 排障题:CriteriaQuery 构造的 between 查询返回空结果,但用 DSL 直接查有数据,可能是什么原因? 最可能的原因是字段类型不匹配。 between 查询要求字段是数值类型,如果 price 字段在索引里被映射为 text 或 keyword,between 不会报错但返回空。 检查索引映射,确认 price 是 double 类型。 另一个可能是 minPrice 和 maxPrice 传反了。 ## 架构判断题:团队坚持所有查询都写在 Repository 里,理由是“统一入口好维护”。这个判断对吗? 不对。统一入口不等于统一实现。 Repository 适合稳定契约,动态搜索硬塞进去会导致方法名爆炸、上下文丢失。 正确的架构是入口统一在 Service 层,实现按查询复杂度分层:Repository 管固定查询,ElasticsearchOperations 管动态查询。 统一的是调用入口,不是实现方式。 > 理解本质才能选对入口 > 实战保留上下文才算数 > 排障先查映射再查参数 > 架构判断要看长期维护 # 13、升华 ```text 工程结论:查询入口的选择是架构决策,不是代码风格偏好 ``` 有依据的结论是:Repository 方法名长度是查询复杂度的温度计。当方法名超过 4 个参数,说明稳定契约已经变成动态搜索,必须切换到 ElasticsearchOperations。 返回类型决定了搜索上下文能否存活。返回 Entity 是主动丢弃证据,返回 SearchHit 和 SearchPage 才能支撑搜索质量度量与解释。 下一篇可以继续深入高亮与聚合分析,把 SearchHit 里的 highlight 字段消费掉,用 Aggregation 实现类目聚合和价格分布。 > 查询入口是架构的岔路口 > 方法名长度是选择的理由 > 上下文保留是质量的守候 > 分层使用是长久的计谋 **系列项目:** 商城搜索服务,服务对象为运营后台与 C 端用户 **已完成:** 商品索引映射与基础 CRUD,Repository 按品牌查询 **本篇:** Repository、ElasticsearchOperations、CriteriaQuery、StringQuery、NativeQuery 三层查询体系,保留 _score、highlight、sort values **下一篇:** 高亮显示与聚合分析,消费 highlight 字段,实现类目聚合与价格分布

优秀的个人博客,低调大师

《Spring ES实战》第7招 聚合做搜索导航

# 搜索“手机”时,左侧筛选导航是怎么算出来的 ```text 旧方案:Query 拉回全部文档 → Java Stream groupBy 统计品牌/价格/分类 冲突:结果集可能几十万条,内存与网络传输直接爆掉 后果:接口超时、GC 频繁、用户看到筛选导航前已经流失 ``` 电商搜索页有一个看似简单、实则决定体验的组件:左侧筛选导航。 用户搜“手机”,页面左侧立刻出现 Apple、Huawei、Xiaomi 的品牌数量,出现 1000-2000 元、2000-4000 元的价格区间,出现“5G 手机”“折叠屏手机”等分类数量。 我是老李,一个在深圳做了多年搜索后端的人。 最早我也觉得这不就是把搜索结果拉回来,再用 Java Stream groupBy 统计一下吗? 直到一次大促压测,我才真正理解为什么 Elasticsearch 要把 Aggregation 设计成独立于 Query 的查询能力。 为什么先拉回全部搜索结果再用 Java Stream groupBy 是错误方向? 面对“搜索结果”与“筛选统计”这两个目标,应该选择 Query 召回文档、Aggregation 描述结果集合,还是选择一条 SQL 或一次查询把两者混在一起? 如果选错方向,把统计压力全部压到应用内存和网络传输上,会带来什么后果? # 01、故事 ```text 2024 年 11 月 11 日 00:18 → 深圳南山某电商搜索中台 → 任务是让“手机”搜索页左侧筛选导航可用 → 冲突:大促流量上来后接口开始超时 ``` 那天晚上我在公司盯着监控大屏。任务是让搜索“手机”时,左侧筛选导航能实时显示品牌、价格区间和分类数量。 凌晨刚过,流量开始爬升。搜索接口的 P99 延迟从 180ms 一路涨到 3 秒以上,错误率也开始冒头。 我打开日志,发现大量请求卡在应用层。有人在代码里写了全量拉取结果集,然后用 Java Stream 做 groupBy 统计。 大促期间“手机”这个关键词能命中几十万条商品文档。每次搜索都把这些文档的 _source 全部拉到应用内存里,再一条条分组统计。 我当时心里一沉:这不是搜索,这是在把 Elasticsearch 当数据库全表扫描用。 > 凌晨大促流量涌, > 手机搜索卡成钟。 > 全量拉回再分组, > 内存告急一场空。 # 02、问题 ```text 需求变化:从“只要搜索结果列表”变成“结果列表 + 品牌/价格/分类统计” 冲突:应用层用 Java Stream groupBy 统计,必须先把命中的几十万文档全部拉到内存 后果:网络传输巨大、内存暴涨、GC 频繁、接口超时,筛选导航反而拖垮搜索主链路 ``` 失效的直接原因是:Query 的职责是召回文档,它返回的是文档列表。但筛选导航需要的是“对结果集合的描述”,不是文档本身。 用 Java Stream groupBy 做统计,意味着你必须先把所有命中的文档从 Elasticsearch 传输到应用服务器。 “手机”这个词在大促期间命中几十万条商品,每条 _source 可能几 KB 到几十 KB。一次请求传输几十 MB 甚至上百 MB 数据,网络首先扛不住。 数据到了应用层,几十万条文档对象进入堆内存,groupBy 过程中还会产生大量临时对象。GC 压力直接拉满。 可验证的验收标准是:搜索“手机”时,接口在 200ms 内同时返回结果列表和品牌、价格区间、分类统计,且应用内存占用不随命中文档数线性增长。 > 召回文档是本职, > 描述集合是另一事。 > 错把统计当列表, > 全量拉回必出事。 # 03、原理 ```text 核心机制:Query 负责召回文档,Aggregation 负责在结果集合上做 Bucket/Metric 统计,两者在同一轮查询中并行执行 ``` Elasticsearch 的 Aggregation 是在 Query 召回的结果集合上执行的。它不会返回文档本身,而是返回对结果集合的描述。 Bucket Aggregation 负责分组:terms 按字段值分桶,range 按数值区间分桶,date_histogram 按时间间隔分桶。 Metric Aggregation 负责计算:avg 算平均值,sum 算总和,cardinality 算去重数量。 一个反直觉的判断是:Aggregation 不是“先查出来再统计”,而是在每个分片上先局部聚合,再在协调节点上合并。这意味着统计计算发生在数据所在的地方,而不是应用内存里。 另一个反直觉的点:terms 聚合返回的 doc_count 默认是近似值,不是精确值。因为分片间合并时,每个分片只返回 top N 个桶,可能漏掉某些分片上的长尾品牌。 > 召回文档靠 Query, > 描述集合靠 Agg。 > 分片先算局部桶, > 协调节点再合并。 # 04、架构 ```text 输入:用户搜索词“手机” + 筛选条件(可选) 本模块:Query 召回结果集 → 在同一结果集上执行 terms/range/date_histogram + avg/sum/cardinality 输出:文档列表 + 品牌桶 + 价格区间桶 + 分类桶 边界:Aggregation 只描述 Query 命中的结果集合,不跨出该集合 ``` 整体分三层。第一层是 Query 层,负责召回文档,决定哪些商品属于“手机”这个搜索结果集合。 第二层是 Aggregation 层,在结果集合上并行执行多个聚合。品牌用 terms,价格用 range,分类用 terms,平均价格用 avg,去重品牌数用 cardinality。 第三层是响应组装层,把文档列表和聚合结果合并成一个响应返回给前端。 收益是统计计算下沉到 Elasticsearch,应用层不再需要拉全量数据。网络传输从几十 MB 降到几 KB。 代价是 Elasticsearch 集群的 CPU 和内存消耗增加,聚合查询比纯 Query 更重。 适用条件是:统计维度来自结果集合本身,且维度基数可控。品牌、分类这类低基数字段非常适合 terms 聚合。 > 三层架构各司职, > 召回统计两不误。 > 网络传输降百倍, > 集群压力要盯住。 # 05、第一版实战 ```text 环境:Elasticsearch 8.11 + Java 17 + Spring Boot 3.2 配置:创建 products 索引,mapping 定义 brand/keyword、category/keyword、price/double、created_at/date 代码:SearchService 中用 ElasticsearchClient 构建 Query + Aggregation 启动:mvn spring-boot:run 验证:curl 请求搜索接口,观察聚合结果 V1:返回文档列表 + 品牌桶 + 价格区间桶 + 分类桶 ``` 先创建索引。mapping 中 brand 和 category 用 keyword 类型,price 用 double,created_at 用 date。 ```json PUT /products { "mappings": { "properties": { "name": { "type": "text" }, "brand": { "type": "keyword" }, "category": { "type": "keyword" }, "price": { "type": "double" }, "created_at": { "type": "date" } } } } ``` 写入几条测试数据。 ```json POST /products/_bulk { "index": { "_id": "1" } } { "name": "iPhone 15 Pro", "brand": "Apple", "category": "5G手机", "price": 7999, "created_at": "2024-09-01T00:00:00Z" } { "index": { "_id": "2" } } { "name": "Huawei Mate 60", "brand": "Huawei", "category": "5G手机", "price": 5999, "created_at": "2024-09-02T00:00:00Z" } { "index": { "_id": "3" } } { "name": "Xiaomi 14", "brand": "Xiaomi", "category": "5G手机", "price": 3999, "created_at": "2024-09-03T00:00:00Z" } { "index": { "_id": "4" } } { "name": "Redmi Note 13", "brand": "Xiaomi", "category": "4G手机", "price": 1299, "created_at": "2024-09-04T00:00:00Z" } ``` 在 SearchService 中构建查询。Query 用 match 搜索“手机”,Aggregation 用 terms 统计品牌、range 统计价格区间、terms 统计分类。 ```java SearchRequest request = SearchRequest.of(s -> s .index("products") .query(q -> q.match(m -> m.field("name").query("手机"))) .aggregations("brand_agg", a -> a.terms(t -> t.field("brand").size(10))) .aggregations("price_agg", a -> a.range(r -> r .field("price") .ranges(r1 -> r1.to(2000)) .ranges(r2 -> r2.from(2000).to(4000)) .ranges(r3 -> r3.from(4000)))) .aggregations("category_agg", a -> a.terms(t -> t.field("category").size(10))) .size(10) ); ``` 启动应用后,用 curl 验证。 ```bash curl -X POST "http://localhost:8080/api/search?keyword=手机" -H "Content-Type: application/json" ``` 示例输出: ```json { "hits": [ { "name": "iPhone 15 Pro", "brand": "Apple", "price": 7999 }, { "name": "Huawei Mate 60", "brand": "Huawei", "price": 5999 } ], "aggregations": { "brand_agg": { "buckets": [ { "key": "Xiaomi", "doc_count": 2 }, { "key": "Apple", "doc_count": 1 }, { "key": "Huawei", "doc_count": 1 } ] }, "price_agg": { "buckets": [ { "key": "*-2000.0", "doc_count": 1 }, { "key": "2000.0-4000.0", "doc_count": 1 }, { "key": "4000.0-*", "doc_count": 2 } ] }, "category_agg": { "buckets": [ { "key": "5G手机", "doc_count": 3 }, { "key": "4G手机", "doc_count": 1 } ] } } } ``` 成功标准:一次请求同时返回文档列表和三个维度的聚合统计,应用层没有拉取全量文档。 > 索引建好字段清, > 查询聚合一起行。 > 一次请求双结果, > 筛选导航初成形。 # 06、排查 ```text 诊断树: 现象1:terms 聚合返回的品牌数量不准确 → 怀疑:分片间合并时 top N 截断 → 检查:查看索引分片数和 terms 聚合的 size 参数 → 证据:索引有 5 个分片,size 设为 10,但某个品牌只在一个分片上排第 11 名 → 根因:terms 聚合在分片级取 top N,协调节点合并时可能漏掉长尾桶 → 修复:调大 shard_size 参数,或使用 cardinality 聚合做精确去重统计 现象2:聚合查询在大促期间 CPU 飙高 → 怀疑:聚合在 Query 命中的大结果集上执行,计算量过大 → 检查:查看 Elasticsearch 慢查询日志和节点 CPU 监控 → 证据:慢日志显示聚合查询耗时 800ms,节点 CPU 使用率超过 90% → 根因:range 聚合在 double 字段上执行,且结果集过大 → 修复:将 price 字段改为 scaled_float 或使用 index sorting 优化范围查询 ``` 第一个问题是品牌数量不准确。测试环境索引有 5 个分片,terms 聚合的 size 设为 10。 某个小众品牌只在其中一个分片上排第 11 名,分片级 top 10 截断后,协调节点合并时根本看不到这个品牌。 检查命令: ```bash curl -X GET "http://localhost:9200/products/_settings?pretty" curl -X GET "http://localhost:9200/products/_search?pretty" -H "Content-Type: application/json" -d '{ "size": 0, "aggs": { "brand_agg": { "terms": { "field": "brand", "size": 10 } } } }' ``` 修复方式是把 shard_size 调大,让每个分片返回更多候选桶。 ```json { "size": 0, "aggs": { "brand_agg": { "terms": { "field": "brand", "size": 10, "shard_size": 100 } } } } ``` 第二个问题是聚合查询 CPU 飙高。慢日志显示聚合耗时 800ms,节点 CPU 超过 90%。 **错误尝试:** 我一开始以为是 Query 太慢,给 match 查询加了 minimum_should_match,结果聚合还是慢。后来才发现瓶颈在 range 聚合对 double 字段的遍历计算上。 最终把 price 字段类型从 double 改为 scaled_float,减少浮点运算开销。 > 分片合并有截断, > 长尾品牌被漏算。 > 调大 shard_size 补, > 浮点字段换缩放。 # 07、优化 ```text V1:terms/range 聚合直接返回统计结果 证据:品牌数量不准确 + 聚合 CPU 飙高 V2:shard_size 调大 + price 改 scaled_float + 增加 cardinality 去重统计 ``` V1 的问题是 terms 聚合的近似性导致品牌数量不准确,range 聚合在 double 字段上计算开销大。 V2 做了三处改动。第一,terms 聚合增加 shard_size 参数,让每个分片返回更多候选桶,减少合并时的截断误差。 第二,price 字段从 double 改为 scaled_float,用整数存储价格,减少浮点运算开销。 ```json PUT /products_v2 { "mappings": { "properties": { "name": { "type": "text" }, "brand": { "type": "keyword" }, "category": { "type": "keyword" }, "price": { "type": "scaled_float", "scaling_factor": 100 }, "created_at": { "type": "date" } } } } ``` 第三,增加 cardinality 聚合,精确统计去重品牌数。 ```java .aggregations("brand_count", a -> a.cardinality(c -> c.field("brand"))) ``` 改动后重新验证,品牌数量准确,聚合耗时从 800ms 降到 120ms。 > 近似统计有误差, > 调大参数补偏差。 > 浮点换整降开销, > 去重统计用基数。 # 08、演进 ```text V2:shard_size 调大 + scaled_float + cardinality 能力:品牌/价格/分类统计准确,聚合性能可控 遗留:date_histogram 时间维度聚合尚未接入,筛选条件与聚合的联动尚未实现 ``` 用 V1 同一组请求前后验证。优化前,品牌数量偶尔少 1-2 个,聚合耗时 800ms。优化后,品牌数量稳定准确,聚合耗时 120ms。 收益是筛选导航的数据准确性和响应速度都达到上线标准。应用层内存占用不再随命中文档数增长。 代价是 shard_size 调大会增加每个分片的计算量和内存消耗,需要在准确性和性能之间找平衡。 适用条件是:品牌、分类这类低基数字段适合 terms 聚合,价格这类数值字段适合 range 聚合。 遗留问题是:用户点击某个品牌后,其他维度的统计应该基于筛选后的结果集合重新计算,这个联动逻辑还没实现。 > 优化之后数据准, > 响应速度也提升。 > 筛选联动还未做, > 时间维度待接入。 # 09、洞见 ```text 从现象到判断:搜索结果告诉用户“有什么”,Aggregation 告诉用户“接下来还能怎么找” ``` ## 搜索结果与筛选导航是两种不同的信息需求 ```text Query 回答“有什么”,Aggregation 回答“还能怎么找” ``` 搜索结果列表回答的是“有哪些商品符合条件”,而筛选导航回答的是“这些商品分布在哪些品牌、哪些价格区间、哪些分类里”。这是两种完全不同的信息需求,不应该用同一种技术手段解决。 ## 反直觉判断:Aggregation 不是“先查再统计” ```text 聚合发生在分片内部,不是应用内存里 ``` 很多人以为 Aggregation 是 Elasticsearch 把文档查出来再统计。 实际上,每个分片先在自己的数据上做局部聚合,协调节点再合并局部结果。 统计计算发生在数据所在的地方,这是它能高效的根本原因。 ## 反直觉判断:terms 聚合的 doc_count 默认是近似值 ```text 分片级 top N 截断导致长尾桶可能丢失 ``` terms 聚合在每个分片上只返回 top N 个桶,协调节点合并时可能漏掉某些分片上的长尾品牌。要获得更准确的结果,需要调大 shard_size,或者用 cardinality 聚合做去重统计。 ## 筛选导航的价值在于引导下一步搜索 ```text 用户点“Apple”不是过滤,是发起一次新的搜索 ``` 筛选导航上的每个数字都在告诉用户:如果你点这个条件,会得到多少结果。这是一种搜索引导,不是简单的数据展示。设计聚合查询时,要站在用户下一步会怎么找的角度来思考。 > 搜索结果有何物, > 聚合指引怎么找。 > 分片先算局部桶, > 近似统计要知晓。 # 10、系统落地 ```text 入口:用户搜索“手机” 服务:搜索中台接收请求,构建 Query + Aggregation 本模块:Elasticsearch 执行 Query 召回 + terms/range/cardinality 聚合 存储:products 索引,brand/category 为 keyword,price 为 scaled_float 未实现:筛选条件与聚合的联动、date_histogram 时间维度聚合 ``` 原来系统只有 Query 召回文档列表的能力。用户搜索“手机”,只返回商品列表,没有筛选导航。 本篇增加了 Aggregation 能力,一次请求同时返回品牌桶、价格区间桶、分类桶和去重品牌数。 现在系统能做的是:用户搜索“手机”时,左侧筛选导航实时显示各品牌、价格区间、分类的商品数量。 下一篇缺的是:用户点击某个筛选条件后,其他维度的统计基于筛选后的结果集合重新计算,以及 date_histogram 时间维度聚合的接入。 官方文档参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html > 原来只有召回力, > 本篇加上聚合器。 > 筛选导航已成形, > 联动逻辑待补齐。 # 11、小结 ```text 三问三答: 为什么 Java Stream groupBy 是错误方向? 选择 Query + Aggregation 还是全量拉回? 选错方向会带来什么后果? ``` 第一问:为什么 Java Stream groupBy 是错误方向?因为 Query 命中的结果集可能几十万条,全量拉回应用层再做统计,网络传输和内存消耗都不可控。 第二问:选择 Query + Aggregation。Query 负责召回文档,Aggregation 在结果集合上做统计,两者在同一轮查询中并行执行,统计计算下沉到 Elasticsearch。 第三问:选错方向会导致接口超时、GC 频繁、内存暴涨,筛选导航反而拖垮搜索主链路。 当前状态:V2 已实现品牌、价格区间、分类三个维度的聚合统计,数据准确性和性能均达到上线标准。 > 三问三答已清晰, > 聚合方向不可移。 > 统计下沉到集群, > 应用内存得喘息。 # 12、作业 ```text 理解 → 实战 → 排障 → 架构判断 ``` ## 理解题:为什么 terms 聚合的 doc_count 默认是近似值? 因为 terms 聚合在每个分片上只返回 top N 个桶,协调节点合并时可能漏掉某些分片上的长尾桶。分片级截断是近似性的根源。 ## 实战题:在 products 索引上实现一个 date_histogram 聚合,按月份统计商品数量 使用 date_histogram 聚合,field 设为 created_at,calendar_interval 设为 month。 ```json { "size": 0, "aggs": { "monthly_agg": { "date_histogram": { "field": "created_at", "calendar_interval": "month" } } } } ``` ## 排障题:用户反馈筛选导航上某个品牌数量比实际搜索结果少,如何排查? 先检查索引分片数,再查看 terms 聚合的 size 和 shard_size 参数。 如果 shard_size 小于该品牌在某个分片上的排名,就会漏掉。 调大 shard_size 或改用 cardinality 聚合验证。 ## 架构判断题:把 Aggregation 结果缓存到 Redis,用户再次搜索时直接读缓存,这个方案是否合理? 不合理。聚合结果依赖 Query 命中的结果集合,不同搜索词、不同筛选条件下的结果集合不同,缓存命中率低且容易返回过期数据。 应该让 Elasticsearch 实时计算聚合,或使用 Elasticsearch 自身的查询缓存。 > 理解近似知根源, > 实战时间分桶练。 > 排障先查分片数, > 架构判断缓存险。 # 13、升华 ```text 工程结论:搜索系统的核心能力是“召回 + 描述”,Query 与 Aggregation 各司其职,不可混为一谈 ``` 有依据的结论是:搜索结果告诉用户“有什么”,Aggregation 告诉用户“接下来还能怎么找”。这是两种不同的信息需求,需要两种不同的技术手段。 把统计下沉到 Elasticsearch,不是简单的性能优化,而是架构上的职责分离。应用层不应该承担描述结果集合的责任。 下一篇可以深入探讨:用户点击筛选条件后,如何实现多维度聚合的联动计算,以及 date_histogram 在时间维度筛选导航中的应用。 > 召回描述两分明, > 各司其职架构清。 > 统计下沉集群算, > 搜索导航一路行。 **系列项目:** 电商搜索中台,服务商品搜索与筛选导航 **已完成:** Query 召回文档列表 **本篇:** Aggregation 实现品牌、价格区间、分类筛选导航 **下一篇:** 筛选条件联动与时间维度聚合

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册