框架,本質上是一些實用經驗集合。即是前輩們在實際開發過程中積攢下來的實戰經驗,累積成一套實用工具,避免你在開發過程中重覆去造輪子,特別是幫你把日常中能遇到的場景或問題都給屏蔽掉,框架的意義在於屏蔽掉開發的基礎複雜度、屏蔽掉此類共性的東西,同時建立嚴格的編碼規範,讓框架使用者開箱即用,並且只需要關註差... ...
尊重原創版權: https://www.gewuweb.com/hot/6213.html
java電商項目全文搜索
尊重原創版權: https://www.gewuweb.com/sitemap.html
搜索
什麼是搜索, 電腦根據用戶輸入的關鍵詞進行匹配,從已有的資料庫中摘錄出相關的記錄反饋給用戶。
常見的全網搜索引擎,像百度、谷歌這樣的。但是除此以外,搜索技術在垂直領域也有廣泛的使用,比如淘寶、京東搜索商品,萬芳、知網搜索期刊,csdn中搜索問題貼。也都是基於海量數據的搜索。
1、如何處理搜索
1.1用傳統關係性資料庫
弊端:
1) 對於傳統的關係性資料庫對於關鍵詞的查詢,只能逐字逐行的匹配,性能非常差。
2)匹配方式不合理,比如搜索“小密手機” ,如果用like進行匹配,
根本匹配不到。但是考慮使用者的用戶體驗的話,除了完全匹配的記錄,還應該顯示一部分近似匹配的記錄,至少應該匹配到“手機”。
1.2專業全文索引是怎麼處理的
全文搜索引擎目前主流的索引技術就是倒排索引的方式。
傳統的保存數據的方式都是
記錄→單詞
而倒排索引的保存數據的方式是
單詞→記錄
例如
搜索“紅海行動”
但是資料庫中保存的數據如圖:
那麼搜索引擎是如何能將兩者匹配上的呢?
基於分詞技術構建 倒排索引 :
首先每個記錄保存數據時,都不會直接存入資料庫。系統先會對數據進行分詞,然後以倒排索引結構保存。如下:
然後等到用戶搜索的時候,會把搜索的關鍵詞也進行分詞,會把“ 紅海行動 ”分詞分成: 紅海 和 行動 兩個詞。
這樣的話,先用紅海進行匹配,得到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 新增文檔
- 格式 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
然後重啟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中編寫關鍵詞,每一行代表一個詞。
然後重啟es伺服器,重啟nginx。
在kibana中測試分詞效果
更新完成後,es只會對新增的數據用新詞分詞。歷史數據是不會重新分詞的。如果想要歷史數據重新分詞。需要執行:
POST movies_index_chn/_update_by_query?conflicts=proceed
三 Java程式中的應用
1 、搭建模塊
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為主。
所以在官方的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);
}
}
列印結果:
以上技術方面的準備就做好了。下麵回到咱們電商的業務
三、利用elasticSearch開發電商的搜索列表功能
1、功能簡介
1.1入口: 兩個
首頁的分類
搜索欄
列表展示頁面
2、根據業務搭建數據結構
這時我們要思考三個問題:
- 哪些欄位需要分詞
- 我們用哪些欄位進行過濾
- 哪些欄位我們需要通過搜索顯示出來。
需要分詞的欄位
|
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 sku的列表(關鍵字高亮顯示)
- 這些sku涉及了哪些屬性和屬性值
- 命中個數,用於分頁
基於以上
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 中觀察一下:
命中的結果
高亮顯示
分組統計結果:
針對這三個部分來解析searchResult
** private ** SkuLsResult makeResultForSearch(SkuLsParams
skuLsParams,SearchResult searchResult){
SkuLsResult skuLsResult= ** new ** SkuLsResult();
List
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
_ //把帶有高亮標簽的字元串替換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
_ ListattrValueIdList= ** 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、檢索的頁面
檢索功能
5.1 創建gmall-list-web模塊
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目錄
手工替換一下目錄
把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目錄下
修改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列表功能
首先是根據關鍵字、屬性值、分類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 思路:
這個列表有兩種情況
- 如果是通過首頁的3級分類點擊進入的,要按照分類Id查詢對應的屬性和屬性值列表。
- 如果是直接用搜索欄輸入文字進入的,要根據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>
完成後
5.4 頁面功能--麵包屑
麵包屑導航是為了能夠讓用戶清楚的知道當前頁面的所在位置和篩選條件的功能。但是這個小的人性化功能卻有點麻煩。
功能點: 1、點擊某個屬性值的時候對應的那行屬性要消失掉不能再次選擇。
2、列在上面的屬性麵包屑,要可以取消掉,恢復到沒選擇之前。
5.4.1 思路:
- 把本應顯示的列表與用戶已選擇的屬性值列表用迴圈交叉判斷,如果匹配把本應顯示的那個屬性去掉。
- 已選擇的屬性值列表,要攜帶點擊跳轉的路徑,這個路徑參數就是咱們上邊講的那個“歷史參數”,但是要把自己本身的屬性值去掉。
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
** 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
** 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部分組成,通過判斷頁面決定顯示效果。
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); //最終應該由非同步方式調用
}