第六章:全文检索——elasticsearch
一、安装elasticsearch
1.拉取镜像
docker pull elasticsearch:7.4.2
2创建容器
注意加权限,不然外部无法访问
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
chmod -R 777 /mydata/elasticsearch/
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
3.访问测试
http://localhost:9200/
成功页面
4.Kibana(可以不用安,可视化工具)
docker pull kibana:7.4.2(版本要一致)
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://(虚拟机地址):9200 -p 5601:5601 \
-d kibana:7.4.2
二、安装 ik 分词器
1安装对应版本的ik
解压压缩包,放入Plugins中
unzip elasticsearch-analysis-ik-7.4.2.zip
改变权限
chmod -R 777 ik/
测试
POST _analyze
{ "analyzer": "ik_max_word", "text": "我是中国人"
}
自定义词库
2安装nginx
随便启动一个 nginx 实例,只是为了复制出配置
docker run -p 80:80 --name nginx -d nginx:1.10
将容器内的配置文件拷贝到当前目录:docker container cp nginx:/etc/nginx . 别忘了后面的点
修改文件名称:mv nginx conf 把这个 conf 移动到/mydata/nginx 下
终止原容器:docker stop nginx
执行命令删除原容器:docker rm $ContainerId
创建新的 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的html文件夹下新建es文件夹,创建fenci.txt来放自定义的分词
访问 http://地址/es/fenci.txt 来查看是否创建成功
3.为ik配置自定义的分词
来到ik的config目录下,打开IKAnalyzer.cfg.xml
在文件中修改配置为nginx中的地址
<?xml version="1.0" encoding="UTF-8"?>
<!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://自己的地址/es/fenci.txt</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
4.测试
POST _analyze
{
"analyzer": "ik_max_word",
"text": "乔碧萝殿下"
}
三、测试elasticsearch的整合
1.新建gulimall-search
1.导入api和common
<dependency>
<groupId>com.yangyan.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--elasticsearch的api-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
2.检查Dependencies版本中与springboot版本一致
修改springboot中的elasticsearch版本
3.新建配置文件GulimallElasticSearchConfig和注册中心
@Configuration
public class GulimallElasticSearchConfig {
@Bean
RestHighLevelClient client() {
RestClientBuilder builder = RestClient.builder(new HttpHost("47.113.199.234", 9200, "http"));
return new RestHighLevelClient(builder);
}
}
二、测试
1.插入测试
@Resource
private RestHighLevelClient client;
@Test
void index() throws IOException {
IndexRequest request = new IndexRequest("product").id("20")
.source("spuName","华为","id",20L);
try {
IndexResponse response = client.index(request, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println(request);
} catch (ElasticsearchException e) {
if (e.status() == RestStatus.CONFLICT) {
}
}
}
@Data
class Product{
private Long id;
private String SpuName;
}
2.search查询测试
@Test
public void searchData() throws IOException{
//1.构建
//创建检索请求
SearchRequest searchRequest = new SearchRequest();
//指定DSL,检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("spuName", "华为"));
System.out.println(searchSourceBuilder.toString());
searchRequest.source(searchSourceBuilder);
//2.执行
SearchResponse search = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println(search);
}
四、elasticsearch的整合
1.商品上架
1控制器
/**
*商品上架
*/
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
2.在common中封装es的SkuEsModel
@Data
public class SkuEsModel {
private Long skuId;
private Long spuId;
private String skuTitle;
private BigDecimal skuPrice;
private String skuImg;
private Long saleCount;
private Boolean hasStock;
private Long hotScore;
private Long brandId;
private Long catalogId;
private String brandName;
private String brandImg;
private String catalogName;
private List<Attrs> attrs;
@Data
public static class Attrs{
private Long attrId;
private String attrName;
private String attrValue;
}
}
3.业务的实现
@Resource
BrandService brandService;
@Resource
CategoryService categoryService;
@Resource
WareFeignService wareFeignService;
@Resource
SearchFeignService searchFeignService;
//商品上架
@Override
public void up(Long spuId) {
//1.查出当前spuid对应的所有sku信息,品牌的名字。
List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);
List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
//TODO 4.查询当前sku的所有可被检索的规格属性;
List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs);
return attrs;
}).collect(Collectors.toList());
//TODO 1.是否有库存
Map<Long, Boolean> stockMap = null;
try{
R r = wareFeignService.getSkusHasStock(skuIdList);
//
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {
};
stockMap = r.getData(typeReference).stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
}catch (Exception e){
log.error("库存服务查询异常:原因{}",e);
}
//2.封装每个sku的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> upProducts = skus.stream().map(sku -> {
//组装需要的数据
SkuEsModel esModel = new SkuEsModel();
BeanUtils.copyProperties(sku,esModel);
//组装skuPrice,skuImg
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
//TODO 1.是否有库存
if (finalStockMap == null){
esModel.setHasStock(true);
}else {
esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
//TODO 2.热度评分
esModel.setHotScore(0L);
//TODO 3.查询品牌和分类的名字
BrandEntity brand = brandService.getById(esModel.getBrandId());
esModel.setBrandName(brand.getName());
esModel.setBrandImg(brand.getLogo());
//TODO 4.查询当前sku的所有可被检索的规格属性;
CategoryEntity category = categoryService.getById(esModel.getCatalogId());
esModel.setCatalogName(category.getName());
//设置检索属性
esModel.setAttrs(attrsList);
return esModel;
}).collect(Collectors.toList());
//TODO 5.发送给检索服务es进行执行
R r = searchFeignService.productStatusUp(upProducts);
if(r.getCode()==0){
//远程调用成功
//TODO 6.修改sku的状态
baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
}else {
//远程调用失败
//TODO 7.重复调用接口幂等性
}
}
4.选出指定集合里的属性
@Override
public List<Long> selectSearchAttrIds(List<Long> attrIds) {
return baseMapper.selectSearchAttrIds(attrIds);
}
@Mapper
public interface AttrDao extends BaseMapper<AttrEntity> {
List<Long> selectSearchAttrIds(@Param("attrIds") List<Long> attrIds);
}
<select id="selectSearchAttrIds" resultType="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>
5.远程调用是否有库存
/**
* 查询sku是否有库存
*/
@PostMapping("/hasstock")
public R getSkusHasStock(@RequestBody List<Long> skuIds) {
List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);
return R.ok().setData(vos);
}
@Override
public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
SkuHasStockVo vo = new SkuHasStockVo();
//查询当前sku的总库存量
Long count = baseMapper.getSkuStock(skuId);
vo.setSkuId(skuId);
vo.setHasStock(count==null?false:count>0);
return vo;
}).collect(Collectors.toList());
return collect;
}
<select id="getSkuStock" resultType="java.lang.Long">
SELECT SUM(stock-stock_locked) FROM wms_ware_sku WHERE sku_id=#{skuId};
</select>
远程调用接口
@FeignClient("gulimall-ware")
public interface WareFeignService {
/**
* 查询sku是否有库存
*/
@PostMapping("/ware/waresku/hasstock")
R getSkusHasStock(@RequestBody List<Long> skuIds);
}
修改R类
//利用fastjson进行逆转
public <T> T getData(String key,TypeReference<T> typeReference){
Object data = get(key);//默认是map
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
//利用fastjson进行逆转
public <T> T getData(TypeReference<T> typeReference){
Object data = get("data");//默认是map
String s = JSON.toJSONString(data);
T t = JSON.parseObject(s, typeReference);
return t;
}
public R setData(Object data){
put("data",data);
return this;
}
6.发送给es执行
1)定制常量
public class EsConstant {
public static final String PRODUCT_INDEX = "product"; //sku数据在es中的索引
}
2)控制器
@Slf4j
@RequestMapping("/search/save")
@RestController
public class ElasticSearchController {
@Resource
ProductSaveService productSaveService;
/**
* 上架商品
*/
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
boolean b =false;
try {
b = productSaveService.productStatusUp(skuEsModels);
} catch (Exception e) {
log.error("ElasticSearchController商品上架错误:{}",e);
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
if (!b){
return R.ok();
}else {
return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnume.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}
3)实现类
@Resource
RestHighLevelClient restHighLevelClient;
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
//保存到es
//1.建立索引和映射关系
//2.各es中保存这些数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel model : skuEsModels) {
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(model.getSkuId().toString());
String s = JSON.toJSONString(model);
indexRequest.source(s, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
boolean b = bulk.hasFailures();
List<String> errors = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.error("商品上架错误:{},返回的数据:{}",errors,bulk.toString());
return b;
}
4)调用接口
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
7.修改sku状态
@Mapper
public interface SpuInfoDao extends BaseMapper<SpuInfoEntity> {
void updateSpuStatus(@Param("spuId") Long spuId,@Param("code") int code);
}
<update id="updateSpuStatus">
UPDATE pms_spu_info SET publish_status=#{code},update_time=NOW() WHERE id=#{spuId};
</update>