Java項目筆記之首頁和全文搜索


不點藍字,咱們哪來故事?


網站首頁


es/mysql/mongodb/redis區別

關係型數據庫: MySQL

關係型數據庫是一種基於關係的數據庫,而關係模型可經過二維表來進行表示,因此數據的存儲方式是由行列組成的表,每一列是一個字段,每一行是一個記錄。在關係型數據庫中一般包含了三個概念:數據庫(database)、表(table)、記錄(record)。在大部分關係型數據庫中,都是適用B+樹做爲索引,好比MySQL。php

  • MySQL也是一種硬盤型數據庫,操做數據是IO級別的,它全部的數據都是存放在硬盤中,須要使用的時候纔會交換到內存中。所以MySQL可以處理海量的數據,可是數據量很大的時,速度會稍慢。html

  • MySQL的使用須要提早建表,不適用於數據結構變換頻繁的狀況前端

非關係型數據庫:MongoDB、Redis
MongoDB介紹

MongoDB是由c++語言編寫的非關係型數據庫,是一個基於分佈式文件存儲的開源數據庫系統,其內容存儲相似JSON對象,它的字段能夠包含其餘的文檔、數組以及文檔數組。MongoDB包含了三個層次概念:數據庫(database)、集合(collection)、文檔(document)。MongoDB的數據索引是B-樹。vue

  • MongoDB 在建立數據庫的時候,會直接在磁盤上面分配一組數據文件,全部的集合、索引和數據庫的其餘元數據都保存在這些文件中。java

  • 在使用MongoDB中,操做系統會經過mmap將進程所須要的全部數據都映射到虛擬內存中,而後在將當前須要處理的數據映射到內存中。當須要訪問的數據不在虛擬內存的時候,會觸發page fault,而後os就會硬盤中的數據加載到虛擬內存和內存中。而當內存已滿時,會觸發swap-out操做,將一些數據寫回硬盤。因此有了這種內存映射文件的方法,就會有種好像全部須要訪問的數據都在內存裏同樣。mysql

  • MongoDB的特色:c++

    • 提供面向文檔存儲,操做簡單web

    • 擴展性強,第三方支持豐富面試

    • 具備failover機制(失效轉移:一種備份操做模式,當一個系統由於一些故障沒法完成工做的時候,另外一個系統自動接替已失效系統的工做繼續執行)redis

    • 支持大容量存儲,內置GridFS(可用於存放大量的小文件)

    • 在高負載的狀況下,能夠添加更多的節點,保證服務器性能

  • 缺點

    • 無事務機制(數據庫事務(database transaction)對單個的邏輯單元執行一系列的操做,要麼徹底執行,要麼徹底不執行)

    • 佔用空間過大

    • 沒有mysql那樣成熟的維護工具

  • 適用場景

    • 適合那種數據格式不明確或者常常變化的模型,好比事件記錄、內容管理或者博客平臺。

Redis

Redis是一種內存數據庫,全部的數據都是放在內存之中,按期寫入磁盤中,當內存不夠的時候,可選擇指定的LRU算法刪除數據。Redis是基於哈希字典創建的,所以其索引方式是哈希。

  • 特色

    • 因爲數據存放在內存中,所以讀寫性能高

    • 支持豐富的數據類型,如鍵值對、集合、列表、散列存儲


elasticsearch

一、Elasticsearch和MongoDB/Redis/Memcache同樣,是非關係型數據庫。是一個接近實時的搜索平臺,從索引這個文檔到這個文檔可以被搜索到只有一個輕微的延遲,企業應用定位:採用Restful API標準的可擴展和高可用的實時數據分析的全文搜索工具。

二、可拓展:支持一主多從且擴容簡易,只要cluster.name一致且在同一個網絡中就能自動加入當前集羣;自己就是開源軟件,也支持不少開源的第三方插件。

三、高可用:在一個集羣的多個節點中進行分佈式存儲,索引支持shards和複製,即便部分節點down掉,也能自動進行數據恢復和主從切換。

三、採用RestfulAPI標準:經過http接口使用JSON格式進行操做數據。

四、數據存儲的最小單位是文檔,本質上是一個JSON 文本;

實際項目開發中,幾乎每一個系統都會有一個搜索的功能,數據量少時能夠直接從主數據庫中好比Mysql搜索,但當搜索作到必定程度時,好比系統數據量上了10億、100億條的時候,傳統的關係型數據庫的I/O性能和統計分析性能就難以知足用戶須要了。因此不少公司都會把搜索單獨作成一個獨立的模塊,用ElasticSearch等來實現。雖然內存緩存數據庫的讀寫性能很高,但徹底把數據放在內存中是不太現實的


