2022java電商項目之實現全文搜索

来源:https://www.cnblogs.com/lihanlin/archive/2022/04/28/16202581.html
-Advertisement-
Play Games

框架,本質上是一些實用經驗集合。即是前輩們在實際開發過程中積攢下來的實戰經驗,累積成一套實用工具,避免你在開發過程中重覆去造輪子,特別是幫你把日常中能遇到的場景或問題都給屏蔽掉,框架的意義在於屏蔽掉開發的基礎複雜度、屏蔽掉此類共性的東西,同時建立嚴格的編碼規範,讓框架使用者開箱即用,並且只需要關註差... ...


尊重原創版權: https://www.gewuweb.com/hot/6213.html

java電商項目全文搜索

尊重原創版權: https://www.gewuweb.com/sitemap.html

java電商項目全文搜索

搜索

什麼是搜索, 電腦根據用戶輸入的關鍵詞進行匹配,從已有的資料庫中摘錄出相關的記錄反饋給用戶。

常見的全網搜索引擎,像百度、谷歌這樣的。但是除此以外,搜索技術在垂直領域也有廣泛的使用,比如淘寶、京東搜索商品,萬芳、知網搜索期刊,csdn中搜索問題貼。也都是基於海量數據的搜索。

1、如何處理搜索

1.1用傳統關係性資料庫

java電商項目全文搜索

弊端:

1) 對於傳統的關係性資料庫對於關鍵詞的查詢,只能逐字逐行的匹配,性能非常差。

2)匹配方式不合理,比如搜索“小密手機” ,如果用like進行匹配,
根本匹配不到。但是考慮使用者的用戶體驗的話,除了完全匹配的記錄,還應該顯示一部分近似匹配的記錄,至少應該匹配到“手機”。

1.2專業全文索引是怎麼處理的

全文搜索引擎目前主流的索引技術就是倒排索引的方式。

傳統的保存數據的方式都是

記錄→單詞

而倒排索引的保存數據的方式是

單詞→記錄

例如

java電商項目全文搜索

搜索“紅海行動”

但是資料庫中保存的數據如圖:

那麼搜索引擎是如何能將兩者匹配上的呢?

基於分詞技術構建 倒排索引 :

首先每個記錄保存數據時,都不會直接存入資料庫。系統先會對數據進行分詞,然後以倒排索引結構保存。如下:

java電商項目全文搜索

然後等到用戶搜索的時候,會把搜索的關鍵詞也進行分詞,會把“ 紅海行動 ”分詞分成: 紅海 和 行動 兩個詞。

這樣的話,先用紅海進行匹配,得到id=1和id=2的記錄編號,再用行動匹配可以迅速定位id為1,3的記錄。

那麼全文索引通常,還會根據匹配程度進行打分,顯然1號記錄能匹配的次數更多。所以顯示的時候以評分進行排序的話,1號記錄會排到最前面。而2、3號記錄也可以匹配到。

二、全文檢索工具elasticsearch

1、lucene與elasticsearch

咱們之前講的處理分詞,構建倒排索引,等等,都是這個叫lucene的做的。那麼能不能說這個lucene就是搜索引擎呢?

還不能。lucene只是一個提供全文搜索功能類庫的核心工具包,而真正使用它還需要一個完善的服務框架搭建起來的應用。

好比lucene是類似於jdk,而搜索引擎軟體就是tomcat 的。

目前市面上流行的搜索引擎軟體,主流的就兩款,elasticsearch和solr,這兩款都是基於lucene的搭建的,可以獨立部署啟動的搜索引擎服務軟體。由於內核相同,所以兩者除了伺服器安裝、部署、管理、集群以外,對於數據的操作,修改、添加、保存、查詢等等都十分類似。就好像都是支持sql語言的兩種資料庫軟體。只要學會其中一個另一個很容易上手。

從實際企業使用情況來看,elasticSearch的市場份額逐步在取代solr,國內百度、京東、新浪都是基於elasticSearch實現的搜索功能。國外就更多了
像維基百科、GitHub、Stack Overflow等等也都是基於ES的

2、elasticSearch的使用場景

1)為用戶提供按關鍵字查詢的全文搜索功能。

2)著名的ELK框架(ElasticSearch,Logstash,Kibana),實現企業海量日誌的處理分析的解決方案。大數據領域的重要一份子。

3、elasticSearch的安裝

詳見《elasticSearch的安裝手冊》

4、elasticsearch的基本概念

cluster

|

整個elasticsearch 預設就是集群狀態,整個集群是一份完整、互備的數據。

---|---

node

|

集群中的一個節點,一般只一個進程就是一個node

shard

|

分片,即使是一個節點中的數據也會通過hash演算法,分成多個片存放,預設是5片。

index

|

相當於rdbms的database, 對於用戶來說是一個邏輯資料庫,雖然物理上會被分多個shard存放,也可能存放在多個node中。

type

|

類似於rdbms的table,但是與其說像table,其實更像面向對象中的class , 同一Json的格式的數據集合。

document

|

類似於rdbms的 row、面向對象里的object

field

|

相當於欄位、屬性

5、利用kibana學習 elasticsearch restful api (DSL)

5.1 es中保存的數據結構

public class Movie {
String id;
String name;
Double doubanScore;
List<Actor> actorList;
}
public class Actor{
String id;
String name;
}

這兩個對象如果放在關係型資料庫保存,會被拆成2張表,但是elasticsearch是用一個json來表示一個document。

所以他保存到es中應該是:

{
“id”:”1”,
“name”:”operation red sea”,
“doubanScore”:”8.5”,
“actorList”:[
{“id”:”1”,”name”:”zhangyi”},
{“id”:”2”,”name”:”haiqing”},
{“id”:”3”,”name”:”zhanghanyu”}
]
}

5.2 對數據的操作

5.2.1 查看es中有哪些索引

GET /_cat/indices?v

es 中會預設存在一個名為.kibana的索引

表頭的含義

health

|

green(集群完整) yellow(單點正常、集群不完整) red(單點不正常)

---|---

status

|

是否能使用

index

|

索引名

uuid

|

索引統一編號

pri

|

主節點幾個

rep

|

從節點幾個

docs.count

|

文檔數

docs.deleted

|

文檔被刪了多少

store.size

|

整體占空間大小

pri.store.size

|

主節點站

5.2.2 增加一個索引

PUT /movie_index

5.2.3 刪除一個索引

ES 是不刪除也不修改任何數據

DELETE /movie_index

5.2.4 新增文檔

  1. 格式 PUT /index/type/id
