好好學習,天天向上
本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star,更多文章請前往:目錄導航
- 暢購商城(一):環境搭建
- 暢購商城(二):分布式文件系統FastDFS
- 暢購商城(三):商品管理
- 暢購商城(四):Lua、OpenResty、Canal實現廣告緩存與同步
- 暢購商城(五):Elasticsearch實現商品搜索
- 暢購商城(六):商品搜索
- 暢購商城(七):Thymeleaf實現靜態頁
- 暢購商城(八):微服務網關和JWT令牌
- 暢購商城(九):Spring Security Oauth2
- 暢購商城(十):購物車
- 暢購商城(十一):訂單
- 暢購商城(十二):接入微信支付
- 暢購商城(十三):秒殺系統「上」
- 暢購商城(十四):秒殺系統「下」
前期准備
今天的任務就是用ElasticSearcher實現商品搜索的功能。關於Elasticsearch、IK分詞器、Kibana的安裝及基本使用可以看我的另一篇文章Elasticsearch入門指南。
搜索微服務的API工程的搭建
在changgou-service-api下創建一個Module叫changgou-service-search-api。我們后面所要是實現的功能都是基於Spring Data ElasticSearch實現的,所以相關依賴不能少:
<dependencies>
<!--goods API依賴-->
<dependency>
<groupId>com.robod</groupId>
<artifactId>changgou-service-goods-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringDataES依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
搜索微服務搭建
在changgou-service下新建一個changgou-service-search工程作為搜索微服務。在搜索微服務里面需要用到API工程的JavaBean和Feign接口,所以將search-api和goods-api作為依賴添加進來。
<dependencies>
<!--依賴search api-->
<dependency>
<groupId>com.robod</groupId>
<artifactId>changgou-service-search-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.robod</groupId>
<artifactId>changgou-service-goods-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
啟動類和配置文件自然不能少👉
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.robod.goods.feign")
@EnableElasticsearchRepositories(basePackages = "com.robod.mapper")
public class SearchApplication {
public static void main(String[] args) {
//解決SpringBoot的netty和elasticsearch的netty相關jar沖突
System.setProperty("es.set.netty.runtime.available.processors", "false");
SpringApplication.run(SearchApplication.class,args);
}
}
server:
port: 18085
spring:
application:
name: search
data:
elasticsearch:
cluster-name: my-application # 集群節點的名稱,就是在es的配置文件中配置的
cluster-nodes: 192.168.31.200:9300 # 這里用的是TCP端口所以是9300
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#超時配置
ribbon:
ReadTimeout: 500000 # Feign請求讀取數據超時時間
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 50000 # feign連接超時時間
數據導入ES
數據從MySQL導入到ES中大概分為以下幾個步驟:
首先我們需要去創建一個JavaBean來定義相關的映射配置,Index,Type,Field。在changgou-service-search-api的com.robod.entity包下創建一個JavaBean叫SkuInfo:
@Data
@Document(indexName = "sku_info", type = "docs")
public class SkuInfo implements Serializable {
@Id
private Long id;//商品id,同時也是商品編號
/**
* SKU名稱
* FieldType.Text支持分詞
* analyzer 創建索引的分詞器
* searchAnalyzer 搜索時使用的分詞器
*/
@Field(type = FieldType.Text, analyzer = "ik_smart",searchAnalyzer = "ik_smart")
private String name;
@Field(type = FieldType.Double)
private Long price;//商品價格,單位為:元
private Integer num;//庫存數量
private String image;//商品圖片
private String status;//商品狀態,1-正常,2-下架,3-刪除
private LocalDateTime createTime;//創建時間
private LocalDateTime updateTime;//更新時間
private String isDefault; //是否默認
private Long spuId;//SPU_ID
private Long categoryId;//類目ID
@Field(type = FieldType.Keyword)
private String categoryName;//類目名稱,不分詞
@Field(type = FieldType.Keyword)
private String brandName;//品牌名稱,不分詞
private String spec;//規格
private Map<String, Object> specMap;//規格參數
}
在SkuInfo中,設置了Index是"sku_info",Tpye為"docs",並為幾個字段設置了分詞。然后在changgou-service-goods-api的com.robod.goods.feign包下創建一個Feign的接口SkuFeign:
@FeignClient(name = "goods")
@RequestMapping("/sku")
public interface SkuFeign {
/**
* 查詢所有的sku數據
* @return
*/
@GetMapping
Result<List<Sku>> findAll();
}
我們將使用這個Feign去調用Goods微服務中的findAll方法去數據庫中獲取所有的Sku數據。最后,在changgou-service-search微服務中寫出Controller,Service,Dao層的相關代碼,實現數據導入的功能。
//SkuEsController
@GetMapping("/import")
public Result importData(){
skuEsService.importData();
return new Result(true, StatusCode.OK,"數據導入成功");
}
-----------------------------------------------------------
//SkuEsServiceImpl
@Override
public void importData() {
List<Sku> skuList = skuFeign.findAll().getData();
List<SkuInfo> skuInfos = JSON.parseArray(JSON.toJSONString(skuList), SkuInfo.class);
//將spec字符串轉化成map,map的key會自動生成Field
for (SkuInfo skuInfo : skuInfos) {
Map<String,Object> map = JSON.parseObject(skuInfo.getSpec(),Map.class);
skuInfo.setSpecMap(map);
}
skuEsMapper.saveAll(skuInfos);
}
-------------------------------------------------------------
//繼承自ElasticsearchRepository,泛型為SkuInfo,主鍵類型為Long
public interface SkuEsMapper extends ElasticsearchRepository<SkuInfo,Long> {
}
現在將程序運行起來,訪問http://localhost:18085/search/import就可以開始導入了。
經過漫長的等待之后,9萬多條數據成功導入到ES中了。耗費的時間有點長,大概十五分鍾,可以是和虛擬機的配置有關吧。
當我做完這個的時候就提交到Github上了,后來我改來改去的,改亂了就退回到之前提交的版本。然后啟動項目就報了一個錯說Bean注入失敗,我就納悶了,我這是之前提交的正常的版本,怎么就出問題了。然后仔細地翻了翻日志,發現有一行
這個貌似是索引出了問題,刪除索引,啟動項目,沒問題了,重新導入數據到ES,搞定!
功能實現
根據關鍵詞搜索
在開始實現這個功能之前,得先規定好前后端傳參的格式。視頻中用的是Map,但我覺得Map不好,可讀性太差了。比較好的做法是封裝一個實體類,所以我在search-api工程中添加了一個SearchEntity作為前后端傳參的格式:
@Data
public class SearchEntity {
private long total; //搜索結果的總記錄數
private int totalPages; //查詢結果的總頁數
private List<SkuInfo> rows; //搜索結果的集合
public SearchEntity() {
}
public SearchEntity(List<SkuInfo> rows, long total, int totalPages) {
this.rows = rows;
this.total = total;
this.totalPages = totalPages;
}
}
然后就是在搜索微服務中寫出相應的代碼了
@GetMapping
public Result<SearchEntity> searchByKeywords(@RequestParam(required = false)String keywords) {
SearchEntity searchEntity = skuEsService.searchByKeywords(keywords);
return new Result<>(true,StatusCode.OK,"根據關鍵詞搜索成功",searchEntity);
}
---------------------------------------------------------------------------------------------------
@Override
public SearchEntity searchByKeywords(String keywords) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
if (!StringUtils.isEmpty(keywords)) {
nativeSearchQueryBuilder.withQuery(QueryBuilders.queryStringQuery(keywords).field("name"));
}
AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate
.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class);
List<SkuInfo> content = skuInfos.getContent();
return new SearchEntity(content,skuInfos.getTotalElements(),skuInfos.getTotalPages());
}
然后將項目啟動起來,訪問http://localhost:18085/search?keywords=小米
,結果報錯了,報了一個failed to map,然后我在報錯信息中找到了下面這個:
大概意思就是LocalDateTime出了問題,因為Date類不是很好,所以我就改成了LocaDateTime。我看了一下Kibana中的內容,發現
原來是ES自動把LocalDateTime分成了多個Filed,可是我不想讓它分成多個Filed,也不想用Date,怎么辦呢?我在網上找個一個方法,成功解決了我的問題,就是增加 @JsonSerialize 和 @JsonDeserialize 注解,所以我在SkuInfo的createTime和updateTime上面加了幾個注解:
/**
* 只用后兩個注解就可以實現LocalDateTime不分成多個Field,但是格式不對。
* 所以還需要添加前面兩個注解去指定格式與時區
**/
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createTime;//創建時間
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss || yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime updateTime;//更新時間
現在再次重新導入一下
現在格式沒有問題了,現在再來測試看看
OK!
分類統計
當我們在小米商城上面搜索一件商品的時候,下面會將分類展示出來幫助用戶進一步地篩選產品。在暢購商城的表設計中,也有一個叫categoryName的字段。接下來就是要實現把我們搜索出來的數據進行分類統計。
我們要實現的就是圖中的效果,只不過是在Elasticsearch中而不是MySQL。
修改SearchEntity,添加一個categoryList字段:
private List<String> categoryList; //分類集合
修改SkuEsServiceImpl中的searchByKeywords方法,添加分組統計的的代碼:
public SearchEntity searchByKeywords(String keywords) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
if (!StringUtils.isEmpty(keywords)) {
nativeSearchQueryBuilder.withQuery(QueryBuilders.queryStringQuery(keywords).field("name"));
//terms: Create a new aggregation with the given name.
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("categories_grouping")
.field("categoryName"));
}
NativeSearchQuery nativeSearchQuery = nativeSearchQueryBuilder.build();
AggregatedPage<SkuInfo> skuInfos = elasticsearchTemplate.queryForPage(nativeSearchQuery, SkuInfo.class);
StringTerms stringTerms = skuInfos.getAggregations().get("categories_grouping");
List<String> categoryList = new ArrayList<>();
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
categoryList.add(bucket.getKeyAsString());
}
return new SearchEntity(skuInfos.getTotalElements(),skuInfos.getTotalPages(),
categoryList,skuInfos.getContent());
}
現在再來測試一下:
OK!分組統計的功能已經實現了。
小結
這篇文章主要寫了Elasticsearch環境的搭建,然后把數據導入到ES中。最后實現了關鍵詞搜索以及分類統計的功能。
如果我的文章對你有些幫助,不要忘了點贊,收藏,轉發,關注。要是有什么好的意見歡迎在下方留言。讓我們下期再見!