SpringDataElasticsearch聚合实现过滤搜索

news/2025/10/14 23:19:14/文章来源:https://www.cnblogs.com/chucz/p/19142296

SpringDataElasticsearch聚合实现过滤搜索

过滤功能分析

整个过滤部分有3块:

  • 顶部的导航,已经选择的过滤条件展示:
    • 商品分类面包屑
    • 其它已选择过滤参数
  • 过滤条件展示,又包含3部分
    • 商品分类展示
    • 品牌展示
    • 其它规格参数
  • 展开或收起的过滤条件的按钮

顶部导航要展示的内容跟用户选择的过滤条件有关。这部分需要依赖第二部分:过滤条件的展示和选择。展开或收起的按钮是否显示,取决于过滤条件有多少,如果很少,那么就没必要展示。所以也是跟第二部分的过滤条件有关。

先做第二部分:过滤条件展示。

分类和品牌过滤条件 获取和展示

数据库中已经有所有的分类和品牌信息。无论是分类信息,还是品牌信息,都应该从搜索的结果商品中进行聚合得到。

扩展返回的结果

返回的结果PageResult对象,里面只有total、totalPage、items3个属性。现在要对商品分类和品牌进行聚合,添加分类和品牌的数据。

分类:页面显示了分类名称,但背后肯定要保存id信息。所以至少要有id和name

品牌:页面展示的有logo,有文字,当然肯定有id,基本上是品牌的完整数据

新建一个类,继承PageResult,然后扩展两个新的属性:分类集合和品牌集合:

package com.deltaqin.search.pojo;import com.deltaqin.common.vo.PageResult;
import com.deltaqin.item.pojo.Brand;
import com.deltaqin.item.pojo.Category;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.List;@Data
@NoArgsConstructor
public class SearchResult extends PageResult<Goods> {private List<Category> categories;private List<Brand> brands;public SearchResult(Long total, Integer totalPage, List<Goods> items, List<Category> categories, List<Brand> brands) {super(total, totalPage, items);this.categories = categories;this.brands = brands;}
}

聚合商品分类和品牌

修改搜索的业务逻辑,对分类和品牌聚合。

因为索引库中只有id,所以根据id聚合,然后再根据id去查询完整数据。所以,商品微服务需要提供一个接口:根据品牌id集合,批量查询品牌。

修改SearchService:

// 二、返回搜索结果public PageResult<Goods> search(SearchRequest request) {// 获取并判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品String key = request.getKey();if (StringUtils.isBlank(key)) {return null;}// 0、构建查询条件NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 1、搜索条件:对key进行全文检索查询// 定义基本查询条件QueryBuilder basicQuery = QueryBuilders.matchQuery("all", key).operator(Operator.AND);queryBuilder.withQuery(basicQuery);// 2、结果过滤:通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitlequeryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null));// 3、分页// 3、准备分页参数int page = request.getPage();int size = request.getSize();queryBuilder.withPageable(PageRequest.of(page - 1, size));// 4、排序String sortBy = request.getSortBy();Boolean desc = request.getDescending();if (StringUtils.isNotBlank(sortBy)) {queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));}// 5、聚合分类和品牌// 聚合字段:cid3 brandIdString categoryAggName = "categories";String brandAggName = "brands";queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));// 4、查询,获取结果//        Page<Goods> pageInfo = this.goodsRepository.search(queryBuilder.build());AggregatedPage<Goods> result = elasticsearchTemplate.queryForPage(queryBuilder.build(), Goods.class);// 5、处理结果,获取分页数和结果数Long totalElements = result.getTotalElements();//        int totalPages = pageInfo.getTotalPages();int totalPages = (totalElements.intValue() + size - 1) / size;// 5、解析聚合结果Aggregations aggs = result.getAggregations();List<Category> categories = parseCategoryAgg(aggs.get(categoryAggName));List<Brand> brands = parseBrandAgg(aggs.get(brandAggName));// 解析封装结果并返回return new SearchResult(totalElements, totalPages, result.getContent(), categories, brands);}private List<Brand> parseBrandAgg(LongTerms aggregation) {try {// 最后使用collect将流收集成集合List<Long> ids = aggregation.getBuckets().stream().map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList());List<Brand> brands = brandClient.queryBrandByIds(ids);return brands;} catch (Exception e) {log.error("[搜索服务]查询品牌异常:", e);return null;}}private List<Category> parseCategoryAgg(LongTerms aggregation) {try {// 最后使用collect将流收集成集合List<Long> ids = aggregation.getBuckets().stream().map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList());List<Category> category = categoryClient.queryNameByIds(ids);return category;} catch (Exception e) {log.error("[搜索服务]查询分类异常:", e);return null;}}

1581654907149-164ce52d-5048-480f-947b-0a28cd6a5e27.png

页面渲染数据

过滤参数数据结构

分类、品牌内容结构相似,都是key和value的结构。可以把所有的过滤条件放入一个数组中,然后在页面利用v-for遍历一次生成。

[{k:"过滤字段名",options:[{/*过滤字段值对象*/},{/*过滤字段值对象*/}]}
]

先在data中定义数组:filters,等待组装过滤参数:

data: {ly,search:{key: "",page: 1},goodsList:[], // 接收搜索得到的结果total: 0, // 总条数totalPage: 0, // 总页数filters:[] // 过滤参数集合
},

然后在查询搜索结果的回调函数中,对过滤参数进行封装,所有的过滤条件的数据结构都是这样的:key是过滤条件的名字,value是json对象组成的对象。

1581661730101-41b9345a-43c8-4671-bbe7-7b94652c5727.png

然后刷新页面,注意上面是在network里面查看后端返回的数据,这里应该在Vue工具里面查看实例里面已经添加的数据。通过浏览器工具,查看封装的结果:

1581661760575-e5a16ef0-e287-442c-871a-08b599e75928.png

页面渲染数据

虽然页面元素是一样的,但是品牌会比其它搜索条件多出一些样式,因为品牌是以图片展示。需要进行特殊处理。数据展示是一致的,采用v-for处理:

<!--显示分类-->
<div class="type-wrap" v-for="f in filters" :key="f.k" v-if="f.k !== 'brandId'"><div class="fl key" v-text="f.k === 'cid3' ? '分类' : 'f.k'"></div><div class="fl value"><ul class="type-list"><li v-for="(o,i) in f.options" :key="i"></li></ul></div><div class="fl ext"></div>
</div>
<!--显示品牌-->
<div class="type-wrap logo" v-else><div class="fl key brand">品牌</div><div class="value logos"><ul class="logo-list" v-for="(o,i) in f.options" :key="i"><li v-if="o.image"><img :src="o.image"/></li><li v-else></li></ul></div><div class="fl ext"><a href="javascript:void(0);" class="sui-btn">多选</a></div>
</div>

规格参数过滤条件 获取和展示

如果用户尚未选择商品分类,或者聚合得到的分类数大于1,那么就没必要进行规格参数的聚合。因为不同分类的商品,其规格是不同的。因此,在后台需要对聚合得到的商品分类数量进行判断,如果等于1,才继续进行规格参数的聚合。不能把索引库中的所有规格参数都拿来过滤。因为并不是所有的规格参数都可以用来过滤,参数的值是不确定的。在设计规格参数时,已经标记了某些规格可搜索,某些不可搜索。

因此,一旦商品分类确定,就可以根据商品分类查询到其对应的规格,从而知道哪些规格要进行搜索。虽然数据库中有所有的规格参数,但是不能把一切数据都用来供用户选择。与商品分类和品牌一样,应该是从用户搜索得到的结果中聚合,得到与结果品牌的规格参数可选值。

  • 1)用户搜索得到商品,并聚合出商品分类
  • 2)判断分类数量是否等于1,如果是则进行规格参数聚合
  • 3)先根据分类,查找可以用来搜索的规格
  • 4)对规格参数进行聚合
  • 5)将规格参数聚合结果整理后返回

修改返回结果SearchResult

前面的返回结果中需要增加新数据,用来保存规格参数过滤条件。

在Vue实例里面存放的是下面map格式的数组,所以希望可以得到相似的结构就不用处理直接放在实例里面使用,上面传递分类和品牌的时候没有使用这种而是直接返回数组,在前端转换为map格式的数据。下面的规格参数选择在后端返回之前就将数据封装好:

[{"k":"规格参数名","options":["规格参数 值","规格参数值"]}
]

用List<Map<String, Object>>来表示。