PUT /movie_index/movie/1
{ "id":1,
"name":"operation red sea",
"doubanScore":8.5,
"actorList":[
{"id":1,"name":"zhang yi"},
{"id":2,"name":"hai qing"},
{"id":3,"name":"zhang han yu"}
]
}
PUT /movie_index/movie/2
{
"id":2,
"name":"operation meigong river",
"doubanScore":8.0,
"actorList":[
{"id":3,"name":"zhang han yu"}
]
}
PUT /movie_index/movie/3
{
"id":3,
"name":"incident red sea",
"doubanScore":5.0,
"actorList":[
{"id":4,"name":"zhang chen"}
]
}

如果之前沒見過index或者type,es 會自動創建。

5.2.5 直接用id查找

GET movie_index/movie/1

5.2.6 修改—整體替換

和新增的沒有區別

PUT /movie_index/movie/3
{
"id":"3",
"name":"incident red sea",
"doubanScore":"5.0",
"actorList":[
{"id":"1","name":"zhang chen"}
]
}

5.2.7修改—某個欄位

POST movie_index/movie/3/_update
{
"doc": {
"doubanScore":"7.0"
}
}

5.2.8 刪除一個document

DELETE movie_index/movie/3

5.2.9 搜索type全部數據

GET movie_index/movie/_search

結果

{
"took": 2, //耗費時間 毫秒
"timed_out": false, //是否超時
"_shards": {
"total": 5, //發送給全部5個分片
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3, //命中3條數據
"max_score": 1, //最大評分
"hits": [ // 結果
{
"_index": "movie_index",
"_type": "movie",
"_id": 2,


"_score": 1,
"_source": {
"id": "2",
"name": "operation meigong river",
"doubanScore": 8.0,
"actorList": [
{
"id": "1",
"name": "zhang han yu"
}
]
}
。。。。。。。。
。。。。。。。。
}

5.2.10 按條件查詢(全部)

GET movie_index/movie/_search
{
"query":{
"match_all": {}
}
}

5.2.11 按分詞查詢

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red"}
}
}

註意結果的評分

5.2.12 按分詞子屬性查詢

GET movie_index/movie/_search
{
"query":{
"match": {"actorList.name":"zhang"}
}
}

5.2.13 match phrase

GET movie_index/movie/_search
{
"query":{
"match_phrase": {"name":"operation red"}
}
}

按短語查詢,不再利用分詞技術,直接用短語在原始數據中匹配

5.2.14 fuzzy查詢

GET movie_index/movie/_search
{
"query":{
"fuzzy": {"name":"rad"}
}
}

校正匹配分詞,當一個單詞都無法準確匹配,es通過一種演算法對非常接近的單詞也給與一定的評分,能夠查詢出來,但是消耗更多的性能。

5.2.15 過濾--查詢後過濾

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red"}
},
"post_filter":{
"term": {
"actorList.id": 3
}
}
}

5.2.16 過濾--查詢前過濾(推薦)

GET movie_index/movie/_search
{
"query":{
"bool":{
"filter":[ {"term": { "actorList.id": "1" }},
{"term": { "actorList.id": "3" }}
],
"must":{"match":{"name":"red"}}
}
}
}

5.2.17 過濾--按範圍過濾

GET movie_index/movie/_search
{
"query": {
"bool": {
"filter": {
"range": {
"doubanScore": {"gte": 8}
}
}
}
}
}

關於範圍操作符:

gt

|

大於

---|---

lt

|

小於

gte

|

大於等於

lte

|

小於等於

5.2.18 排序

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red sea"}
}
, "sort": [
{
"doubanScore": {
"order": "desc"
}
}
]
}

5.2.19 分頁查詢

GET movie_index/movie/_search
{
"query": { "match_all": {} },
"from": 1,
"size": 1
}

5.2.20 指定查詢的欄位

GET movie_index/movie/_search
{
"query": { "match_all": {} },
"_source": ["name", "doubanScore"]
}

5.2.21 高亮

GET movie_index/movie/_search
{
"query":{
"match": {"name":"red sea"}
},
"highlight": {
"fields": {"name":{} }
}

}

5.2.22 聚合

取出每個演員共參演了多少部電影

GET movie_index/movie/_search
{
"aggs": {
"groupby_actor": {
"terms": {
"field": "actorList.name.keyword"
}
}
}
}

每個演員參演電影的平均分是多少,並按評分排序

GET movie_index/movie/_search
{
"aggs": {
"groupby_actor_id": {
"terms": {
"field": "actorList.name.keyword" ,
"order": {
"avg_score": "desc"
}
},
"aggs": {
"avg_score":{
"avg": {
"field": "doubanScore"
}
}
}
}
}
}

5.3 關於mapping

之前說type可以理解為table,那每個欄位的數據類型是如何定義的呢

查看看mapping

GET movie_index/_mapping/movie

實際上每個type中的欄位是什麼數據類型,由mapping定義。

但是如果沒有設定mapping系統會自動,根據一條數據的格式來推斷出應該的數據格式。

  • true/false → boolean
  • 1020 → long
  • 20.1 → double
  • “2018-02-01” → date
  • “hello world” → text +keyword

預設只有text會進行分詞,keyword是不會分詞的字元串。

mapping除了自動定義,還可以手動定義,但是只能對新加的、沒有數據的欄位進行定義。一旦有了數據就無法再做修改了。

註意:雖然每個Field的數據放在不同的type下,但是同一個名字的Field在一個index下只能有一種mapping定義。

5.4 中文分詞

elasticsearch本身自帶的中文分詞,就是單純把中文一個字一個字的分開,根本沒有辭彙的概念。但是實際應用中,用戶都是以辭彙為條件,進行查詢匹配的,如果能夠把文章以辭彙為單位切分開,那麼與用戶的查詢條件能夠更貼切的匹配上,查詢速度也更加快速。

分詞器下載網址:https://github.com/medcl/elasticsearch-analysis-ik

5.4.1 安裝

下載好的zip包,請解壓後放到 /usr/share/elasticsearch/plugins/ik

java電商項目全文搜索

然後重啟es

5.4.2測試使用

使用預設

GET movie_index/_analyze
{
"text": "我是中國人"
}

請觀察結果

使用分詞器

GET movie_index/_analyze
{ "analyzer": "ik_smart",
"text": "我是中國人"
}

請觀察結果

另外一個分詞器

ik_max_word

GET movie_index/_analyze
{ "analyzer": "ik_max_word",
"text": "我是中國人"
}

請觀察結果

能夠看出不同的分詞器,分詞有明顯的區別,所以以後定義一個type不能再使用預設的mapping了,要手工建立mapping, 因為要選擇分詞器。

5.4.3基於中文分詞搭建索引

1、建立mapping

PUT movie_chn
{
"mappings": {
"movie":{
"properties": {
"id":{
"type": "long"
},
"name":{
"type": "text"
, "analyzer": "ik_smart"
},
"doubanScore":{
"type": "double"
},
"actorList":{
"properties": {
"id":{
"type":"long"
},
"name":{
"type":"keyword"
}
}
}
}
}
}
}

插入數據

PUT /movie_chn/movie/1
{ "id":1,
"name":"紅海行動",
"doubanScore":8.5,
"actorList":[
{"id":1,"name":"張譯"},
{"id":2,"name":"海清"},
{"id":3,"name":"張涵予"}
]
}
PUT /movie_chn/movie/2
{
"id":2,
"name":"湄公河行動",
"doubanScore":8.0,
"actorList":[
{"id":3,"name":"張涵予"}
]
}
PUT /movie_chn/movie/3
{
"id":3,
"name":"紅海事件",
"doubanScore":5.0,
"actorList":[
{"id":4,"name":"張晨"}
]
}

查詢測試

GET /movie_chn/movie/_search
{
"query": {
"match": {
"name": "紅海戰役"
}
}
}
GET /movie_chn/movie/_search
{
"query": {
"term": {
"actorList.name": "張譯"
}
}
}

5.4.4 自定義詞庫

修改/usr/share/elasticsearch/plugins/ik/config/中的IKAnalyzer.cfg.xml

<?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://192.168.67.163/fenci/myword.txt</entry>
<!--用戶可以在這裡配置遠程擴展停止詞字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

按照標紅的路徑利用nginx發佈靜態資源

在nginx.conf中配置

server {
listen 80;
server_name 192.168.67.163;
location /fenci/ {
root es;
}
}

並且在/usr/local/nginx/下建/es/fenci/目錄,目錄下加myword.txt

myword.txt中編寫關鍵詞,每一行代表一個詞。

java電商項目全文搜索

然後重啟es伺服器,重啟nginx。

在kibana中測試分詞效果

java電商項目全文搜索

更新完成後,es只會對新增的數據用新詞分詞。歷史數據是不會重新分詞的。如果想要歷史數據重新分詞。需要執行:

POST movies_index_chn/_update_by_query?conflicts=proceed

三 Java程式中的應用

1 、搭建模塊

java電商項目全文搜索

java電商項目全文搜索

java電商項目全文搜索

pom.xml

_
_ < project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns: xsi
="http://www.w3.org/2001/XMLSchema-instance"
xsi :schemaLocation ="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd" >
< modelVersion >4.0.0</ modelVersion >
< groupId >com.atguigu.gmall</ groupId >
< artifactId >gmall-list-service</ artifactId >
< version >0.0.1-SNAPSHOT</ version >
< packaging >jar</ packaging >
< name >gmall-list-service</ name >
< description >Demo project for Spring Boot</ description >
< parent >
< groupId >com.atguigu.gmall</ groupId >
< artifactId >gmall-parent</ artifactId >
< version >1.0-SNAPSHOT</ version >
</ parent >
< dependencies >
< dependency >
< groupId >com.atguigu.gmall</ groupId >
< artifactId >gmall-interface</ artifactId >
< version >1.0-SNAPSHOT</ version >
</ dependency >
< dependency >
< groupId >com.atguigu.gmall</ groupId >
< artifactId >gmall-service-util</ artifactId >
< version >1.0-SNAPSHOT</ version >
</ dependency >
</ dependencies >
< build >
< plugins >
< plugin >
< groupId >org.springframework.boot</ groupId >
< artifactId >spring-boot-maven-plugin</ artifactId >
</ plugin >
</ plugins >
</ build >
</ project >


elasticsearch的 版本號,請提取到gmall-parent中

2、 關於es 的java 客戶端的選擇

目前市面上有兩類客戶端

一類是TransportClient 為代表的ES原生客戶端,不能執行原生dsl語句必須使用它的Java api方法。

另外一種是以Rest Api為主的missing client,最典型的就是jest。
這種客戶端可以直接使用dsl語句拼成的字元串,直接傳給服務端,然後返回json字元串再解析。

兩種方式各有優劣,但是最近elasticsearch官網,宣佈計劃在7.0以後的版本中廢除TransportClient。以RestClient為主。

java電商項目全文搜索

所以在官方的RestClient 基礎上,進行了簡單包裝的Jest客戶端,就成了首選, 而且該客戶端也與springboot完美集成。

3 、導入Jest依賴

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.searchbox/jest -->
<dependency>
<groupId>io.searchbox</groupId>
<artifactId>jest</artifactId>
<version>5.3.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.java.dev.jna/jna -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.5.1</version>
</dependency>

其中jest和jna請將版本號,部分納入gmall-parent中管理。spring-boot-starter-data-
elasticsearch不用管理版本號,其版本跟隨springboot的1.5.10大版本號。

4 、在測試類中測試ES

application.properties中加入

spring.elasticsearch.jest.uris=http://192.168.67.163:9200

測試類

@Autowired
JestClient jestClient ;
@Test
public void testEs() throws IOException {
String query= "{ \n " +
" " query " : { \n " +
" " match " : { \n " +
" " actorList.name " : " 張譯 "\n " +
" } \n " +
" } \n " +
"}" ;
Search search = new Search.Builder(query).addIndex( "movie_chn" ).addType(
"movie" ).build();
SearchResult result = jestClient .execute(search);
List<SearchResult.Hit<HashMap, Void>> hits = result.getHits(HashMap. class
);
for (SearchResult.Hit<HashMap, Void> hit : hits) {
HashMap source = hit. source ;
System. _ err _ .println( "source = " + source);
}
}


列印結果:

java電商項目全文搜索

以上技術方面的準備就做好了。下麵回到咱們電商的業務

三、利用elasticSearch開發電商的搜索列表功能

1、功能簡介

1.1入口: 兩個

首頁的分類

java電商項目全文搜索

搜索欄

java電商項目全文搜索

列表展示頁面

java電商項目全文搜索

2、根據業務搭建數據結構

這時我們要思考三個問題:

  1. 哪些欄位需要分詞
  2. 我們用哪些欄位進行過濾
  3. 哪些欄位我們需要通過搜索顯示出來。

需要分詞的欄位

|

sku名稱 sku描述

|

分詞、定義分詞器

---|---|---

有可能用於過濾的欄位

|

平臺屬性、三級分類、價格

|

要索引

其他需要顯示的欄位

|

skuId 圖片路徑

|

不索引

根據以上制定出如下結構:

PUT gmall
{
"mappings": {
"SkuInfo":{
"properties": {
"id":{
"type": "keyword"
, "index": false
},
"price":{
"type": "double"
},
"skuName":{
"type": "text",
"analyzer": "ik_max_word"
},
"skuDesc":{
"type": "text",
"analyzer": "ik_smart"
},
"catalog3Id":{
"type": "keyword"
},
"skuDefaultImg":{
"type": "keyword",
"index": false
},
"skuAttrValueList":{
"properties": {
"valueId":{
"type":"keyword"
}
}
}
}
}
}
}

3、sku數據保存到ES

思路:

回顧一下,es數據保存的dsl

PUT /movie_index/movie/1
{ "id":1,
"name":"operation red sea",
"doubanScore":8.5,
"actorList":[
{"id":1,"name":"zhang yi"},
{"id":2,"name":"hai qing"},
{"id":3,"name":"zhang han yu"}
]
}

es存儲數據是以json格式保存的,那麼如果一個javabean的結構剛好跟要求的json格式吻合,我們就可以直接把javaBean序列化為json保持到es中,所以我們要製作一個與es中json格式一致的javabean.

3.1 JavaBean

skuLsInfo

public class SkuLsInfo implements Serializable {
String id;
BigDecimal price;
String skuName;
String skuDesc;
String catalog3Id;
String skuDefaultImg;
Long hotScore;
List<SkuLsAttrValue> skuAttrValueList;
}

SkuLsAttrValue

public class SkuLsAttrValue implements Serializable {
String valueId;
}

3.2 保存sku數據的業務實現類

在gmall-list-service模塊中增加業務實現類listServiceImpl

public static final String index_name_gmall="gmall";
public static final String type_name_gmall="SkuInfo";
public void saveSkuInfo(SkuLsInfo skuLsInfo){
Index index= new Index.Builder(skuLsInfo).index(index_name_gmall).type(type_name_gmall).id(skuLsInfo.getId()).build();
try {
jestClient.execute(index);
} catch (IOException e) {
e.printStackTrace();
}
}

自行添加gmall-inteface中增加介面方法

3.3 在後臺管理的sku保存中,調用該方法

