暢購商城(五):Elasticsearch實現商品搜索


好好學習,天天向上

本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star,更多文章請前往:目錄導航

前期准備

今天的任務就是用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中。最后實現了關鍵詞搜索以及分類統計的功能。

如果我的文章對你有些幫助,不要忘了點贊收藏轉發關注。要是有什么好的意見歡迎在下方留言。讓我們下期再見!

微信公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM