聚合提供了从数据中分组和提取数据的能力。
最简单的聚合方法大致等于SQL Group by(分组统计)和SQL聚合函数(求平均 求最大 求最小)
是对查询出的数据做一些处理
# 分别为包含mill、平均年龄、
GET bank/_search
{
"query": {
# 查询出包含mill的
"match": {
"address": "Mill"
}
},
"aggs": {
#基于查询聚合
"ageAgg": {
# 聚合的名字,随便起
"terms": {
# 看值的可能性分布 就是分组统计 类似于group by
"field": "age",
"size": 10
}
},
"ageAvg": {
"avg": {
# 看age值的平均
"field": "age"
}
},
"balanceAvg": {
"avg": {
# 看balance的平均
"field": "balance"
}
}
},
"size": 0 # 不看详情
}
也就是在aggs里再写一个aggs
GET bank/_search
{
"query": {
"match_all": {
}
},
"aggs": {
"ageAgg": {
"terms": {
# 看分布 对年龄分组
"field": "age",
"size": 100
},
"aggs": {
# 与terms并列
"ageAvg": {
#平均 对已根据年龄分组的数据 进行求平均 比如 求20岁所有人的平均薪资
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}
是直接定义在索引下的,因为不同类型下、同名文档的处理方式是没有区别的。所以使用mapping映射,相当于屏蔽了类型,把文档直接放在了索引的下一级。
ElasticSearch7-去掉type概念
PUT /my_index #相当于mysql创建一个表 指定各个字段的类型
{
"mappings": {
"properties": {
"age": {
"type": "integer"
},
"email": {
"type": "keyword" # 指定为keyword
},
"name": {
"type": "text" # 全文检索。保存时候分词,检索时候进行分词匹配
}
}
}
}
PUT /my_index/_mapping #相当于往表里填数据
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false # 字段不能被检索。检索 表明新增的字段不能被检索,只是一个冗余字段。不能更新映射
对于已经存在的字段映射,我们不能更新。更新必须创建新的索引,进行数据迁移。
}
}
}
GET /my_index #查看映射
对于已经存在的字段映射,我们不能更新。更新必须创建新的索引,进行数据迁移。
相当于不能 更改表的属性 (类比于mysql)
必须重新新建一个索引,然后把旧数据迁移过来。
创建新索引
PUT /newbank
{
"mappings": {
"properties": {
"account_number": {
"type": "long"
},
"address": {
"type": "text"
},
"age": {
"type": "integer"
},
"balance": {
"type": "long"
},
"city": {
"type": "keyword"
},
"email": {
"type": "keyword"
},
"employer": {
"type": "keyword"
},
"firstname": {
"type": "text"
},
"gender": {
"type": "keyword"
},
"lastname": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"state": {
"type": "keyword"
}
}
}
}
将bank中的数据迁移到newbank中
POST _reindex
{
"source": {
"index": "bank",
"type": "account" #原索引的名称和类型
},
"dest": {
"index": "newbank" #新索引的名称
}
}
新索引的type会变为_doc (默认的索引类型 ,旧索引是account)
一个tokenizer(分词器)接收一个字符流,将之分割为独立的tokens(词元,通常是独立的单词),然后输出tokens流。
POST _analyze
{
"analyzer": "standard",
"text": "The 2 Brown-Foxes bone."
}
会按单词分开,但是 对于中文,就不合适,因为它会把每个字都当成一个词 去分
所以我们需要使用其他分词器
在前面安装的elasticsearch时,我们已经将elasticsearch容器的“/usr/share/elasticsearch/plugins”目录,映射到宿主机的“ /mydata/elasticsearch/plugins”目录下,所以比较方便的做法就是下载“/elasticsearch-analysis-ik-7.4.2.zip”文件,然后解压到该文件夹下即可。安装完毕后,需要重启elasticsearch容器。
就是下载然后解压 然后放到/mydata/elasticsearch/plugins目录下 然后重启容器。
GET _analyze
{
"analyzer": "ik_smart",
"text":"我是中国人"
}
GET _analyze
{
"analyzer": "ik_max_word",
"text":"我是中国人"
}
vi 文件名
i 进入插入模式
esc退出插入模式
:wq退出并保存
修改/usr/share/elasticsearch/plugins/ik/config中的IKAnalyzer.cfg.xml
DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置comment>
<entry key="ext_dict">entry>
<entry key="ext_stopwords">entry>
<entry key="remote_ext_dict">http://192.168.56.10/es/fenci.txtentry> #在这里配置自定义分词文件的路径
properties>
具体可以看笔记,这里就不深究了
https://blog.csdn.net/hancoder/article/details/113922398
<dependency> <groupId>org.elasticsearch.clientgroupId> <artifactId>elasticsearch-rest-high-level-clientartifactId> <version>7.4.2version>dependency>
由于 springboot已经整合了ES 版本为6.8.5 所以要把spring-boot-dependencies中所依赖的ES版本改掉
<properties>
<java.version>1.8java.version>
<elasticsearch.version>7.4.2elasticsearch.version> #这里原来是6.8.5
properties>
启动类加注解
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@Configuration
public class GulimallElasticSearchConfig {
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient esRestClient() {
RestClientBuilder builder = null;
// 可以指定多个es
builder = RestClient.builder(new HttpHost(host, 9200, "http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
@Testpublic void indexData() throws IOException {
// 设置索引 IndexRequest indexRequest = new IndexRequest ("users"); indexRequest.id("1"); User user = new User(); user.setUserName("张三"); user.setAge(20); user.setGender("男"); String jsonString = JSON.toJSONString(user); //设置要保存的内容,指定数据和类型 indexRequest.source(jsonString, XContentType.JSON); //执行创建索引和保存数据 IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); System.out.println(index);}
保存语句再次发送就会变成修改操作。
@Test public void find() throws IOException {
// 1 创建检索请求 SearchRequest searchRequest = new SearchRequest(); searchRequest.indices("bank"); //填充索引 SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); // 构造检索条件// sourceBuilder.query();// sourceBuilder.from();// sourceBuilder.size();// sourceBuilder.aggregation(); sourceBuilder.query(QueryBuilders.matchQuery("address","mill")); //查询address包含mill的 System.out.println(sourceBuilder.toString()); //sourceBuilder就是一个查询语句的json串 //任何的查询条件 match matchall agg 都用各自的构造器构造好了放到SearchSourceBuilder中,然后塞入searchRequest //构建第一个聚合条件:按照年龄的值分布 TermsAggregationBuilder agg1 = AggregationBuilders.terms("agg1").field("age").size(10);// 聚合名称 // 参数为AggregationBuilder sourceBuilder.aggregation(agg1); // 构建第二个聚合条件:平均薪资 AvgAggregationBuilder agg2 = AggregationBuilders.avg("agg2").field("balance"); sourceBuilder.aggregation(agg2); searchRequest.source(sourceBuilder); // 2 执行检索 SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); // 3 分析响应结果 System.out.println(response.toString()); //response也是一个结果json串 // 3.1 获取java bean SearchHits hits = response.getHits(); SearchHit[] hits1 = hits.getHits(); for (SearchHit hit : hits1) { hit.getId(); hit.getIndex(); String sourceAsString = hit.getSourceAsString(); //真正的查询结果在source中,获取到source,用json转换工具转成bean对象 这个bean需要自己创建VO类(或者已有的PO) Account account = JSON.parseObject(sourceAsString, Account.class); System.out.println(account); } // 3.2 获取聚合的结果 Aggregations aggregations = response.getAggregations(); Terms agg21 = aggregations.get("agg2"); //根据聚合名称获取 for (Terms.Bucket bucket : agg21.getBuckets()) { String keyAsString = bucket.getKeyAsString(); System.out.println(keyAsString); } }
一般一个类名加s 代表这个类的构造器
nginx可以理解为tomcat 是一个web服务器
随便启动一个nginx实例,只是为了复制出配置
docker run -p80:80 --name nginx -d nginx:1.10
将容器内的配置文件拷贝到/mydata/nginx/conf/ 下
mkdir -p /mydata/nginx/htmlmkdir -p /mydata/nginx/logsmkdir -p /mydata/nginx/confdocker container cp nginx:/etc/nginx/* /mydata/nginx/conf/ #由于拷贝完成后会在config中存在一个nginx文件夹,所以需要将它的内容移动到conf中mv /mydata/nginx/conf/nginx/* /mydata/nginx/conf/rm -rf /mydata/nginx/conf/nginx
终止原容器:
docker stop nginx
执行命令删除原容器:
docker rm nginx
创建新的Nginx,执行以下命令
docker run -p 80:80 --name nginx \ -v /mydata/nginx/html:/usr/share/nginx/html \ -v /mydata/nginx/logs:/var/log/nginx \ -v /mydata/nginx/conf/:/etc/nginx \ -d nginx:1.10
设置开机启动nginx
docker update nginx --restart=always
创建“/mydata/nginx/html/index.html”文件,测试是否能够正常访问
echo 'hello nginx!
' >index.html
访问:http://nginx所在主机的IP:80/index.html
ES在内存中,所以在检索中优于mysql。ES也支持集群,数据分片存储。
两种方案
一是按照spu查,也就是检索条件是spu,sku只存一个sku的id,这种方式很省空间。但是,有一个致命问题,就是当检索到一个符合条件的spu时,会一次性返回大量的skuid,这样在高并发环境下可能会造成阻塞。
二是按照sku存,也就是检索条件是sku,所有的sku信息都存,这种方式由于很多sku的spu属性是一样的,所以会有很多冗余字段。但是这种方式,虽然和第一种方式比起来占用的空间更多,但是每次查询返回的数据量比较少。
综合考虑,依照“空间换时间”的理念,选取第二种方案。
索引模型如下:
PUT product
{
"mappings":{
"properties": {
"skuId":{
"type": "long" },
"spuId":{
"type": "keyword" }, # 不可分词
"skuTitle": {
"type": "text",
"analyzer": "ik_smart" # 中文分词器
},
"skuPrice": {
"type": "keyword" }, # 保证精度问题
"skuImg" : {
"type": "keyword" ,
"index": false, # 不可被检索,不生成index
}, # 视频中有false
"saleCount":{
"type":"long" },
"hasStock": {
"type": "boolean" },
"hotScore": {
"type": "long" },
"brandId": {
"type": "long" },
"catalogId": {
"type": "long" },
"brandName": {
"type": "keyword"}, # 视频中有false
"brandImg":{
"type": "keyword",
"index": false, # 不可被检索,不生成index,只用做页面使用
"doc_values": false # 不可被聚合,默认为true
},
"catalogName": {
"type": "keyword"
"index": false, # 不可被检索,不生成index
}, # 视频里有false
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long" },
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword" }
}
}
}
}
}
属性是"type": “nested”,因为是内部的属性进行检索
数组类型的对象会被扁平化处理(对象的每个属性会分别存储到一起)
user.name=["aaa","bbb"]
user.addr=["ccc","ddd"]
这种存储方式,可能会发生如下错误:
错误检索到{aaa,ddd},这个组合是不存在的
数组的扁平化处理会使检索能检索到本身不存在的,为了解决这个问题,就采用了嵌入式属性,数组里是对象时用嵌入式属性(不是对象无需用嵌入式属性)
后台管理系统把spuid传到后端,后端根据spuid从mysql数据库查出spuid对应的一系列sku,然后把这些sku上传到es。当然这中间涉及到PO类与ESModel的转换。
// sku的规格参数相同,因此我们要将查询规格参数提前,只查询一次/*** 查询sku是否有库存* 返回skuId 和 stock库存量*/@PostMapping("/hasStock")public R getSkuHasStock(@RequestBody List SkuIds){ List vos = wareSkuService.getSkuHasStock(SkuIds); return R.ok().setData(vos);}
/**
* 上架商品
*/
@PostMapping("/product") // ElasticSaveController
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels){
boolean status;
try {
status = productSaveService.productStatusUp(skuEsModels);
} catch (IOException e) {
log.error("ElasticSaveController商品上架错误: {}", e);
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
}
if(!status){
return R.ok();
}
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
}
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
// 1.给ES建立一个索引 product
BulkRequest bulkRequest = new BulkRequest();
// 2.构造保存请求
for (SkuEsModel esModel : skuEsModels) {
// 设置索引
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
// 设置索引id
indexRequest.id(esModel.getSkuId().toString());
String jsonString = JSON.toJSONString(esModel);
indexRequest.source(jsonString, XContentType.JSON);
// add
bulkRequest.add(indexRequest);
}
// bulk批量保存
BulkResponse bulk = client.bulk(bulkRequest, GuliESConfig.COMMON_OPTIONS);
// TODO 是否拥有错误
boolean hasFailures = bulk.hasFailures();
if(hasFailures){
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> item.getId()).collect(Collectors.toList());
log.error("商品上架错误:{}",collect);
}
return hasFailures;
}
也就是根据spuid封装ESModels
// SpuInfoServiceImpl
public void upSpuForSearch(Long spuId) {
//1、查出当前spuId对应的所有sku信息,品牌的名字
List<SkuInfoEntity> skuInfoEntities=skuInfoService.getSkusBySpuId(spuId);
//TODO 4、根据spu查出当前sku的所有可以被用来检索的规格属性
List<ProductAttrValueEntity> productAttrValueEntities = productAttrValueService.list(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
List<Long> attrIds = productAttrValueEntities.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchIds=attrService.selectSearchAttrIds(attrIds); #这里是根据id查询数据库 下面有sql语句
Set<Long> ids = new HashSet<>(searchIds);
List<SkuEsModel.Attr> searchAttrs = productAttrValueEntities.stream().filter(entity -> {
return ids.contains(entity.getAttrId());
}).map(entity -> {
SkuEsModel.Attr attr = new SkuEsModel.Attr();
BeanUtils.copyProperties(entity, attr);
return attr;
}).collect(Collectors.toList());
//TODO 1、发送远程调用,库存系统查询是否有库存
Map<Long, Boolean> stockMap = null;
try {
List<Long> longList = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
List<SkuHasStockVo> skuHasStocks = wareFeignService.getSkuHasStocks(longList);
stockMap = skuHasStocks.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));
}catch (Exception e){
log.error("远程调用库存服务失败,原因{}",e);
}
//2、封装每个sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> skuEsModels = skuInfoEntities.stream().map(sku -> {
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(sku, skuEsModel);
skuEsModel.setSkuPrice(sku.getPrice());
skuEsModel.setSkuImg(sku.getSkuDefaultImg());
//TODO 2、热度评分。0
skuEsModel.setHotScore(0L);
//TODO 3、查询品牌和分类的名字信息
BrandEntity brandEntity = brandService.getById(sku.getBrandId());
skuEsModel.setBrandName(brandEntity.getName());
skuEsModel.setBrandImg(brandEntity.getLogo());
CategoryEntity categoryEntity = categoryService.getById(sku.getCatalogId());
skuEsModel.setCatalogName(categoryEntity.getName());
//设置可搜索属性
skuEsModel.setAttrs(searchAttrs);
//设置是否有库存
skuEsModel.setHasStock(finalStockMap==null?false:finalStockMap.get(sku.getSkuId()));
return skuEsModel;
}).collect(Collectors.toList());
//TODO 5、将数据发给es进行保存:gulimall-search
R r = searchFeignService.saveProductAsIndices(skuEsModels);
if (r.getCode()==0){
this.baseMapper.upSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode());
}else {
log.error("商品远程es保存失败");
}
}
selectSearchAttrIds 对应的持久层sql
<resultMap type="com.atguigu.gulimal1.product.entity.AttrEntity" id="attrMap">
<result property="attrId" column="attr_id" />
<result property="attrName" column="attr_name" />
<result property="searchType" column="search_type" />
<result property="valueType" column="value_type" />
<result property="valueSelect" column="value_select" />
<result property="attrType" column="attr_type" />
<result property="enab1e" column= "enable"/>
<result property="catelogId" column="catelog_id" />
<result property="showDesc" column="show_dese" />
< / resu1tMap>
<select id="selectSearchAttrIds" resu1tType="java.lang.Long">
SELECT attr_id FROM ‘pms_attr’ WHERE attr_id IN
<foreach collection="attrIds" item="id" separator=" , " open="(" close=")">
#{id}
< / foreach>
AND search_type = 1
select>
根据spu查出所有sku的信息,然后判断是否有库存,根据是否有库存,写bool类型hasStock(ESModel和PO的差别)
,的封装ESModels、调用 3 的服务 上传
当要对一个集合中的元素做相同处理时
可以这样
List<CategoryEntity> newCategoryEntities = categoryEntities.stream().filter(
categoryEntity ->
categoryEntity.getEntityNum() == 1
).map(categoryEntity -> {
categoryEntity.setSize(new Long(100));
return categoryEntity;
}).collect(Collectors.toList());
集合总的元素都可以用lmbda表达式操作,操作完再收集成一个集合。
正向代理 就是我要访问谷歌,访问不了,然后我请另一台服务器帮我转到谷歌,那么就称这台服务器正向代理了我的请求。
反向代理,就是我要访问谷歌,谷歌把我转到了百度,那么就称谷歌反向代理了我的请求。(我并没有要去百度)
Nginx其实就是为了屏蔽内网的ip,对外只暴露一个Nginx的ip。之后有了域名后,域名映射的地址就是Nginx的地址。
要实现的逻辑:本机浏览器请求gulimall.com,通过配置hosts文件之后,那么当你在浏览器中输入gulimall.com的时候,相当于域名解析DNS服务解析得到ip 192.168.56.10,也就是并不是访问java服务,而是先去找nginx。什么意思呢?是说如果某一天项目上线了,gulimall.com应该是nginx的ip,用户访问的都是nginx
请求到了nginx之后,
如果是静态资源/static/ 直接在nginx服务器中找到静态资源直接返回。
如果不是静态资源/(他配置在/static/*的后面所以才优先级低),nginx把他upstream转交给另外一个ip 192.168.56.1:88这个ip端口是网关gateway。
(在upstream的过程中要注意配置proxy_set_header Host $host;)
到达网关之后,通过url信息断言判断应该转发给nacos中的哪个微服务(在给nacos之前也可以重写url),这样就得到了响应
nginx.conf:
全局块:配置影响nginx全局的指令。如:用户组,nginx进程pid存放路径,日志存放路径,配置文件引入,允许生成worker process故障等
events块:配置影响 Nginx 服务器与用户的网络连接,常用的设置包括是否开启对多 work process下的网络连接进行序列化,是否允许同时接收多个网络连接,选取哪种事件驱动模型来处理连接请求,每个 word process 可以同时支持的最大连接数等。
http块:
http全局块:配置的指令包括文件引入、MIME-TYPE 定义、日志自定义、连接超时时间、单链接请求数上限等。错误页面等
server块:这块和虚拟主机有密切关系,虚拟主机从用户角度看,和一台独立的硬件主机是完全一样的。每个 http 块可以包括多个 server 块,而每个 server 块就相当于一个虚拟主机。
location1:配置请求的路由,以及各种页面的处理情况
location2
修改主机hosts,映射gulimall.com到192.168.56.10。关闭防火墙
此时本机访问gulimall.com 就可以请求到nginx的首页index.html了
要让nginx反向代理到本机的10000端口,主要需要修改server配置
server {
listen 80; server_name gulimall.com ; #charset koi8-r;#access_log/var/log/nginx/log/host.access.logmain;location / {proxy_pass http:/ /192.168.56.1: 10000}
listen 是监听的端口号,server_name是监听的域名 。因为做了映射,所以gulimall.com其实是主机的ip
location就是要转发的ip+端口
修改nginx/conf/nginx.conf,将upstream映射到我们的网关服务
upstream gulimall{
# 88是网关 server 192.168.56.1:88; }
修改nginx/conf/conf.d/gulimall.conf,接收到gulimall.com的访问后,如果是/,转交给指定的upstream,由于nginx的转发会丢失host头,造成网关不知道原host,所以我们添加头信息
location / {
proxy_pass http://gulimall; proxy_set_header Host $host; }
网关路由转发配置
配置gateway为服务器,将域名为**.gulimall.com转发至商品服务。配置的时候注意 网关优先匹配的原则,所以要把这个配置放到后面
- id: gulimall_host_route uri: lb://gulimall-product predicates: - Host=**.gulimall.com
总之一句,nginx就是类似网关的作用,主要是为了隐藏主机的域名。外界的请求请求域名,而域名映射的是nginx的地址,然后nginx再把用户的请求转发到真实的服务器。