@Data
@NoArgsConstructor
public class SearchResult extends PageResult<Goods> {private List<Category> categories;private List<Brand> brands;private List<Map<String, Object>> specs;public SearchResult(Long total, Integer totalPage, List<Goods> items, List<Category> categories, List<Brand> brands, List<Map<String, Object>> specs) {super(total, totalPage, items);this.categories = categories;this.brands = brands;this.specs = specs;}
}

修改SearchService增加获取规格参数

首先,在聚合得到商品分类后,判断分类的个数,如果是1个则进行规格聚合。

将聚合的代码抽取到了一个getParamAggResult方法中。然后,根据商品分类,查询所有可用于搜索的规格参数。因为规格参数保存时不做分词,因此其名称会自动带上一个.keyword后缀:

注意第一句会报错:

//            StringTerms terms = aggregations.get(name);            

InternalTerms, ?> terms = aggregations.get(name);

// 二、返回搜索结果
public PageResult<Goods> search(SearchRequest request) {// 获取并判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品String key = request.getKey();if (StringUtils.isBlank(key)) {return null;}// 0、构建查询条件NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 1、搜索条件:对key进行全文检索查询// 定义基本查询条件QueryBuilder basicQuery = QueryBuilders.matchQuery("all", key).operator(Operator.AND);queryBuilder.withQuery(basicQuery);// 2、结果过滤:通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitlequeryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null));// 3、分页// 3、准备分页参数int page = request.getPage();int size = request.getSize();queryBuilder.withPageable(PageRequest.of(page - 1, size));// 4、排序String sortBy = request.getSortBy();Boolean desc = request.getDescending();if (StringUtils.isNotBlank(sortBy)) {queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));}// 5、聚合分类和品牌// 聚合字段:cid3 brandIdString categoryAggName = "categories";String brandAggName = "brands";queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));// 4、查询,获取结果//        Page<Goods> pageInfo = this.goodsRepository.search(queryBuilder.build());AggregatedPage<Goods> result = elasticsearchTemplate.queryForPage(queryBuilder.build(), Goods.class);// 5、处理结果,获取分页数和结果数Long totalElements = result.getTotalElements();//        int totalPages = pageInfo.getTotalPages();int totalPages = (totalElements.intValue() + size - 1) / size;// 5、解析聚合结果Aggregations aggs = result.getAggregations();List<Category> categories = parseCategoryAgg(aggs.get(categoryAggName));List<Brand> brands = parseBrandAgg(aggs.get(brandAggName));// 判断分类聚合的结果集大小,等于1则聚合List<Map<String, Object>> specs = null;if (categories != null && categories.size() == 1) {// 在原本查询的基础上进行规格参数聚合specs = buildSpecificationAgg((Long)categories.get(0).getId(), basicQuery);}// 解析封装结果并返回return new SearchResult(totalElements, totalPages, result.getContent(), categories, brands, specs);
}/**聚合出规格参数过滤条件*/
private List<Map<String,Object>> buildSpecificationAgg(Long cid, QueryBuilder basicQuery) {// 定义一个集合,收集聚合结果集List<Map<String, Object>> specMapList = new ArrayList<>();// 1. 查询要聚合的规格参数List<SpecParam> params = this.specificationClient.queryParams(null, cid, null, true);// 2. 创建自定义查询构建器NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 2. 基于基本的查询条件,聚合规格参数queryBuilder.withQuery(basicQuery);// 添加n多个聚合params.forEach(param -> {// 使用聚合参数的名字作为聚合名 param.getName()// "specs." + aggName + ".keyword" 对应的就是聚合时候的field字段,keyword是因为不是分词会有keyword属性String aggName = param.getName();queryBuilder.addAggregation(AggregationBuilders.terms(aggName).field("specs." + aggName + ".keyword"));});// 只需要聚合结果集,不需要查询结果集queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));// 3. 执行聚合查询//        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build());AggregatedPage<Goods> goodsPage = elasticsearchTemplate.queryForPage(queryBuilder.build(), Goods.class);// 4. 解析聚合查询的结果集// 会获取到(多个规格参数对应的多个聚合的)一个聚合,先取出每一个聚合,再取出聚合里面的聚合Aggregations aggregations = goodsPage.getAggregations();for (SpecParam param : params) {// 获取规格参数名String name = param.getName();// 获取规格参数对应的聚合//            StringTerms terms = aggregations.get(name);InternalTerms<?, ?> terms = aggregations.get(name);// 收集聚合里面的规格参数值List<Object> options = new ArrayList<>();// 遍历聚合中的每个桶,把桶中key放在一个链表中options = terms.getBuckets().stream().map(b -> b.getKeyAsString()).collect(Collectors.toList());Map<String, Object> map = new HashMap<>();// 放入规格参数名map.put("k", name);map.put("options", options);specMapList.add(map);}// 最终处理好的规格参数链表是一个个map,map里面是<规格参数名字, 值组成的一个链表>return specMapList;
}