private void sendSkuToList(SkuInfo skuInfo){
SkuLsInfo skuLsInfo=new SkuLsInfo();
try {
BeanUtils.copyProperties(skuLsInfo, skuInfo);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
listService.saveSkuInfo(skuLsInfo);
}
public void saveSkuInfo(SkuInfo skuInfo){
……
sendSkuToList( skuInfo); //在保存sku最後保存到es
}

這時在界面中保存sku 是可以,自動向es發送數據的。

4、查詢數據的後臺方法

4.1 分析

首先先觀察功能頁面,咱們一共要用什麼查詢條件,查詢出什麼數據?

查詢條件:

  1. 關鍵字
  2. 可以通過 分類 進入列表頁面
  3. 屬性值
  4. 分頁頁碼

查詢結果:

1 sku的列表(關鍵字高亮顯示)

  1. 這些sku涉及了哪些屬性和屬性值
  2. 命中個數,用於分頁

基於以上

4.2 編寫DSL語句:

GET gmall/SkuInfo/_search
{
"query": {
"bool": {
"filter": [
{"terms":{ "skuAttrValueList.valueId": ["46","45"]}},
{"term":{"catalog3Id":"61"}}
],
"must":
{ "match": { "skuName": "128" } }
}
}
, "highlight": {


"fields": {"skuName":{}}
},
"from": 3,
"size": 1,
"sort":{"hotScore":{"order":"desc"}},
"aggs": {
"groupby_attr": {
"terms": {
"field": "skuAttrValueList.valueId"
}
}

}
}

4.3 製作傳入參數的類

public class SkuLsParams implements Serializable {
String keyword;
String catalog3Id;
String[] valueId;
int pageNo=1;
int pageSize=20;
}

4.4 返回結果的類

public class SkuLsResult implements Serializable {
List<SkuLsInfo> skuLsInfoList;
long total;
long totalPages;
List<String> attrValueIdList;
}

4.5 基於這個DSL查詢編寫Java代碼

public SkuLsResult search(SkuLsParams skuLsParams){
String query=makeQueryStringForSearch(skuLsParams);
Search search= new Search.Builder(query).addIndex(index_name_gmall).addType(type_name_gmall).build();
SearchResult searchResult=null;
try {
searchResult = jestClient.execute(search);
} catch (IOException e) {
e.printStackTrace();
}
SkuLsResult skuLsResult = makeResultForSearch(skuLsParams, searchResult);
return skuLsResult;
}

4.5.1 構造查詢DSL

查詢的過程很簡單,但是要構造查詢的query這個字元串有點麻煩,主要是這個Json串中的數據都是動態的。要拼接這個字元串,需要各種迴圈判斷,處理標點符號等等。操作麻煩,可讀性差。

但是jest這個客戶端包,提供了一組builder工具。這個工具可以比較方便的幫程式員組合複雜的查詢Json。

** private ** String makeQueryStringForSearch(SkuLsParams skuLsParams){
SearchSourceBuilder searchSourceBuilder= ** new ** SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders. boolQuery ();
** if ** (skuLsParams.getKeyword()!= ** null ** ){
MatchQueryBuilder matchQueryBuilder= ** new ** MatchQueryBuilder( **
"skuName" ** ,skuLsParams.getKeyword());
boolQueryBuilder.must(matchQueryBuilder);
HighlightBuilder highlightBuilder= ** new ** HighlightBuilder();
highlightBuilder.field( ** "skuName" ** );
highlightBuilder.preTags( ** "" ** );
highlightBuilder.postTags( ** "
" ** );
searchSourceBuilder.highlight(highlightBuilder);
TermsBuilder groupby_attr = AggregationBuilders. terms ( ** "groupby_attr"
** ).field( ** "skuAttrValueList.valueId" ** );
searchSourceBuilder.aggregation(groupby_attr);
}
** if ** (skuLsParams.getCatalog3Id()!= ** null ** ){
QueryBuilder termQueryBuilder= ** new ** TermQueryBuilder( ** "catalog3Id"
** ,skuLsParams.getCatalog3Id());
boolQueryBuilder.filter(termQueryBuilder);
}
** if ** (skuLsParams.getValueId()!= ** null ** &&skuLsParams.getValueId().
** length ** >= 0 ){
** for ** ( ** int ** i = 0 ; i < skuLsParams.getValueId(). ** length **
; i++) {
String valueId = skuLsParams.getValueId()[i];
QueryBuilder termQueryBuilder= ** new ** TermsQueryBuilder( **
"skuAttrValueList.valueId" ** ,valueId);
boolQueryBuilder.filter(termQueryBuilder);
}
}
searchSourceBuilder.query(boolQueryBuilder);
** int ** from =(skuLsParams.getPageNo()- 1 )*skuLsParams.getPageSize();
searchSourceBuilder.from(from);
searchSourceBuilder.size(skuLsParams.getPageSize());
searchSourceBuilder.sort( ** "hotScore" ** ,SortOrder. _** DESC ** _ );
String query = searchSourceBuilder.toString();
System. _** err ** _ .println( ** "query = " ** + query);
** return ** query;
}


4.5.2 處理返回值

思路:所有的返回值其實都在這個searchResult中

searchResult = jestClient.execute(search);

它的結構其實可以在kibana 中觀察一下:

命中的結果

java電商項目全文搜索

高亮顯示

java電商項目全文搜索

分組統計結果:

java電商項目全文搜索

針對這三個部分來解析searchResult

** private ** SkuLsResult makeResultForSearch(SkuLsParams
skuLsParams,SearchResult searchResult){
SkuLsResult skuLsResult= ** new ** SkuLsResult();
List skuLsInfoList= ** new **
ArrayList<>(skuLsParams.getPageSize());
_ //獲取sku列表
_ List<SearchResult.Hit<SkuLsInfo, Void>> hits =
searchResult.getHits(SkuLsInfo. ** class ** );
** for ** (SearchResult.Hit<SkuLsInfo, Void> hit : hits) {
SkuLsInfo skuLsInfo = hit. ** source ** ;
** if ** (hit. ** highlight ** != ** null ** &&hit. ** highlight **
.size()> 0 ){
List list = hit. ** highlight ** .get( ** "skuName" ** );
_ //把帶有高亮標簽的字元串替換skuName
_ String skuNameHl = list.get( 0 );
skuLsInfo.setSkuName(skuNameHl);
}
skuLsInfoList.add(skuLsInfo);
}
skuLsResult.setSkuLsInfoList(skuLsInfoList);
skuLsResult.setTotal(searchResult.getTotal());
_ //取記錄個數並計算出總頁數
_ ** long ** totalPage= (searchResult.getTotal() + skuLsParams.getPageSize()

  • 1 ) / skuLsParams.getPageSize();
    skuLsResult.setTotalPages( totalPage);
    _ //取出涉及的屬性值id
    _ List attrValueIdList= ** new ** ArrayList<>();
    MetricAggregation aggregations = searchResult.getAggregations();
    TermsAggregation groupby_attr = aggregations.getTermsAggregation( **
    "groupby_attr" ** );
    ** if ** (groupby_attr!= ** null ** ){
    List<TermsAggregation.Entry> buckets = groupby_attr.getBuckets();
    ** for ** (TermsAggregation.Entry bucket : buckets) {
    attrValueIdList.add( bucket.getKey()) ;
    }
    skuLsResult.setAttrValueIdList(attrValueIdList);
    }
    ** return ** skuLsResult;
    }

測試後臺程式…

完成了sku列表數據獲取的方法。回到頁面功能上來。

5、檢索的頁面

檢索功能

java電商項目全文搜索

5.1 創建gmall-list-web模塊

java電商項目全文搜索

java電商項目全文搜索

java電商項目全文搜索

5.1.1 pom.xml

<parent>
<groupId>com.atguigu.gmall</groupId>
<artifactId>gmall-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>com.atguigu.gmall</groupId>
<artifactId>gmall-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.atguigu.gmall</groupId>
<artifactId>gmall-web-util</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

5.1.2 靜態網頁及資源文件

拷貝靜態文件到resources目錄下,手工建立static和templates目錄

java電商項目全文搜索

手工替換一下目錄

把list.html的路徑中”../static/” 替換成 “/”

5.1.3 application.properties

server.port=8085
spring.thymeleaf.cache=false
spring.thymeleaf.mode=LEGACYHTML5

5.1.4 nginx配置

host文件

# gmall
192.168.67.163 item.gmall.com list.gmall.com manage.gmall.com resource.gmall.com

nginx.conf

upstream list.gmall.com{
server 192.168.67.1:8085;
}
server {
listen 80;
server_name list.gmall.com;
location / {
proxy_pass http:// list.gmall.com;
proxy_set_header X-forwarded-for $proxy_add_x_forwarded_for;
}
}

5.1.5 放置nginx首頁靜態資源

用xftp 拷貝首頁資源到nginx/gmall目錄下

java電商項目全文搜索

修改nginx.conf配置文件

server {
listen 80;
server_name www.gmall.com;
location / {
root gmall;
index index.html index.htm;
}
}

host文件

# gmall
192.168.67.163 item.gmall.com list.gmall.com manage.gmall.com www.gmall.com resource.gmall.com

5.2 sku列表功能

java電商項目全文搜索

首先是根據關鍵字、屬性值、分類Id、頁碼查詢sku列表。

5.2.1 ListController

@RequestMapping("list.html")
public String getList( SkuLsParams skuLsParams, Model model){
skuLsParams.setPageSize(4);
//根據參數返回sku列表
SkuLsResult skuLsResult = listService.search(skuLsParams);
model.addAttribute("skuLsInfoList",skuLsResult.getSkuLsInfoList());
return "list";
}

5.2.2 頁面html渲染

<div style="width:215px" th:each="skuLsInfo:${skuLsInfoList}" >
<p class="da">
<a href="#" th:onclick="'javascript:item(\''+${skuLsInfo.id}+'\')'">
<img th:src="${skuLsInfo.skuDefaultImg}" src="img/57d0d400Nfd249af4.jpg" class="dim">
</a>
</p>
<p class="tab_R">
<span th:text="'¥'+${#numbers.formatDecimal(skuLsInfo.price,1,2)}">¥5199.00</span>
</p>
<a href="#" title="" th:onclick="'javascript:item(\''+${skuLsInfo.id}+'\')'" class="tab_JE" th:utext="${skuLsInfo.skuName}" >
Apple iPhone 7 Plus (A1661) 32G 黑色 移動聯通電信4G手機
</a>
</div>

要註意的是其中skuName中因為關鍵字標簽所以必須要用utext否則標簽會被轉義。

5.2.3搜索欄相關html

<!--搜索導航-->
<div class="header_sous">
<div class="logo">
<a href="#"><img src="/image/logo1.jpg" alt=""></a>
</div>
<div class="header_form">
 <input id="keyword" name="keyword" type="text" placeholder="手機" />
<a href="#" onclick="searchList()">搜索</a>
 </div>
<div class="header_ico">
<div class="header_gw">
<span><a href="#">我的購物車</a></span>
<img src="/image/[email protected]" />
<span>0</span>
</div>
<div class="header_ko">
<p>購物車中還沒有商品,趕緊選購吧!</p>
</div>
</div>

5.2.4 js代碼

function searchList(){
var keyword = $("#keyword").val();
window.location.href="/list.html?keyword="+keyword;
}
function item(skuid) {
window.location.href="http://item.gmall.com/"+skuid+".html";
}

這時可以看到列表效果了。

5.3 頁面功能—提供可供選擇的屬性列表

5.3.1 思路:

這個列表有兩種情況

  1. 如果是通過首頁的3級分類點擊進入的,要按照分類Id查詢對應的屬性和屬性值列表。
  2. 如果是直接用搜索欄輸入文字進入的,要根據sku的查詢結果涉及的屬性值(好在我們已經通過es的聚合取出來了),再去查詢資料庫把文字列表顯示出來。

5.3.2 ListController

getList方法中添加

//根據查詢的結果返回屬性和屬性值列表
List<BaseAttrInfo> attrList=null;
if(skuLsParams.getCatalog3Id()!=null){
attrList = manageService.getAttrList(skuLsParams.getCatalog3Id());
}else {
List<String> attrValueIdList = skuLsResult.getAttrValueIdList();
if(attrValueIdList!=null&&attrValueIdList.size()>0){
attrList = manageService.getAttrList(attrValueIdList);
}
}
model.addAttribute("attrList",attrList);

其中 ** manageService ** .getAttrList(String catalog3Id)這個方法我們已經有現成的了。

但是 ** manageService ** .getAttrList(attrValueIdList) 這個方法我們要新添加。

5.3.3 在ManageServiceImpl中增加方法

@Override
public List<BaseAttrInfo> getAttrList(List<String> attrValueIdList) {
String attrValueIds = StringUtils.join(attrValueIdList.toArray(), ",");
List<BaseAttrInfo> baseAttrInfoList = baseAttrInfoMapper.selectAttrInfoListByIds(attrValueIds);
return baseAttrInfoList;
}

5.3.4 BaseAttrInfoMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper SYSTEM "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.gmall.manage.mapper.BaseAttrInfoMapper">
<select id ="selectAttrInfoList" parameterType="long" resultMap="attrInfoMap">
SELECT ba.id,ba.attr_name,ba.catalog3_id,
bv.id value_id ,bv.value_name, bv.attr_id FROM
base_attr_info ba INNER JOIN base_attr_value bv ON ba.id =bv.attr_id
where ba.catalog3_id=#{catalog3Id}
</select>
<resultMap id="attrInfoMap" type="com.atguigu.gmall.bean.BaseAttrInfo" autoMapping="true">
<result property="id" column="id" ></result>
<collection property="attrValueList" ofType="com.atguigu.gmall.bean.BaseAttrValue" autoMapping="true">
<result property="id" column="value_id" ></result>
</collection>
</resultMap>
 <select id ="selectAttrInfoListByIds" resultMap="attrInfoMap">
SELECT ba.id,ba.attr_name,ba.catalog3_id,
bv.id value_id ,bv.value_name, bv.attr_id FROM
base_attr_info ba INNER JOIN base_attr_value bv ON ba.id =bv.attr_id
where bv.id in (${attrValueIds})
</select>
</mapper>

註意這裡面沒有用#{}是因為attrValueIds 是兩個數字用逗號分開的,所以不能整體套上單引,所以使用${}。

5.3.5 BaseAttrInfoMapper.class

public interface BaseAttrInfoMapper extends Mapper<BaseAttrInfo> {
public List<BaseAttrInfo> selectAttrInfoList(long catalog3Id);
 public List<BaseAttrInfo> selectAttrInfoListByIds(@Param("attrValueIds") String attrValueIds);
}

此處必須要用@Param註解否則${ }無法識別。

5.3.6 點擊屬性值的鏈接

getAttrList(List
attrValueIdList)方法實現後,還有一個問題就是,點擊屬性時,要把上次查詢的內容也帶上,即帶上歷史參數。

private String makeUrlParam(String keyword,String catalog3Id, String[] valueIds,String excludeValueId ){
String url="";
List<String> paramList=new ArrayList<>();
if(keyword!=null&&keyword.length()>0){
paramList.add("keyword="+keyword);
}
if(catalog3Id!=null){
paramList.add("catalog3Id="+catalog3Id);
}
if(valueIds!=null) {
for (int i = 0; i < valueIds.length; i++) {
String valueId = valueIds[i];
if (!excludeValueId.equals(valueId)) {
paramList.add("valueId=" + valueId) ;
}
}
}
url = StringUtils.join(paramList, "&");
return url;
}

getList方法中增加

//歷史參數
String[] valueIds=skuLsParams.getValueId();
String catalog3Id = skuLsParams.getCatalog3Id();
String keyword=skuLsParams.getKeyword();
String urlParam = makeUrlParam(keyword,catalog3Id, valueIds, "");
model.addAttribute("urlParam",urlParam);

java部分的代碼就完成了。

5.3.7 生成屬性列表的html部分

<div class="GM_selector">
<!--手機商品篩選-->
<div class="title">
<h3><em>商品篩選</em></h3>
</div>
<div class="GM_nav_logo">
<div class="GM_pre" th:each="attrInfo:${attrList}">
<div class="sl_key">
<span th:text="${attrInfo.attrName}+':'">屬性:</span>
</div>
<div class="sl_value">
<ul>
<li th:each="attrValue:${attrInfo.attrValueList}">
  <a th:href="'/list.html?'+${urlParam}+'&valueId='+${attrValue.id}" th:text="${attrValue.valueName}">屬性值</a>
</li>
</ul>
</div>
</div>
</div>
</div>

完成後

java電商項目全文搜索

5.4 頁面功能--麵包屑

java電商項目全文搜索

麵包屑導航是為了能夠讓用戶清楚的知道當前頁面的所在位置和篩選條件的功能。但是這個小的人性化功能卻有點麻煩。

功能點: 1、點擊某個屬性值的時候對應的那行屬性要消失掉不能再次選擇。

2、列在上面的屬性麵包屑,要可以取消掉,恢復到沒選擇之前。

5.4.1 思路:

  1. 把本應顯示的列表與用戶已選擇的屬性值列表用迴圈交叉判斷,如果匹配把本應顯示的那個屬性去掉。
  2. 已選擇的屬性值列表,要攜帶點擊跳轉的路徑,這個路徑參數就是咱們上邊講的那個“歷史參數”,但是要把自己本身的屬性值去掉。

5.4.2 擴展類

增加BaseAttrValueExt類 ,這個類是擴展了原BaseAttrValue類為了方便攜帶參數。

public class BaseAttrValueExt extends BaseAttrValue {
String attrName ;
String cancelUrlParam;
}

5.4.3 controller中的getList方法增加

代碼如下

_ //去掉已選擇的屬性值
_ ** if ** (skuLsParams.getValueId()!= ** null ** &&attrList!= ** null ** )
{
** for ** ( ** int ** i = 0 ; valueIds != ** null ** && i < valueIds. **
length ** ; i++) {
String selectedValueId = valueIds[i];
** for ** (Iterator iterator = attrList.iterator();
iterator.hasNext(); ) {
BaseAttrInfo baseAttrInfo = iterator.next();
List attrValueList = baseAttrInfo.getAttrValueList();
** for ** (BaseAttrValue baseAttrValue : attrValueList) {
** if ** (selectedValueId.equals(baseAttrValue.getId())) {
BaseAttrValueExt baseAttrValueExt = ** new ** BaseAttrValueExt();
baseAttrValueExt.setAttrName(baseAttrInfo.getAttrName());
baseAttrValueExt.setId(baseAttrValue.getId());
baseAttrValueExt.setValueName(baseAttrValue.getValueName());
baseAttrValueExt.setAttrId(baseAttrInfo.getId());
String cancelUrlParam = makeUrlParam(keyword,catalog3Id, valueIds,
selectedValueId);
baseAttrValueExt.setCancelUrlParam(cancelUrlParam);
selectedValuelist.add(baseAttrValueExt);
iterator.remove(); _ // 如果選中了該屬性 就從屬性列表中去掉
_ }
}
}
}
}
model.addAttribute( ** "selectedValueList" ** , selectedValuelist);
model.addAttribute( ** "keyword" ** ,keyword);
model.addAttribute( ** "attrList" ** ,attrList);


這塊代碼看似多層迴圈嵌套性能隱患,其實因為單次迴圈基本不會超過五次,迴圈中沒有網路或者io訪問,完全在虛擬機中運行,所以即使多層迴圈嵌套壓力也不會太大。

5.4.4 頁面代碼

< div class= "GM_ipone" >
< div class= "GM_ipone_bar" >
< div class= "GM_ipone_one a" >
篩選條件
</ div >
< i >< img src= "/image/[email protected]" alt= "" ></ i >
< span th :if= "${keyword}!=null" th :text= "' " '+${keyword}+' " '"

</ span >
< a class= "select-attr" th :each= "selectedValue:${selectedValueList}"
th :href= "'/list.html?'+${selectedValue.cancelUrlParam}" th :utext=
"${selectedValue.attrName}+':'+${selectedValue.valueName} +' X '" >
屏幕尺寸:5.1-5.5英寸
</ a >
</ div >
</ div >


5.5 頁面功能--分頁

由於咱們在es中查詢數據時已經完成了後臺分頁,這裡主要就是把分頁結果丟給前臺頁面展示就好了。

註意每個頁面都要帶上歷史參數。

5.5.1 controller中的getList裡加入

long totalPages = skuLsResult.getTotalPages();
model.addAttribute("totalPages",totalPages);
model.addAttribute("pageNo",skuLsParams.getPageNo());

至此ListController 就全部完成了

5.5.2 ListController代碼

@Controller
** public class ** ListController {
@Reference
ManageService ** manageService ** ;
@Reference
ListService ** listService ** ;
@RequestMapping ( ** "list.html" ** )
** public ** String getList( SkuLsParams skuLsParams, Model model){
skuLsParams.setPageSize( 4 );
_ //根據參數返回sku列表
_ SkuLsResult skuLsResult = ** listService ** .search(skuLsParams);
model.addAttribute( ** "skuLsInfoList" ** ,skuLsResult.getSkuLsInfoList());
_ //根據查詢的結果返回屬性和屬性值列表
_ List attrList= ** null ** ;
** if ** (skuLsParams.getCatalog3Id()!= ** null ** ){
attrList = ** manageService ** .getAttrList(skuLsParams.getCatalog3Id());
} ** else ** {
List attrValueIdList = skuLsResult.getAttrValueIdList();
** if ** (attrValueIdList!= ** null ** &&attrValueIdList.size()> 0 ){
attrList = ** manageService ** .getAttrList(attrValueIdList);
}
}
model.addAttribute( ** "attrList" ** ,attrList);
_ //歷史參數
_ String[] valueIds=skuLsParams.getValueId();
String catalog3Id = skuLsParams.getCatalog3Id();
String keyword=skuLsParams.getKeyword();
String urlParam = makeUrlParam(keyword,catalog3Id, valueIds, ** "" ** );
model.addAttribute( ** "urlParam" ** ,urlParam);
List selectedValuelist = ** new ** ArrayList<>();
_ //去掉已選擇的屬性值
_ ** if ** (skuLsParams.getValueId()!= ** null ** &&attrList!= ** null ** )
{
** for ** ( ** int ** i = 0 ; valueIds != ** null ** && i < valueIds. **
length ** ; i++) {
String selectedValueId = valueIds[i];
** for ** (Iterator iterator = attrList.iterator();
iterator.hasNext(); ) {
BaseAttrInfo baseAttrInfo = iterator.next();
List attrValueList = baseAttrInfo.getAttrValueList();
** for ** (BaseAttrValue baseAttrValue : attrValueList) {
** if ** (selectedValueId.equals(baseAttrValue.getId())) {
BaseAttrValueExt baseAttrValueExt = ** new ** BaseAttrValueExt();
baseAttrValueExt.setAttrName(baseAttrInfo.getAttrName());
baseAttrValueExt.setId(baseAttrValue.getId());
baseAttrValueExt.setValueName(baseAttrValue.getValueName());
baseAttrValueExt.setAttrId(baseAttrInfo.getId());
String cancelUrlParam = makeUrlParam(keyword,catalog3Id, valueIds,
selectedValueId);
baseAttrValueExt.setCancelUrlParam(cancelUrlParam);
selectedValuelist.add(baseAttrValueExt);
iterator.remove(); _ // 如果選中了該屬性 就從屬性列表中去掉
_ }
}
}
}
}
model.addAttribute( ** "selectedValueList" ** , selectedValuelist);
model.addAttribute( ** "keyword" ** ,keyword);
model.addAttribute( ** "attrList" ** ,attrList);
** long ** totalPages = skuLsResult.getTotalPages();
model.addAttribute( ** "totalPages" ** ,totalPages);
model.addAttribute( ** "pageNo" ** ,skuLsParams.getPageNo());
_ //以下是查詢商品信息
_ ** return "list" ** ;
}
** private ** String makeUrlParam(String keyword,String catalog3Id, String[]
valueIds,String excludeValueId ){
String url= ** "" ** ;
List paramList= ** new ** ArrayList<>();
** if ** (keyword!= ** null ** &&keyword.length()> 0 ){
paramList.add( ** "keyword=" ** +keyword);
}
** if ** (catalog3Id!= ** null ** ){
paramList.add( ** "catalog3Id=" ** +catalog3Id);
}
** if ** (valueIds!= ** null ** ) {
** for ** ( ** int ** i = 0 ; i < valueIds. ** length ** ; i++) {
String valueId = valueIds[i];
** if ** (!excludeValueId.equals(valueId)) {
paramList.add( ** "valueId=" ** + valueId) ;
}
}
}
url = StringUtils. join (paramList, ** "&" ** );
** return ** url;
}
}


5.5.3 分頁的頁面部分代碼

< ** script th :inline= "javascript" ** >
_ /< 上一頁' ** ; ** if ** ( pageNo == 1 ){ $lastPage = ** '< 上一頁' ** ; } ** $ ** ( ** ". page_span1 " ** ). append ( $lastPage ); ** for ** ( ** var ** i = 1 ; i <= totalPage ; i ++) { ** if ** ( i == pageNo ){ ** $ ** ( ** ". page_span1 " ** ). append ( ** '' ** \+ i \+ ** '' ** ); } ** else if ** ( i == 1 || i == totalPage ||( i >=( pageNo \- 2 )&& i <=( pageNo \+ 2 ))){ ** $ ** ( ** ". page_span1 " ** ). append ( ** '' ** \+ i \+ ** '' ** ); } ** else if ** (( i ==( pageNo \+ 3 ) && i < totalPage )||( i ==( pageNo \- 3 ) && i > 1 )){ ** $ ** ( ** ". page_span1 " ** ). append ( ** '...' ** ); } } _ //下一頁 _ ** var ** $nextPage = ** '下一頁 >' ** ; ** if ** ( pageNo == totalPage || totalPage == 0 ){ $nextPage = ** '下一頁 >' ** ; } ** $ ** ( ** ". page_span1 " ** ). append ( $nextPage ); }) _ /*]]>/
_ </ ** script ** >


分頁欄由6部分組成,通過判斷頁面決定顯示效果。

java電商項目全文搜索

6、排序

頁面結構完成了,考慮一下如何排序,es查詢的dsl語句中我們是用了hotScore來進行排序的。

但是hotScore從何而來,根據業務去定義,也可以擴展更多類型的評分,讓用戶去選擇如何排序。

這裡的hotScore我們假定以點擊量來決定熱度。

那麼我們每次用戶點擊,將這個評分+1,不就可以了麽。

6.1 問題:

1、 es大量的寫操作會影響es 性能,因為es需要更新索引,而且es不是記憶體資料庫,會做相應的io操作。

2、而且修改某一個值,在高併發情況下會有衝突,造成更新丟失,需要加鎖,而es的樂觀鎖會惡化性能問題。

從業務角度出發,其實我們為商品進行排序所需要的熱度評分,並不需要非常精確,大致能比出個高下就可以了。

利用這個特點我們可以稀釋掉大量寫操作。

6.2 解決思路:

用redis做精確計數器,redis是記憶體資料庫讀寫性能都非常快,利用redis的原子性的自增可以解決併發寫操作。

redis每計100次數(可以被100整除)我們就更新一次es ,這樣寫操作就被稀釋了100倍,這個倍數可以根據業務情況靈活設定。

代碼 在listServiceImpl中增加更新操作

6.3 代碼:更新es

private void updateHotScore(String skuId,Long hotScore){
String updateJson="{\n" +
" \"doc\":{\n" +
" \"hotScore\":"+hotScore+"\n" +
" }\n" +
"}";
Update update = new Update.Builder(updateJson).index("gmall").type("SkuInfo").id(skuId).build();
try {
jestClient.execute(update);
} catch (IOException e) {
e.printStackTrace();
}
}

6.4 更新redis計數器

//更新熱度評分
@Override
public void incrHotScore(String skuId){
Jedis jedis = redisUtil.getJedis();
int timesToEs=100;
Double hotScore = jedis.zincrby("hotScore", 1, "skuId:" + skuId);
if(hotScore%timesToEs==0){
updateHotScore( skuId, Math.round(hotScore) );
}
}

6.5 詳情頁調用

這個 incrHotScore 方法可以在進入詳情頁面的時候調用。

@Controller
public class ItemController {
@Reference
ListService listService;
@Reference
ManageService manageService;
@RequestMapping("/{skuId}.html")
public String getSkuInfo(@PathVariable("skuId") String skuId, Model model){
……
listService.incrHotScore(skuId); //最終應該由非同步方式調用
}

ore(skuId); //最終應該由非同步方式調用
}


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • R ggplot2 繪製 PCA 主成分分析圖,每個繪圖像素都自己掌控的感覺倍兒爽~ ...
  • java web 學習記錄一下 mvc結構實現mysql 連接 什麼是mvc MVC是模型(model)、視圖(view)、控制(controller)這三個單詞上的首字母組成。它是一種應用模型,它的目的是實現Web系統的職能分工。避免all in one 所有代碼全部寫在一個文件里的一種分工模型。 ...
  • 兄弟們,上一個系列大家多少有點不太喜歡,那今天上點不一樣的。 來吧,直接整活~ 先準備一下 首先咱們需要安裝一下這兩個第三方模塊 requests >>> # pip install requests parsel >>> # pip install parsel 不會安裝的小伙伴,鍵盤按住win+ ...
  • 來源:https://blog.csdn.net/qq_48289488/article/details/121905018 Podman 簡介 什麼是Podman? Podman 是一個開源的容器運行時項目,可在大多數 Linux 平臺上使用。Podman 提供與 Docker 非常相似的功能。正 ...
  • 餅圖常用於統計學模塊,畫餅圖用到的方法為:pie( ) 一、pie()函數用來繪製餅圖 pie(x, explode=None, labels=None, colors=None, autopct=None, pctdistance=0.6, shadow=False, labeldistance= ...
  • 目前【騰訊雲簡訊】為客戶提供【國內簡訊】、【國內語音】和【海外簡訊】三大服務,騰訊雲簡訊SDK支持以下操作: 國內簡訊 國內簡訊支持操作: • 指定模板單發簡訊 • 指定模板群發簡訊 • 拉取簡訊回執和簡訊回覆狀態 海外簡訊 海外簡訊支持操作: • 指定模板單發簡訊 • 指定模板群發簡訊 • 拉取短 ...
  • 前言 最近在學習java,遇到了一個經典列印題目,空心金字塔,初學者記錄,根據網上教程,有一句話感覺很好,就是先把麻煩的問題轉換成很多的簡單問題,最後一一解決就可以了,然後先死後活,先把程式寫死,後面在改成活的。 如下圖是空心金字塔最終的實現效果,先要求用戶輸入層數然後輸出 一.普通矩形 首先我們先 ...
  • 近日,New Relic發佈了最新的2022 Java生態系統報告,這份報告可以幫助我們深入的瞭解Java體系的最新使用情況,下麵就一起來看看2022年,Java發展的怎麼樣了,還是Java 8 YYDS嗎? Java 11成為新的標準 在2020年的時候,Java 11已經推出了1年多,但當時Ja ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...