需求:使用es作站內搜索


核心:怎麼將mongodb中的數據添加到elasticsearch中,同步哪一些數據?

好比:搜索遊記中title和summary中含有廣州字樣的遊記,做爲以廣州爲條件搜索的結果,首先要到mongodb中去把知足條件的數據找到顯示出來。

  1. 從mongodb中同步條件列數據以及主鍵id到es中(推薦:由於內存資源寶貴,選擇犧牲性能)

    先匹配es中條件列搜索知足條件的數據,獲得主鍵id集合,而後以id集合做爲條件去mongodb中對應的id數據集合,以後再頁面顯示;

    • 優勢:節省內存空間(數據量小了);

    • 缺點:稍微有損性能(去兩個數據庫中查詢了);

  2. 從mongodb中同步頁面須要的全部數據(包括條件列數據)以及主鍵id,把數據都放到es中存起來

    先匹配es中條件列搜索知足條件的數據,獲得數據集合,直接在頁面顯示;

    • 優勢:查詢快(全部的數據都在es中了);

    • 缺點:內存空間消耗大(數據量大了);


關鍵字搜索

進入首頁後,輸入關鍵字, 選擇不一樣搜索維度(默認是所有), 進入搜索頁面

關鍵字搜索, 也稱之站內搜索, 系統暫時僅對攻略, 遊記, 目的地, 用戶進行關鍵字查詢, 固然也支持所有查詢。

1:關鍵詞搜索

所有搜索, 會對目的地, 攻略, 遊記, 用戶對象(關鍵字段)進行全文搜索

目的地:名稱(name), 簡介(info)

攻略:標題(title), 副標題(subTitle), 概要(summary)

遊記:標題(title), 概要(summary)

用戶:簡介(info), 城市(city)

查詢到的關鍵詞進行高亮顯示


添加依賴:

 <!--elasticsearch--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.3</version> </dependency>

es的配置:

es中數據的初始化

目的地:

search.domain

search.repository

search.service


攻略:

其餘組件是同樣的,拷貝替換就好;


遊記:

其餘組件是同樣的,拷貝替換就好;


用戶:

其餘組件是同樣的,拷貝替換就好;