页面渲染数据

渲染规格过滤条件

首先把后台传递过来的specs添加到filters数组:

loadData() {// ly.http.post("/search/page", ly.stringify(this.search)).then(resp=>{ly.http.post("/search/page", this.search).then(resp => {// console.log(resp);// 1. 保存分页结果this.total = resp.data.total;this.totalPage = resp.data.totalPage;// 将当前页数据skus拆分为sku之后,保存当前页数据resp.data.items.forEach(goods => {goods.skus = JSON.parse(goods.skus);// 给json对象goods添加一个字段:被选中的字段,初始化默认选中的skugoods.selectedSku = goods.skus[0];})this.goodsList = resp.data.items;// 2. 保存过滤项this.filters.push({k: "cid3",options: resp.data.categories});this.filters.push({k: "brandId",options: resp.data.brands});resp.data.specs.forEach(spec => this.filters.push(spec));}).catch(error => {});
},

要注意:分类、品牌的option选项是对象,里面有name属性,而specs中的option是简单的字符串,直接输出里面的字符串即可

1582333826858-0db74048-6125-4dba-9b5c-4bc8a447f0d1.png

展示或收起过滤条件

我们在data中定义变量,记录展开或隐藏的状态:

1582333981188-9a129e07-d600-4e3e-a741-f968d2f715c9.png

然后在按钮绑定点击事件,以改变show的取值:

<div class="type-wrap" style="text-align: center"><v-btn small flat v-show="!show" @click="show=true">更多<v-icon>arrow_drop_down</v-icon></v-btn><v-btn small="" flat v-show="show" @click="show=false">收起<v-icon>arrow_drop_up</v-icon></v-btn>
</div>

过滤条件的筛选

当我们点击页面的过滤项:

  • 把过滤条件保存在search对象中(watch监控到search变化后就会发送到后台)
  • 在页面顶部展示已选择的过滤项
  • 把商品分类展示到顶部面包屑

前台--保存过滤项

定义属性

把已选择的过滤项保存在search中:要注意,在created构造函数中会对search进行初始化(search.filter)

created() {// 判断是否有请求参数if (!location.search) {return;}// 将请求参数转为对象const search = ly.parse(location.search.substring(1));// 初始化page,防止第一次访问为空search.page = search.page ? parseInt(search.page) : 1;// 初始化过滤条件search.filter = search.filter ? search.filter : {};// 初始化排序,默认不排序search.sortBy = search.sortBy || "";// 排序方式初始化,转为布尔值search.descending = search.descending === "true" || false;// 记录在data的search对象中this.search = search;// 发起请求,根据条件搜索this.loadData();
},

search.filter是一个对象,结构:

{"过滤项名":"过滤项值"
}

绑定点击事件

给所有的过滤项绑定点击事件,要注意,点击事件传2个参数:

  • k:过滤项的key
  • option:当前过滤项对象

在点击事件中,保存过滤项到selectedFilter

selectFilter(k, o){// this.search.filter[k] = o;// 只写上面一句需要手动刷新监视器,Vue没有监视到filter的变化,created时候filter是没有内部属性的// 解构表达式,按照结构去里面取值,从filter里面复制属性const {... obj} = this.search.filter;// 在以前的基础上拓展obj[k] = o;// 这样watch就可以监控到了this.search.filter = obj;
}

search对象中嵌套了filter对象,请求参数格式化时需要进行特殊处理,修改common.js中的一段代码:

1582359520085-06b8c5d3-a18b-4807-a086-f7826e88da35.png

我们刷新页面,点击后通过浏览器功能查看search.filter的属性变化:

1582359611146-9c69b439-f59c-44ca-b4fb-aa72d8afb7f9.png

并且,此时浏览器地址也发生了变化:

1582359672069-40d99b98-0c6d-4377-b1ad-e1b620ada778.png

网络请求也正常发出:

1582359746270-11575d0d-2f4c-46d0-8316-6c981c17c7d0.png

后台--添加过滤条件

拓展请求对象

我们需要在请求类:SearchRequest中添加属性,接收过滤属性。过滤属性都是键值对格式,但是key不确定,所以用一个map来接收即可。

private Map<String, String> filter;

添加过滤条件

基本查询的基础之上,要把页面传递的过滤条件也加入进去。不能使用普通的查询,而是要用到BooleanQuery,基本结构是这样的:

GET /deltaqin/_search
{"query":{"bool":{"must":{ "match": { "title": "小米手机",operator:"and"}},"filter":{"range":{"price":{"gt":2000.00,"lt":3800.00}}}}}
}

所以对原来的基本查询进行改造:(SearchService中的search方法)

// 1、搜索条件:对key进行全文检索查询
// 定义基本查询条件
//        QueryBuilder basicQuery = QueryBuilders.matchQuery("all", key).operator(Operator.AND);
//        queryBuilder.withQuery(basicQuery);
BoolQueryBuilder boolQueryBuilder = buildBooleanQueryBuilder(request);
queryBuilder.withQuery(boolQueryBuilder);

因为比较复杂,将其封装到一个方法中:

/*** 构建bool查询构建器*/
private BoolQueryBuilder buildBooleanQueryBuilder(SearchRequest request) {// 创建布尔查询BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();// 添加基本查询条件boolQueryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));// 添加过滤条件if (CollectionUtils.isEmpty(request.getFilter())){return boolQueryBuilder;}for (Map.Entry<String, String> entry : request.getFilter().entrySet()) {// request.getFilter().entrySet()获取过滤条件进行遍历String key = entry.getKey();// 如果过滤条件是“品牌”, 过滤的字段名:brandIdif (!"cid3".equals(key) && !"brandId".equals(key)) {// 如果是规格参数名,过滤字段名:specs.key.keywordkey = "specs." + key + ".keyword";}boolQueryBuilder.filter(QueryBuilders.termQuery(key, entry.getValue()));}return boolQueryBuilder;
}

页面展示选择的过滤项

隐藏已经选择的过滤项

用户选择的项保存在search.filter中,可以编写一个计算属性,把filters中的 已经被选择的key过滤掉:

