上一章:《第四章:SpringBoot整合ElasticSearch實作模糊查詢,批量CRUD,排序,分頁,高亮》
文章目錄
-
- 5.1 ElasticSearchRepository的基本使用
- 5.2 ElasticsearchRestTemplate的使用
- 5.3 實戰
-
- 1.實體類
- 2.Elasticsearch Service
- 3.查詢
在上一章節,我們學習到了es通過RestHighLevelClient實作最基本的增删改查的文法,在本章我們繼續深入實踐一下es的相關操作,在SpringBoot的相關依賴中,es已經幫我們将基本的操作都進行了封裝,我們隻需要掌握這些api,便可以輕松的操作我們的es
本章主角:
ElasticSearchRepository,
ElasticsearchRestTemplateSpring-data-elasticsearch是Spring提供的操作ElasticSearch的資料層,封裝了大量的基礎操作,通過它可以很友善的操作ElasticSearch的資料。
對于相關的依賴以及配置請參考第四章資訊
5.1 ElasticSearchRepository的基本使用
@NoRepositoryBean
public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> {
<S extends T> S index(S var1);
Iterable<T> search(QueryBuilder var1);
Page<T> search(QueryBuilder var1, Pageable var2);
Page<T> search(SearchQuery var1);
Page<T> searchSimilar(T var1, String[] var2, Pageable var3);
void refresh();
Class<T> getEntityClass();
}
我們是通過繼承ElasticsearchRepository來完成基本的CRUD及分頁操作的,和普通的JPA沒有什麼差別。
我們使用方式也很簡單,寫一個接口然後繼承它即可
ElasticsearchRepository<,>- 第一個就是所準備的實體類
- 第二個是id的類型
public interface EsUserService extends ElasticsearchRepository<User, Integer> {
}
然後便可以進行相關的增删改查以及批量操作
特殊情況下,ElasticsearchRepository裡面有幾個特殊的search方法,這些是ES特有的,和普通的JPA差別的地方,用來建構一些ES查詢的。
主要是看
QueryBuilder和
SearchQuery兩個參數,要完成一些特殊查詢就主要看建構這兩個參數。
我們先來看看它們之間的類關系
從這個關系中可以看到ES的search方法需要的參數SearchQuery是一個接口,有一個實作類叫NativeSearchQuery,實際使用中,
我們的主要任務就是建構NativeSearchQuery來完成一些複雜的查詢的。
我們可以看到要建構NativeSearchQuery,主要是需要幾個構造參數
public NativeSearchQuery(QueryBuilder query, QueryBuilder filter, List<SortBuilder> sorts, Field[] highlightFields) {
this.query = query;
this.filter = filter;
this.sorts = sorts;
this.highlightFields = highlightFields;
}
當然了,我們沒必要實作所有的參數。
可以看出來,大概是需要QueryBuilder,filter,和排序的SortBuilder,和高亮的字段。
一般情況下,我們不是直接是new NativeSearchQuery,而是使用NativeSearchQueryBuilder。
通過如下所示的方式來完成NativeSearchQuery的建構。
從名字就能看出來,QueryBuilder主要用來建構查詢條件、過濾條件,SortBuilder主要是建構排序。
譬如,我們要查詢距離某個位置100米範圍内的所有人、并且按照距離遠近進行排序:
double lat = 39.929986;
double lon = 116.395645;
Long nowTime = System.currentTimeMillis();
//查詢某經緯度100米範圍内
GeoDistanceQueryBuilder builder = QueryBuilders.geoDistanceQuery("address").point(lat, lon)
.distance(100, DistanceUnit.METERS);
GeoDistanceSortBuilder sortBuilder = SortBuilders.geoDistanceSort("address")
.point(lat, lon)
.unit(DistanceUnit.METERS)
.order(SortOrder.ASC);
Pageable pageable = new PageRequest(0, 50);
NativeSearchQueryBuilder builder1 = new NativeSearchQueryBuilder().withFilter(builder).withSort(sortBuilder).withPageable(pageable);
SearchQuery searchQuery = builder1.build();
要完成字元串的查詢:
要建構QueryBuilder,我們可以使用工具類QueryBuilders,裡面有大量的方法用來完成各種各樣的QueryBuilder的建構,字元串的、Boolean型的、match的、地理範圍的等等。
要建構SortBuilder,可以使用SortBuilders來完成各種排序。
然後就可以通過NativeSearchQueryBuilder來組合這些QueryBuilder和SortBuilder,再組合分頁的參數等等,最終就能得到一個SearchQuery了。至此,我們明白了ElasticSearchRepository裡那幾個search查詢方法需要的參數的含義和建構方式了。
5.2 ElasticsearchRestTemplate的使用
ElasticSearchTemplate更多是對ESRepository的補充,裡面提供了一些更底層的方法。
原來的ElasticsearchTemplate已經過時了
這裡主要是一些查詢相關的,同樣是建構各種SearchQuery條件。
雖然ElasticsearchRestTemplate裡也包括save之類的JPA操作,但适用于小資料量,要完成超大資料的插入就要用ES自帶的bulk了,可以迅速插入百萬級的資料。
在ElasticsearchRestTemplate裡也提供了對應的方法
public void bulkIndex(List<Person> personList) {
int counter = 0;
try {
if (!elasticsearchTemplate.indexExists(PERSON_INDEX_NAME)) {
elasticsearchTemplate.createIndex(PERSON_INDEX_TYPE);
}
List<IndexQuery> queries = new ArrayList<>();
for (Person person : personList) {
IndexQuery indexQuery = new IndexQuery();
indexQuery.setId(person.getId() + "");
indexQuery.setObject(person);
indexQuery.setIndexName(PERSON_INDEX_NAME);
indexQuery.setType(PERSON_INDEX_TYPE);
//上面的那幾步也可以使用IndexQueryBuilder來建構
//IndexQuery index = new IndexQueryBuilder().withId(person.getId() + "").withObject(person).build();
queries.add(indexQuery);
if (counter % 500 == 0) {
elasticsearchTemplate.bulkIndex(queries);
queries.clear();
System.out.println("bulkIndex counter : " + counter);
}
counter++;
}
if (queries.size() > 0) {
elasticsearchTemplate.bulkIndex(queries);
}
System.out.println("bulkIndex completed.");
} catch (Exception e) {
System.out.println("IndexerService.bulkIndex e;" + e.getMessage());
throw e;
}
}
這裡是建立了100萬個Person對象,每到500就用bulkIndex插入一次,速度飛快,以秒的速度插入了百萬資料。
講了那麼多理論,下面我們具體實踐一下
5.3 實戰
1.實體類
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Data
@Document(indexName = "user")//索引名稱 建議與實體類一緻
public class User {
@Id
private Integer id;
@Field(type = FieldType.Auto)//自動檢測類型
private Integer age;
@Field(type = FieldType.Keyword)//手動設定為keyword 但同時也就不能分詞
private String name;
@Field(type = FieldType.Text, analyzer = "ik_smart", searchAnalyzer = "ik_max_word")//設定為text 可以分詞
private String info;
}
如果沒有看過第三章:es的相關概念的話,這些注解看起來可能比較麻煩,為了節約大家時間,我把用到的相關概念在重複說一下
- Keyword 用于索引結構化内容的字段,例如電子郵件位址,主機名,狀态代碼,郵政編碼或标簽。它們通常用于過濾,排序,和聚合。 Keyword 字段隻能按其确切值進行搜尋。
- Text
用于索引全文值的字段,例如電子郵件正文或産品說明。
這些字段是被分詞的,它們通過分詞器傳遞 ,以在被索引之前将字元串轉換為單個術語的清單。
分析過程允許 Elasticsearch 搜尋單個單詞中每個完整的文本字段。文本字段不用于排序,很少用于聚合。
- IK提供兩種分詞ik_smart和ik_max_word,其中ik_smart為最少切分,ik_max_word為最細粒度劃分。
由于在本章之前還未教大家安裝ik分詞器,是以可以跳過它,暫時不用考慮
2.Elasticsearch Service
public interface EsUserService extends ElasticsearchRepository<User,Integer> {
//根據name查詢
List<User> findByName(String name);
//根據name和info查詢
List<User> findByNameAndInfo(String name,String info);
}
3.查詢
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
@RestController
public class EsController {
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
@Autowired
private EsUserService esUserService;
private String[] names = {"諸葛亮", "曹操", "李白", "韓信", "趙雲", "小喬", "狄仁傑", "李四", "諸小明", "王五"};
private String[] infos = {"我來自中國的一個小鄉村,地處湖南省", "我來自中國的一個大城市,名叫上海,人們稱作魔都"
, "我來自杭州,這是一個浪漫的城市"};
/**
* 儲存資料
*
* @return
*/
@GetMapping("saveUser")
public Object saveUser() {
//添加索引mapping索引會自動建立但mapping自隻用預設的這會導緻分詞器不生效 是以這裡我們手動導入mapping
Random random = new Random();
List<User> users = new ArrayList<>();
for (int i = 0; i < 20; i++) {
User user = new User();
user.setId(i);
user.setName(names[random.nextInt(9)]);
user.setAge(random.nextInt(40) + i);
user.setInfo(infos[random.nextInt(2)]);
users.add(user);
}
Iterable<User> users1 = esUserService.saveAll(users);
return users1;
}
/**
* 通過id查詢資料
*
* @return
*/
@GetMapping("getDataById")
public Object getDataById(@RequestParam(value = "id") Integer id) {
return esUserService.findById(id);
}
/**
* 分頁查詢
*
* @return
*/
@GetMapping("getAllDataByPage")
public Object getAllDataByPage() {
//本該傳入page和size,這裡為了友善就直接寫死了
//表示通過Id正序排序
Pageable page = PageRequest.of(0, 3, Sort.Direction.ASC, "id");
Page<User> all = esUserService.findAll(page);
return all.getContent();
}
/**
* 根據名字查詢
*
* @param name
* @return
*/
@GetMapping("getDataByName")
public Object getDataByName(@RequestParam(value = "name") String name) {
return esUserService.findByName(name);
}
/**
* 通過名字和Info取交集查詢
*
* @param name
* @param info
* @return
*/
@GetMapping("getDataByNameAndInfo")
public Object getDataByNameAndInfo(String name, String info) {
//這裡是查詢兩個字段取交集,即代表兩個條件需要同時滿足
return esUserService.findByNameAndInfo(name, info);
}
/**
* 分詞高亮查詢
*
* @param value
* @return
*/
@GetMapping("getHightByUser")
public Object getHightByUser(@RequestParam(value = "value") String value) {
//根據一個值查詢多個字段 并高亮顯示 這裡的查詢是取并集,即多個字段隻需要有一個字段滿足即可
//需要查詢的字段
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
.should(QueryBuilders.matchQuery("info", value))
.should(QueryBuilders.matchQuery("name", value));
//建構高亮查詢
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withHighlightFields(
new HighlightBuilder.Field("info")
, new HighlightBuilder.Field("name"))
.withHighlightBuilder(new HighlightBuilder().preTags("<span style='color:red'>").postTags("</span>"))
.build();
//查詢
SearchHits<User> search = elasticsearchTemplate.search(searchQuery, User.class);
//得到查詢傳回的内容
List<SearchHit<User>> searchHits = search.getSearchHits();
//設定一個最後需要傳回的實體類集合
List<User> users = new ArrayList<>();
//周遊傳回的内容進行處理
for (SearchHit<User> searchHit : searchHits) {
//高亮的内容
Map<String, List<String>> highlightFields = searchHit.getHighlightFields();
//将高亮的内容填充到content中
searchHit.getContent().setName(highlightFields.get("name") == null ? searchHit.getContent().getName() : highlightFields.get("name").get(0));
searchHit.getContent().setInfo(highlightFields.get("info") == null ? searchHit.getContent().getInfo() : highlightFields.get("info").get(0));
//放到實體類中
users.add(searchHit.getContent());
}
return users;
}
}
值得我們關注的是分詞高亮接口裡使用就是elasticsearchTemplate進行查詢的,如果我們想使用分頁,隻要在searchQuery添加:
自己可以在getHightByUser接口中修改嘗試一下
git位址:https://gitee.com/ninesuntec/es-better.git