文章目錄
- Spring整合Elasticsearch
- 引入依賴
- 配置Elasticsearch
- 解決沖突
- 使用Elasticsearch
- Spring Data Elasticsearch
- 建立映射關系
- 常用方法
- 添加數據
- 修改數據
- 刪除數據
- 搜索數據(es核心)
- 步驟
- 構造搜索條件 并 應用
- 進行查詢
- 使用查詢結果
Spring整合Elasticsearch
引入依賴
- spring-boot-starter-data-elasticsearch
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency>
配置Elasticsearch
- cluster-name集群名
- cluster-nodes集群節點
# ElasticsearchProperties
# 配置集群名,與es配置文件中的一致
spring.data.elasticsearch.cluster-name=nowcoder
# 集群節點,格式 節點ip地址:端口
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
解決沖突
如果項目中使用了redis,則需要解決沖突
es和redis都基于netty,這兩者在啟動netty時,會產生沖突:系統會認為redis已經啟動了netty,es無法再啟動
要盡可能在服務啟動早期的時候,修改es.set.netty.runtime.available.processors為 false
修改入口類,因為入口類是最先被加載的
@PostConstruct: 管理bean的生命周期,主要用于初始化的方法,該注解修飾的方法在構造器調用完以后被執行
在這個初始化方法中修改系統屬性就足夠早
@SpringBootApplication
public class CommunityApplication {@PostConstructpublic void init() {// 解決netty啟動沖突問題// es.set.netty.runtime.available.processors 從 Netty4Utils.setAvailableProcessors() 中找到// 設置系統屬性System.setProperty("es.set.netty.runtime.available.processors", "false");}public static void main(String[] args) {SpringApplication.run(CommunityApplication.class, args);}}
使用Elasticsearch
Spring Data Elasticsearch
用于訪問es服務器的API
-
ElasticsearchTemplate :有特殊情況,DiscussPostRepository處理不了時使用
-
ElasticsearchRepository: 接口,需要定義一個子接口繼承他,聲明訪問哪些數據,Spring會自動實現這個接口
所有的代碼都是Spring自動生成的,Spring會自動將實體數據和es服務器的索引進行映射,因此需要用注解
代碼實例:
// es可以看成特殊的數據庫,因此加上注解@Repository // @Mapper是MyBa'ti'd專有注解 // @Repository是spring提供的,針對數據訪問層的注解 @Repository // es的接口一般取名XXXRepository,該接口訪問的是帖子,故叫DiscussPostRepository public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {// 繼承時要用泛型聲明:當前接口要處理的實體類,以及實體類中的id類型// 父接口ElasticsearchRepository中 已經定義好了對es服務器訪問的增刪改查方法// 聲明完泛型,加上注解之后,spring會自動實現自定義的子接口DiscussPostRepository }
建立映射關系
要對spring說明哪個實體類和es的索引怎樣進行對應,建立映射關系,映射完成后,spring底層就可以幫我們生成實現類
-
用@Document指明 表 和 es 中索引的對應關系
@Document ( indexName = “…”, type = “…”, shards = , replicas = )
indexName: 實體數據映射到哪個索引上。通常為全小寫的類名
type: 實體數據映射到哪個類型上。類型已經在逐步被弱化甚至取消了,因此寫成固定的 _doc
shards: 創建幾個分片。根據服務器處理能力配
replicas: 創建幾個副本。
沒有指定索引會創建這個索引,并且是根據指定分片和副本進行創建的 -
指明 實體中屬性 和 es中字段 的對應關系
給類中每個 屬性 上加注解用于和 索引中的字段 相關聯
? 表的id屬性要 加 @id 注解
@Id // 與索引中id字段對應private int id;
? 其他普通屬性 加 @Field注解并指明字段類型
// 用于普通字段,需指明字段類型@Field(type = FieldType.Integer)private int userId;
? 當某些屬性 對應的 es字段要用于關鍵詞匹配時,需在注解中指明使用的analyzer和searchAnalyzer
? analyzer為存儲時候的解析器/分詞器。
當我們存一句話時,會提取出關鍵詞,并用關鍵詞關聯這句話,搜索時就可以通過關鍵詞搜到這句話
因此存的時候,因該盡可能將一句話拆出盡可能多的關鍵詞,以擴大搜索范圍。
故需要一個范圍非常大的分詞器,而我們安裝的中文分詞器中存在這樣的分詞器——ik_max_word
? searchAnalyzer為搜索時候解析器/分詞器
搜索時,輸入的句子不需要拆出過多關鍵詞,不用拆的過細
如”互聯網校招“,可以拆出:互聯網、聯網、網校、校招等關鍵詞,但實際上我們沒有這些意思
此時要使用拆分出盡可能少但滿足用戶需求的詞語——ik_smart
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")private String content;
// 將數據庫中帖子存到es服務器里,就可以去es服務器中搜索這些帖子了
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3)
public class DiscussPost {@Id // 與索引中id字段對應private int id;// userId為普通字段@Field(type = FieldType.Integer)private int userId;// 搜帖子主要在title和content中查找@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")private String title;@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")private String content;// 不用在這些字段進行搜索,就不用analyzer和searchAnalyzer屬性@Field(type = FieldType.Integer)private int type;@Field(type = FieldType.Integer)private int status;@Field(type = FieldType.Date)private Date createTime;@Field(type = FieldType.Integer)private int commentCount;@Field(type = FieldType.Double)private double score;}
常用方法
添加數據
一次添加一條數據:save(一條數據)
@Testpublic void testInsert() {// 給es服務器添加數據:save(一條數據)// 在mysql中找到一條數據discussMapper.selectDiscussPostById(241),添加到es服務器// 不用特地創建索引,索引不存在時,es會幫我們自動創建discussRepository.save(discussMapper.selectDiscussPostById(241));discussRepository.save(discussMapper.selectDiscussPostById(242));discussRepository.save(discussMapper.selectDiscussPostById(243));}
一次添加多條數據:saveAll(多條數據)
@Testpublic void testInsertList() {// 一次添加多條數據:saveAll(多條數據)// discussMapper.selectDiscussPosts(101, 0, 100) mysql分頁查找discussRepository.saveAll(discussMapper.selectDiscussPosts(101, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(102, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(103, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(111, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(112, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(131, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(132, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(133, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(134, 0, 100));}
修改數據
調用save方法將之前的數據再覆蓋一遍
@Testpublic void testUpdate() {// 查出第231條數據,修改屬性DiscussPost post = discussMapper.selectDiscussPostById(231);post.setContent("我是新人,使勁灌水.");// 用save覆蓋原來的discussRepository.save(post);}
刪除數據
一次刪除一條數據:deleteById( id )
@Testpublic void testDelete() {discussRepository.deleteById(231);}
一次刪除所有數據:deleteAll
風險高,不常用
@Testpublic void testDelete() {discussRepository.deleteAll();}
搜索數據(es核心)
步驟
-
構造搜索條件 并 應用
搜索條件:要不要排序、分頁、結果要不要高亮顯示等
高亮顯示:給關鍵詞加em標簽,在文本顯示到網頁上時,前端可以給em加樣式
搜索條件構造方式:SearchQuery對象,實現類是NativeSearchQuery,而NativeSearchQueryBuilder是一個可以構造NativeSearchQuery的工具類
SearchQuery searchQuery = new NativeSearchQueryBuilder()// 1)指定查詢條件:withQuery// 查詢條件由QueryBuilders對象構造,multiMatchQuery用于指定查詢關鍵詞和查詢字段范圍.withQuery(QueryBuilders.multiMatchQuery("互聯網寒冬", "title", "content"))// 2)指定排序條件// 優先按照置頂排序,再按分數(精品貼會被折算成分數),都相同就按創建時間排序.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)).withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC)).withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))// 3)指定分頁條件.withPageable(PageRequest.of(0, 10))// 4)指定給哪些字段里匹配詞進行高亮顯示.withHighlightFields(new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>"))// 5)執行,即應用搜索條件.build();
-
進行查詢
分頁查詢結果用spring提供的Page對象接收
Page中封裝多個實體,即當前這一頁的實體方法一:用Repository進行搜索
Page<DiscussPost> page = discussRepository.search(searchQuery);
存在問題:
es返回結果包含:原始結果(即匹配到的結果) 和 高亮顯示部分(即匹配到的關鍵詞前后一部分內容,不是整個內容,不會浪費空間)
需要將高亮顯示部分整合到原始結果中,進行一個替換,太過麻煩,不夠完善問題原因:
查詢方法discussRepository.search(searchQuery)的源碼底層調用如下方法進行查詢:
elasticTemplate.queryForPage(searchQuery, class, SearchResultMapper)
得到的兩份數據,需要用SearchResultMapper進行組裝,但默認實現類底層沒有組裝,即底層獲取得到了高亮顯示部分, 但是沒有返回(結果里看不到).
方法二:直接用ElasticsearchTemplate進行搜索
elasticTemplate. queryForPage(搜索條件, 實體類型, 處理兩部分結果合并問題的接口)
通過匿名內部類方式,實現接口:
new SearchResultMapper() {@Overridepublic <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {... ...} }
實現接口方法:
-
獲取搜索命令查詢結果
通過response獲取搜索命令的數據,可能會得到多條數據,放在SearchHits中
SearchHits hits = response.getHits();
-
判斷結果是否為空
搜索命令返回結果的數據量,即返回結果有幾條數據
if (hits.getTotalHits() <= 0) {return null; }
-
遍歷每一條數據,轉成目標實體存儲
將這些實體存儲在集合中List<DiscussPost> list
for (SearchHit hit : hits)
hits中每一條數據hit的形式如下
1)處理非高亮顯示內容:
獲取非高亮內容:
es的返回數據是json格式,SearchHit對象里將json格式對象數據封裝成了Map格式
hit.getSourceAsMap():可以獲取map形式數據,通過指定map的key可以調用每一個字段的值
處理步驟:
(1)對所有的字段,不管實際有沒有高亮顯示,都先獲取非高亮顯示版本不能直接獲取高亮顯示內容并存入實體,可能導致某些實體屬性為空,因為不確定具體在哪個字段中匹配到關鍵字,某些字段可能沒有匹配到關鍵字
后續處理高亮顯示數據時,會用有高亮顯示的字段,覆蓋 實體屬性 原來的非高亮內容
hit.getSourceAsMap().get("id")
(2)再把 獲取到的任何類型的數據 都轉成 字符串
String id = hit.getSourceAsMap().get("id").toString();
(3)存到java實體中時,轉為對應類型
post.setId(Integer.valueOf(id));
2)處理高亮顯示內容
獲取高亮顯示數據
hit.getHighlightFields()
(1)獲取指定字段高亮顯示數據
HighlightField contentField = hit.getHighlightFields().get("content");
高亮數據格式:
(2)判斷該字段是否有高亮顯示數據:有些字段中沒有關鍵字,就沒有高亮內容
if (contentField != null)
(3)有高亮顯示數據時,獲取高亮內容第一段
getFragments():返回值是個數組,將內容做了分段,每一段都是 匹配的詞語 前后的一部分內容,如上圖
由于字段中匹配的詞語可能是多個,因此我們只需要第一段設置高亮了就可以
contentField.getFragments()[0].toString()
(4)存入實體對應屬性(此時就替換了屬性中非高亮數據)
post.setContent(contentField.getFragments()[0].toString());
-
返回一個包含 實體集合 的數據
方法返回值是AggregatedPage類型
因此為需要構造AggregatedPage接口的實現類AggregatedPageImpl
實現類中會傳多個參數,參數順序需要看底層源碼// list: 結果集合 // pageable:方法的參數 // hits.getTotalHits():數據總條數 return new AggregatedPageImpl(list, pageable, hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
代碼匯總
Page<DiscussPost> page = elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {@Overridepublic <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {SearchHits hits = response.getHits();if (hits.getTotalHits() <= 0) {return null;}List<DiscussPost> list = new ArrayList<>();for (SearchHit hit : hits) {DiscussPost post = new DiscussPost();// 處理非高亮顯示結果String id = hit.getSourceAsMap().get("id").toString();post.setId(Integer.valueOf(id));String userId = hit.getSourceAsMap().get("userId").toString();post.setUserId(Integer.valueOf(userId));String title = hit.getSourceAsMap().get("title").toString();post.setTitle(title);String content = hit.getSourceAsMap().get("content").toString();post.setContent(content);String status = hit.getSourceAsMap().get("status").toString();post.setStatus(Integer.valueOf(status));String createTime = hit.getSourceAsMap().get("createTime").toString();// String轉Date:String-->Long-->Datepost.setCreateTime(new Date(Long.valueOf(createTime)));String commentCount = hit.getSourceAsMap().get("commentCount").toString();post.setCommentCount(Integer.valueOf(commentCount));// 處理高亮顯示的結果HighlightField titleField = hit.getHighlightFields().get("title");if (titleField != null) {post.setTitle(titleField.getFragments()[0].toString());}HighlightField contentField = hit.getHighlightFields().get("content");if (contentField != null) {post.setContent(contentField.getFragments()[0].toString());}list.add(post);}return new AggregatedPageImpl(list, pageable,hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());}});
-
-
使用查詢結果
// 一共查到多少數據匹配System.out.println(page.getTotalElements());// 一共有多少頁System.out.println(page.getTotalPages());// 當前處在第幾頁System.out.println(page.getNumber());// 每一頁顯示多少條數據System.out.println(page.getSize());// 遍歷Page中數據,逐一查看// Page繼承了Iterable接口,可以被遍歷for (DiscussPost post : page) {System.out.println(post);}