computed:{remainFilters(){// Object.keys(this.search.filter);获取json对象的key数组const keys = Object.keys(this.search.filter);// 不包含f.k且,如果只剩下一个可选项,不显示return this.filters.filter(f => !keys.includes(f.k) && f.options.length > 1);}
}

然后页面不再直接遍历filters,而是遍历remainFilters

1582368361582-65397472-9741-4f11-9980-285891d8444c.png

展示已经选择的过滤项

需要在页面展示用户已选择的过滤项,所有已选择过滤项都保存在search.filter中,因此在页面遍历并展示即可。

  • 商品分类:分类展示在面包屑位置
  • 品牌:这个要展示,但是其key和值不合适,不能显示一个id在页面。需要找到其name值
  • 数值类型规格:这个展示的时候,需要把单位查询出来
  • 非数值类型规格:这个直接展示其值即可
<!--已选择过滤项-->
<ul class="tags-choose"><li class="tag" v-for="(v,k) in search.filter" v-if="k !== 'cid3'" :key="k">{{k === 'brandId' ? '品牌' : k}}:<span style="color: red" v-text="findValue(k,v)"></span><i class="sui-icon icon-tb-close" @click="deleteFilter(k)"></i></li>
</ul>
  • 判断如果 k === 'cid3'说明是商品分类,直接忽略
  • 判断k === 'brandId'说明是品牌,页面显示品牌,其它规格则直接显示k的值
  • 值的处理比较复杂,我们用一个方法getFilterValue(k,v)来处理,调用时把kv都传递
findValue(k,v){// 如果没有过滤参数,我们跳过展示if(!this.filters || this.filters.length === 0){return null;}if (k != 'brandId') return v;return this.filters.find(f => f.k === 'brandId').options[0].name;
},

然后刷新页面,即可

取消过滤项

绑定点击事件:

<li class="tag" v-for="(v,k) in search.filter" :key="k">{{k === 'brandId' ? '品牌' : k}}:<span style="color: red" v-text="findValue(k,v)"></span><i class="sui-icon icon-tb-close" @click="deleteFilter(k)"></i>
</li>

删除过滤项

deleteFilter(k){const {... obj} = this.search.filter;// 在以前的基础上拓展delete obj[k];// 这样watch就可以监控到了this.search.filter = obj;
}

商品分类面包屑

当用户选择一个商品分类以后,应该在过滤模块的上方展示一个面包屑,把三级商品分类都显示出来。用户选择的商品分类就存放在search.filter中,但是里面只有第三级分类的id:cid3。需要根据它查询出所有三级分类的id及名称

提供查询分类接口

在商品微服务中提供一个根据三级分类id查询1~3级分类集合的方法:

Controller

/*** 根据3级分类id,查询1~3级的分类* @param id* @return*/
@GetMapping("all/level")
public ResponseEntity<List<Category>> queryAllByCid3(@RequestParam("id") Long id){List<Category> list = this.categoryService.queryAllByCid3(id);if (list == null || list.size() < 1) {return new ResponseEntity<>(HttpStatus.NOT_FOUND);}return ResponseEntity.ok(list);
}

Service

public List<Category> queryAllByCid3(Long id) {Category c3 = this.categoryMapper.selectByPrimaryKey(id);Category c2 = this.categoryMapper.selectByPrimaryKey(c3.getParentId());Category c1 = this.categoryMapper.selectByPrimaryKey(c2.getParentId());return Arrays.asList(c1,c2,c3);
}

1582369738523-1992dfd5-754d-464c-93c2-6bfd2a00ad44.png

页面展示面包屑

判断商品分类是否只有1个,如果是,则查询三级商品分类,添加到面包屑即可。

1582370918982-1f4941ed-9e42-4e32-94b9-b43281fb682e.png

渲染:

<!--面包屑-->
<ul class="fl sui-breadcrumb"><li><span>全部结果:</span></li><li v-for="(c,i) in breads" :key="i"><a href="#" v-if="i < 2">{{c.name}}</a><span v-else>{{c.name}}</span></li>
</ul>

优化

搜索系统需要优化的点:

  • 查询规格参数部分可以添加缓存
  • 聚合计算interval变化频率极低,所以可以设计为定时任务计算(周期为天),然后缓存起来。
  • elasticsearch本身有查询缓存,可以不进行优化
  • 商品图片应该采用缩略图,减少流量,提高页面加载速度
  • 图片采用延迟加载
  • 图片还可以采用CDN服务器
  • sku信息应该在页面异步加载,而不是放到索引库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/937147.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全局解释器锁(GIL)

2025.10.14 1.全局解释器锁(GIL)在Python中主要影响多线程应用程序的性能,具体表现为限制CPU密集型任务的并行执行能力,因为GIL只允许一个线程同时执行Python字节码,导致无法充分利用多核CPU进行真正并行计算。

How to Speak English with Only 50 Sentences

How to Speak English with Only 50 SentencesWelcome to my channel, Bookish English 2. Today I will show you something real. Last week I met a student who spoke English with only 50 sentences. Sh…

Python 并发编程:concurrent.futures

一、模块简介 concurrent.futures 是 Python 标准库提供的 高级并发接口,用来执行多线程或多进程任务。 特点:简化线程/进程管理;提供统一接口 Executor;支持异步结果 Future 对象;支持任务异常捕获和超时控制。核…

2025/10/14 模拟赛总结 - sb

2025/10/14 模拟赛总结 A. 好冷好热好冷好热 期望得分:100pts 实际得分:100pts 时间分配:10min 简单题 B. 杀戮尖塔 期望得分:100pts 实际得分:100pts 时间分配:50min 树剖是简单的,但是考虑有没有更优秀的做法…

HEAD以及分离头指针

HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。 HEAD 通常情况下是…

git思维导图总结

导图有笔记 https://www.processon.com/view/link/60ab180a5653bb690f73a0fa新建仓库 新建的时候最好有文件 合并分支 merge dev和master分支都有修改的话,使用git log只能看到自己的分支的log git merge dev 当前是在…

Python 并发编程:`concurrent.futures` 模块

一、模块简介 concurrent.futures 是 Python 标准库提供的 高级并发接口,用来执行多线程或多进程任务。 特点:简化线程/进程管理;提供统一接口 Executor;支持异步结果 Future 对象;支持任务异常捕获和超时控制。核…

ZR3365

Sol 不要忽略看上去没用的东西。 不要忽略看上去没用的东西。 不要忽略看上去没用的东西。 显然暴力 \(f_{i}=\sum_{j=1}^i[j*(i-j+1)\le i]f_{j-1}f_{i-j}\binom{i-1}{j-1}\)。 假设 \(j-1\le i-j\),\(j-1>i-j\) …

记一次因对象构造顺序引发的踩内存问题

记一次因对象构造顺序引发的踩内存问题 背景与现象 template<typename T> struct range_reader {range_reader(const T &low_bound, const T &upper_bound): low_(low_bound), high_(upper_bound){}T op…

恒流电路的震荡问题

背景:需要设计一个300mA内可调恒流源用于LED灯的驱动,最高工作电压6V;PCB空间非常有限。 以现成物料搭建恒流电路,用单级运放加MOS管3400,反馈回路直连。项目选用的MCU自带DAC输出,给设计上带来很大的方便。如下…

六维力传感器材质选择:影响性能与精度的关键因素 - 实践

六维力传感器材质选择:影响性能与精度的关键因素 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas…

CSharp: Aspose.CAD 25.10 Convert DWG and DXF to PDF

/// <summary>/// geovindu, Geovin Du,塗聚文,涂聚文/// </summary>/// <param name="sender"></param>/// <param name="e"></param>protected void Pag…

vtk学习——Pipeline

Pipeline 可视化管线是指用于获取或创建数据、处理数据以及把数据写入文件或者把数据传递给渲染引擎进行显示,这样的一种结构在 VTK 里就称为可视化管线。数据对象(Data Object)、处理对象(Process Object)和数据…

长沙四大名校x东方project

本人(KK_SpongeBob)蒟蒻,写不出好文章,但转载请注明原文链接:https://www.cnblogs.com/OIer-QAQ/p/19142154

Rust 的英文数字验证码识别系统设计与实现

一、引言 验证码(Completely Automated Public Turing test to tell Computers and Humans Apart, CAPTCHA)是区分人类与自动程序的重要技术手段。 随着 OCR 与深度学习的普及,传统验证码逐渐面临被机器识别的风险。…

IOS开发 - UIViewController 界面控制基类解析

UIViewController 是什么UIViewController 就是 iPhone 每个界面的“总管”,它负责这个界面怎么显示、怎么动、点按钮后干啥。类比解析:假设开一个演唱会演唱会元素 在 iOS 中对应舞台(观众能看到的地方) UIView(…

SpringBoot运维实用篇(YW-1.SpringBoot程序的打包与运行,YW-2.配置高级,YW-3.多环境开发,YW-4.日志) - a

SpringBoot运维实用篇 目录SpringBoot运维实用篇YW-1.SpringBoot程序的打包与运行程序打包程序运行SpringBoot程序打包失败处理命令行启动常见问题及解决方案SpringBoot项目快速启动(Linux版)YW-2.配置高级YW-2-1.临…

CSP-S模拟31

CSP-S模拟31 A. 远征 (expedition) 简单题,直接大力 \(O(nV)\) 预处理 对于每个数每个位置 记录这个数下一个会被更改的位置。 查询直接跳即可,复杂度是 \(O(\log V)\) Code: #include<bits/stdc++.h>using n…

matlab 2025b + adalm-pluto 链接测试

matlab 2025b + adalm-pluto 链接测试1、matlab 菜单栏 → 获取硬件支持包→搜索 pluto , 点进去进行安装,并安装驱动, 我这里已安装 2、点击 上面 截图 中的 已安装 对应 的图标, 进入界面, 点击 管理 3、 …

Fortran 实现英文数字验证码识别系统

一、引言 验证码识别是人工智能与图像处理技术的重要应用场景之一。尽管现代验证码识别大多使用 Python、C++ 或 JavaScript 等语言实现,但为了探索底层计算与矩阵操作的效率,我们可以尝试用 Fortran 来实现一个基础…