初始化controller
 @RestController public class DataController {  //es相關服務 @Autowired private IDestinationEsService destinationEsService; @Autowired private IStrategyEsService strategyEsService; @Autowired private ITravelEsService travelEsService; @Autowired private IUserInfoEsService userInfoEsService;   //mongodb相關服務 @Autowired private IDestinationService destinationService; @Autowired private IStrategyService strategyService; @Autowired private ITravelService travelService; @Autowired private IUserInfoService userInfoService;   @GetMapping("/dataInit") public Object dataInit() {  //攻略須要存到es中的數據初始化 List<Strategy> sts = strategyService.list(); for (Strategy st : sts) { StrategyEs es = new StrategyEs(); BeanUtils.copyProperties(st, es); strategyEsService.save(es); } //遊記須要存到es中的數據初始化 List<Travel> ts = travelService.list(); for (Travel t : ts) { TravelEs es = new TravelEs(); BeanUtils.copyProperties(t, es); travelEsService.save(es); }  //用戶須要存到es中的數據初始化 List<UserInfo> uf = userInfoService.list(); for (UserInfo u : uf) { UserInfoEs es = new UserInfoEs(); BeanUtils.copyProperties(u, es); userInfoEsService.save(es); }   //目的地須要存到es中的數據初始化 List<Destination> dests = destinationService.list(); for (Destination d : dests) { DestinationEs es = new DestinationEs(); BeanUtils.copyProperties(d, es); destinationEsService.save(es); }  return "ok"; } }

查到數據放到es中:

同理可得,剩下的拷貝。


啓動服務器:檢查head中的數據是否按要求建立好了索引了:

索引信息必定要和配置的一致


打開mongodb數據庫:必需要保證全部的數據是合法合規的,把本身加的錯誤的壞的數據刪了。


以後再進行數據的初始化:發出初始化數據的請求,剛剛設置的controller

查看初始化完成的數據是否正確:



關鍵字搜索

注意:目的地是精確搜索,無高亮顯示,找不到就找不到;其餘的是全文搜索,關鍵字高亮顯示;


目的地關鍵詞搜索

目的查詢:輸入關鍵詞是精確查詢輸入的地區, 若是找到, 顯示該目的地下全部攻略, 遊記, 用戶

若是目的找不到, 顯示:


前端代碼:

查看首頁前臺代碼引用了rip-website\js\vue\common.js:

高查條件的封裝,後面要用於分頁,根據前臺以int類型來區分集中不一樣的搜索目標來設計qo:


全部的搜索請求都是同一個映射地址:

一個方法中完成不一樣的搜索目的,如何區分?

怎麼將這些不一樣的搜索類型區分開:用switch語句

這樣處理還有一個問題:不一樣的搜索目標類型,請求的返回數據是不同的

如何處理:由分支的方法本身來處理;


目的地關鍵詞搜索:

system/search/searchDest.js

頁面html:

trip-website\views\search\searchDest.html

顯然result是鍵值對的存在,使用map仍是用對象(相似vo)封裝,選擇第二種;


後臺:

JPA中定義的方法ByXxx()要去檢查一下es中是否有Xxx屬性,不然報錯:

去哪個數據庫查詢數據給前臺?由前臺須要顯示的數據來決定。es能不能知足頁面全部的顯示的數據。


其餘的三個查詢方式相同;


定義封裝result數據的類型:

用result封裝數據:

返回結果


測試查看查詢的數據:

查不到數據:

get請求的時候:會將中文字符進行編碼了,

後臺須要解碼,才能轉換成中文:

再測試:

看頁面少了引用:

報錯:找不到用戶暱稱,查看數據有沒有到後天,查看前臺有沒有按要求封裝數據;

頭像沒了:打印後臺傳過來的數據,發現沒有頭像信息;

測試,0條的0沒有顯示:或者在SearchResultVO中設置total默認值爲0;



攻略全文搜索:

僅僅對攻略進行全文搜索

攻略:標題(title), 副標題(subTitle), 概要(summary)

拷貝接口:

拷貝實現類:

修改BeanUtils

爲何這麼寫:由於查詢高亮的接口的定義,對好比下:


測試:

查看攻略查詢結果正不正常,有沒有高亮顯示關鍵字;


攻略

遊記

用戶


所有

默認狀況下查詢所有顯示:

數據的封裝:


測試:




全文搜索方法設計的解釋:

EQL語句全文檢索:

方法設計:

根據上面的語句如何設計全文搜索的方法:這個方法中有重複的操做,怎麼保證通用性呢?————使用泛型設計方法,方法的可變參數

 /** * 全部 es 公共服務,全文搜索並高亮顯示關鍵詞 */ public interface ISearchService {  /** * 全文搜索 + 高亮顯示 * * @param index 索引 * @param type 類型 * @param clz 經過字節碼對象告訴Page<T>中的 T 究竟是什麼類型,傳什麼封裝什麼 * @param qo 高查條件(關鍵詞等)都在qo中 * @param fields 字段:須要對哪些字段中的內容作關鍵詞匹配,不一樣的需求字段不同,可變參數可完美匹配 * @param <T> * @return 帶有分頁的全文搜索(高亮顯示)結果集,返回的結果集用泛型來達到通用的目的 * <p> * <T> 泛型方法的語法:申明泛型,讓java不去解析 T 具體是什麼類型,不加就報沒法解析的錯。 */ <T> Page<T> searchWithHighlight(String index, String type, Class<T> clz, SearchQueryObject qo, String... fields);  }


方法中須要作什麼:

EQL語句查詢到的響應結果:

怎麼把結果解析成前臺認識的頁面:

高亮解析:


代碼:

 @Service public class SearchServiceImpl implements ISearchService { @Autowired private IUserInfoService userInfoService; @Autowired private IStrategyService strategyService; @Autowired private ITravelService travelService; @Autowired private IDestinationService destinationService;   @Autowired private ElasticsearchTemplate template;   //類比:select * from xxx where title like %#{keyword}% or subTitle like %#{keyword}% or summary like %#{keyword}% //關鍵字: keyword = 廣州 //以title爲例: //原始匹配: "有娃必看,廣州長隆野生動物園全攻略" //高亮顯示後:"有娃必看,<span style="color:red;">廣州</span>長隆野生動物園全攻略" @Override public <T> Page<T> searchWithHighlight(String index, String type, Class<T> clz, SearchQuery qo, String... fields) { String preTags = "<span style='color:red;'>"; String postTags = "</span>";  //須要進行高亮顯示的字段對象, 他是一個數組, 個數由搜索的字段個數決定: fields 個數決定 //fields : title subTitle summary HighlightBuilder.Field[] fs = new HighlightBuilder.Field[fields.length]; for (int i = 0; i < fs.length; i++) { //最終查詢結果: <span style="color:red;">廣州</span> fs[i] = new HighlightBuilder.Field(fields[i]) .preTags(preTags) //拼接高亮顯示關鍵字的開始的樣式 <span style="color:red;"> .postTags(postTags);//拼接高亮顯示關鍵字的結束的樣式 </span> }  NativeSearchQueryBuilder searchQuery = new NativeSearchQueryBuilder(); searchQuery.withIndices(index) //設置搜索索引 .withTypes(type); // 設置搜索類型 /*"query":{ "multi_match": { "query": "廣州", "fields": ["title","subTitle","summary"] } },*/ searchQuery.withQuery(QueryBuilders.multiMatchQuery(qo.getKeyword(), fields)); //拼接查詢條件 /** "from": 0, "size":3, */ searchQuery.withPageable(qo.getPageable()); //分頁操做  //高亮顯示 /** "highlight": { "fields" : { "title" : {}, "subTitle" : {}, "summary" : {} } } */ searchQuery.withHighlightFields(fs);  //List<UserInfoEs> es = template.queryForList(searchQuery.build(), UserInfoEs.class);  //調用template.queryForPage 實現結果處理 //參數1:DSL語句封裝對象 //參數2:返回Page對象中list的泛型 //參數3:SearchResultMapper 全文搜索返回的結果處理對象 // 功能: 將DSL語句執行結果處理成Page 分頁對象 return template.queryForPage(searchQuery.build(), clz, new SearchResultMapper() { ///mapResults 真正處理DSL語句返回結果 方法 //參數1: DSL語句查詢結果 //參數2: 最終處理完以後, 返回Page對象中list的泛型 //參數3: 分頁對象 @Override public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) { List<T> list = new ArrayList<>(); SearchHits hits = response.getHits(); //結果對象中hist 裏面包含全文搜索結果集 SearchHit[] searchHits = hits.getHits();//結果對象中hist的hit 裏面包含全文搜索結果集 for (SearchHit searchHit : searchHits) { T t = mapSearchHit(searchHit, clazz); //必須使用擁有高亮顯示的效果字段替換原先的數據 //參數1: 原始對象(字段中沒有高亮顯示) //參數2:具備高亮顯示效果字段, 他是一個map集合, key: 高亮顯示字段名, value: 高亮顯示字段值對象 //參數3:須要替換全部字段 Map<String, String> map = highlightFieldsCopy(searchHit.getHighlightFields(), fields); //BeanUtils.copyProperties(map, t);  /*兩個不一樣包下BeanUtils工具類的區別: 1.springboot 框架中的BeanUtils類,若是參數是map集合,將沒法進行屬性的複製 copyProperties(源, 目標); 2.Apache 的BeanUtils類,能夠對map進行屬性的複製 copyProperties(目標, 源); */ try { BeanUtils.copyProperties(t, map); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }  list.add(t); }  //將結果集封裝成分頁對象Page : 參數1:查詢數據, 參數2:分頁數據, 參數3:查詢到總記錄數 AggregatedPage<T> result = new AggregatedPageImpl<>(list, pageable, response.getHits().getTotalHits()); return result; }  @Override public <T> T mapSearchHit(SearchHit searchHit, Class<T> clz) { String id = searchHit.getSourceAsMap().get("id").toString(); T t = null; if (clz == UserInfo.class) { t = (T) userInfoService.get(id); } else if (clz == Travel.class) { t = (T) travelService.get(id); } else if (clz == Strategy.class) { t = (T) strategyService.get(id); } else if (clz == Destination.class) { t = (T) destinationService.get(id); } else { t = null; } return t; } }); }   //fields: title subTitle summary private Map<String, String> highlightFieldsCopy(Map<String, HighlightField> map, String... fields) {  Map<String, String> mm = new HashMap<>(); //title: "<em>廣州</em>小吃名店紅黑榜:你仍是當年珠江畔那個老字號嗎?" //subTitle: "<em>廣州</em>小吃名店紅黑榜" //summary: "企鵝吃喝指南|「城市指南「第4站-<em>廣州</em> 小吃篇"  //title subTitle summary for (String field : fields) {  HighlightField hf = map.get(field); if (hf != null) { //獲取高亮顯示字段值, 由於是一個數組, 全部使用string拼接 Text[] fragments = hf.fragments(); String str = ""; for (Text text : fragments) { str += text; } mm.put(field, str); //使用map對象將全部能替換字段先緩存, 後續統一替換 //BeanUtils.setProperty(t,field, str); 識別一個替換一個 } } return mm; }  }






java學途

只分享有用的Java技術資料 

掃描二維碼關注公衆號

 


筆記|學習資料|面試筆試題|經驗分享 

若有任何需求或問題歡迎騷擾。微信號:JL2020aini

或掃描下方二維碼添加小編微信

 




小夥砸,歡迎再看分享給其餘小夥伴!共同進步!




本文分享自微信公衆號 - java學途(javaxty)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索