當前Spring Boot非常流行,包括我本身,也是在用Spring Boot集成其餘框架進行項目開發,因此這一節,咱們一塊兒來探討Spring Boot整合ElasticSearch的問題。php
本文主要講如下內容:html
第一部分,通讀文檔java
第二部分,Spring Boot整合ElasticSearchnode
第三部分,基本的CRUD操做git
第四部分,搜索github
第五部分,例子web
尚未學過Elasticsearch的朋友,能夠先學這個系列的第一節(這個系列共三節),若是你有不明白或者不正確的地方,能夠給我評論、留言或者私信。spring
Spring Data Elasticsearch 官方文檔,這是當前最新的文檔。apache
文檔一開始就介紹 CrudRepository
,好比,繼承 Repository
,其餘好比 JpaRepository
、MongoRepository
是繼承CrudRepository
。也對其中的方法作了簡單說明,咱們一塊兒來看一下:json
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> { // Saves the given entity. <S extends T> S save(S entity); // Returns the entity identified by the given ID. Optional<T> findById(ID primaryKey); // Returns all entities. Iterable<T> findAll(); // Returns the number of entities. long count(); // Deletes the given entity. void delete(T entity); // Indicates whether an entity with the given ID exists. boolean existsById(ID primaryKey); // … more functionality omitted. }
好了,下面咱們看一下今天的主角 ElasticsearchRepository
他是怎樣的吧。
這說明什麼?
清楚了這以後,是否是應該考慮該如何使用了呢?
沒錯,接下來,開始說如何用,也寫了不少示例代碼。相對來講,仍是比較簡單,這裏就貼一下代碼就好了吧。
interface PersonRepository extends Repository<User, Long> { List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); // Enables the distinct flag for the query List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); // Enabling ignoring case for an individual property List<Person> findByLastnameIgnoreCase(String lastname); // Enabling ignoring case for all suitable properties List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); // Enabling static ORDER BY for a query List<Person> findByLastnameOrderByFirstnameAsc(String lastname); List<Person> findByLastnameOrderByFirstnameDesc(String lastname); }
是否是這樣,就能夠正常使用了呢?
固然能夠,可是若是錯了問題怎麼辦呢,官網寫了一個常見的問題,好比包掃描問題,沒有你要的方法。
interface HumanRepository { void someHumanMethod(User user); } class HumanRepositoryImpl implements HumanRepository { public void someHumanMethod(User user) { // Your custom implementation } } interface ContactRepository { void someContactMethod(User user); User anotherContactMethod(User user); } class ContactRepositoryImpl implements ContactRepository { public void someContactMethod(User user) { // Your custom implementation } public User anotherContactMethod(User user) { // Your custom implementation } }
你也能夠本身寫接口,而且去實現它。
說完理論,做爲我,應該在實際的代碼中如何運用呢?
官方也提供了不少示例代碼,咱們一塊兒來看看。
@Controller class PersonController { @Autowired PersonRepository repository; @RequestMapping(value = "/persons", method = RequestMethod.GET) HttpEntity<PagedResources<Person>> persons(Pageable pageable, PagedResourcesAssembler assembler) { Page<Person> persons = repository.findAll(pageable); return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK); } }
這段代碼相對來講仍是十分經典的,我相信不少人都看到別人的代碼,可能都會問,它爲何會這麼用呢,答案或許就在這裏吧。
固然,這是之前的代碼,或許如今用不必定合適。
終於到高潮了!
學完個人第一節,你應該已經發現了,Elasticsearch搜索是一件十分複雜的事,爲了用好它,咱們不得不學好它。一塊兒加油。
到這裏,官方文檔咱們算是過了一遍了,大體明白了,他要告訴咱們什麼。其實,文檔還有不少內容,可能你遇到的問題都能在裏面找到答案。
最後,咱們繼續看一下官網寫的一段處理得十分優秀的一段代碼吧:
SearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(matchAllQuery()) .withIndices(INDEX_NAME) .withTypes(TYPE_NAME) .withFields("message") .withPageable(PageRequest.of(0, 10)) .build(); CloseableIterator<SampleEntity> stream = elasticsearchTemplate.stream(searchQuery, SampleEntity.class); List<SampleEntity> sampleEntities = new ArrayList<>(); while (stream.hasNext()) { sampleEntities.add(stream.next()); }
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
spring: data: elasticsearch: cluster-nodes: localhost:9300 cluster-name: es-wyf
這樣就完成了整合,接下來咱們用兩種方式操做。
咱們先寫一個的實體類,藉助這個實體類呢來完成基礎的CRUD功能。
@Data @Accessors(chain = true) @Document(indexName = "blog", type = "java") public class BlogModel implements Serializable { private static final long serialVersionUID = 6320548148250372657L; @Id private String id; private String title; //@Field(type = FieldType.Date, format = DateFormat.basic_date) @DateTimeFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private Date time; }
注意id字段是必須的,能夠不寫註解@Id。
public interface BlogRepository extends ElasticsearchRepository<BlogModel, String> { }
基礎操做的代碼,都是在 BlogController
裏面寫。
@RestController @RequestMapping("/blog") public class BlogController { @Autowired private BlogRepository blogRepository; }
@PostMapping("/add") public Result add(@RequestBody BlogModel blogModel) { blogRepository.save(blogModel); return Result.success(); }
咱們添加一條數據,標題是:Elasticsearch實戰篇:Spring Boot整合ElasticSearch,時間是:2019-03-06。咱們來測試,看一下成不成功。
POST http://localhost:8080/blog/add
{ "title":"Elasticsearch實戰篇:Spring Boot整合ElasticSearch", "time":"2019-05-06" }
獲得響應:
{ "code": 0, "msg": "Success" }
嘿,成功了。那接下來,咱們一下查詢方法測試一下。
@GetMapping("/get/{id}") public Result getById(@PathVariable String id) { if (StringUtils.isEmpty(id)) return Result.error(); Optional<BlogModel> blogModelOptional = blogRepository.findById(id); if (blogModelOptional.isPresent()) { BlogModel blogModel = blogModelOptional.get(); return Result.success(blogModel); } return Result.error(); }
測試一下:
ok,沒問題。
@GetMapping("/get") public Result getAll() { Iterable<BlogModel> iterable = blogRepository.findAll(); List<BlogModel> list = new ArrayList<>(); iterable.forEach(list::add); return Result.success(list); }
測試一下:
GET http://localhost:8080/blog/get
結果:
{ "code": 0, "msg": "Success", "data": [ { "id": "fFXTTmkBTzBv3AXCweFS", "title": "Elasticsearch實戰篇:Spring Boot整合ElasticSearch", "time": "2019-05-06" } ] }
@PostMapping("/update") public Result updateById(@RequestBody BlogModel blogModel) { String id = blogModel.getId(); if (StringUtils.isEmpty(id)) return Result.error(); blogRepository.save(blogModel); return Result.success(); }
測試:
POST http://localhost:8080/blog/update
{ "id":"fFXTTmkBTzBv3AXCweFS", "title":"Elasticsearch入門篇", "time":"2019-05-01" }
響應:
{ "code": 0, "msg": "Success" }
查詢一下:
ok,成功!
@DeleteMapping("/delete/{id}") public Result deleteById(@PathVariable String id) { if (StringUtils.isEmpty(id)) return Result.error(); blogRepository.deleteById(id); return Result.success(); }
測試:
DELETE http://localhost:8080/blog/delete/fFXTTmkBTzBv3AXCweFS
響應:
{ "code": 0, "msg": "Success" }
咱們再查一下:
@DeleteMapping("/delete") public Result deleteById() { blogRepository.deleteAll(); return Result.success(); }
爲了方便測試,咱們先構造數據
搜索標題中的關鍵字
BlogRepository
List<BlogModel> findByTitleLike(String keyword);
BlogController
@GetMapping("/rep/search/title") public Result repSearchTitle(String keyword) { if (StringUtils.isEmpty(keyword)) return Result.error(); return Result.success(blogRepository.findByTitleLike(keyword)); }
咱們來測試一下。
POST http://localhost:8080/blog/rep/search/title?keyword=java
結果:
{ "code": 0, "msg": "Success", "data": [ { "id": "f1XrTmkBTzBv3AXCeeFA", "title": "java實戰", "time": "2018-03-01" }, { "id": "fVXrTmkBTzBv3AXCHuGH", "title": "java入門", "time": "2018-01-01" }, { "id": "flXrTmkBTzBv3AXCUOHj", "title": "java基礎", "time": "2018-02-01" }, { "id": "gFXrTmkBTzBv3AXCn-Eb", "title": "java web", "time": "2018-04-01" }, { "id": "gVXrTmkBTzBv3AXCzuGh", "title": "java ee", "time": "2018-04-10" } ] }
繼續搜索:
GET http://localhost:8080/blog/rep/search/title?keyword=入門
結果:
{ "code": 0, "msg": "Success", "data": [ { "id": "hFXsTmkBTzBv3AXCtOE6", "title": "Elasticsearch入門", "time": "2019-01-20" }, { "id": "fVXrTmkBTzBv3AXCHuGH", "title": "java入門", "time": "2018-01-01" }, { "id": "glXsTmkBTzBv3AXCBeH_", "title": "php入門", "time": "2018-05-10" } ] }
爲了驗證,咱們再換一個關鍵字搜索:
GET http://localhost:8080/blog/rep/search/title?keyword=java入門
{ "code": 0, "msg": "Success", "data": [ { "id": "fVXrTmkBTzBv3AXCHuGH", "title": "java入門", "time": "2018-01-01" }, { "id": "hFXsTmkBTzBv3AXCtOE6", "title": "Elasticsearch入門", "time": "2019-01-20" }, { "id": "glXsTmkBTzBv3AXCBeH_", "title": "php入門", "time": "2018-05-10" }, { "id": "gFXrTmkBTzBv3AXCn-Eb", "title": "java web", "time": "2018-04-01" }, { "id": "gVXrTmkBTzBv3AXCzuGh", "title": "java ee", "time": "2018-04-10" }, { "id": "f1XrTmkBTzBv3AXCeeFA", "title": "java實戰", "time": "2018-03-01" }, { "id": "flXrTmkBTzBv3AXCUOHj", "title": "java基礎", "time": "2018-02-01" } ] }
哈哈,有沒有以爲很眼熟。
那根據上次的經驗,咱們正好換一種方式解決這個問題。
@Query("{\"match_phrase\":{\"title\":\"?0\"}}") List<BlogModel> findByTitleCustom(String keyword);
值得一提的是,官方文檔示例代碼多是爲了好看,出現問題。
官網文檔給的錯誤示例:
官網示例代碼:
另外,?0
代指變量的意思。
@GetMapping("/rep/search/title/custom") public Result repSearchTitleCustom(String keyword) { if (StringUtils.isEmpty(keyword)) return Result.error(); return Result.success(blogRepository.findByTitleCustom(keyword)); }
測試一下:
ok,沒有問題。
@Autowired private ElasticsearchTemplate elasticsearchTemplate; @GetMapping("/search/title") public Result searchTitle(String keyword) { if (StringUtils.isEmpty(keyword)) return Result.error(); SearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(queryStringQuery(keyword)) .build(); List<BlogModel> list = elasticsearchTemplate.queryForList(searchQuery, BlogModel.class); return Result.success(list); }
測試:
POST http://localhost:8080/blog/search/title?keyword=java入門
結果:
{ "code": 0, "msg": "Success", "data": [ { "id": "fVXrTmkBTzBv3AXCHuGH", "title": "java入門", "time": "2018-01-01" }, { "id": "hFXsTmkBTzBv3AXCtOE6", "title": "Elasticsearch入門", "time": "2019-01-20" }, { "id": "glXsTmkBTzBv3AXCBeH_", "title": "php入門", "time": "2018-05-10" }, { "id": "gFXrTmkBTzBv3AXCn-Eb", "title": "java web", "time": "2018-04-01" }, { "id": "gVXrTmkBTzBv3AXCzuGh", "title": "java ee", "time": "2018-04-10" }, { "id": "f1XrTmkBTzBv3AXCeeFA", "title": "java實戰", "time": "2018-03-01" }, { "id": "flXrTmkBTzBv3AXCUOHj", "title": "java基礎", "time": "2018-02-01" } ] }
OK,暫時先到這裏,關於搜索,咱們後面會專門開一個專題,學習搜索。
咱們寫個什麼例子,想了好久,那就寫一個搜索手機的例子吧!
咱們先看下最後實現的效果吧
主頁效果:
分頁效果:
咱們搜索 「小米」:
咱們搜索 「1999」:
咱們搜索 「黑色」:
高級搜索頁面:
咱們使用高級搜索,搜索:「小米」、「1999」:
高級搜索 「小米」、「1999」 結果:
上面的而且關係生效了嗎?咱們試一下搜索 「華爲」,「1999」:
最後,咱們嘗試搜索時間段:
看一下,搜索結果吧:
說實話,這個時間搜索結果,我不是很滿意,ES 的時間問題,我打算在後面花一些時間去研究下。
基於Gradle搭建Spring Boot項目,把我折騰的受不了(若是哪位這方面有經驗,能夠給我指點指點),這個demo寫了好久,那天都跑的好好的,今早上起來,就跑步起來了,一氣之下,就改爲Maven了。
下面看一下個人依賴和配置
pom.xml 片斷
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> </repositories> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 添加 JavaLib 支持 用於接口返回 --> <dependency> <groupId>com.github.fengwenyi</groupId> <artifactId>JavaLib</artifactId> <version>1.0.7.RELEASE</version> </dependency> <!-- 添加 webflux 支持 用於編寫非阻塞接口 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- 添加 fastjson 的支持 用於處理JSON格式數據 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.56</version> </dependency> <!-- 添加 Httpclient 的支持 用於網絡請求 --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.7</version> </dependency> <!-- 添加 jsoup 的支持 用於解析網頁內容 --> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.10.2</version> </dependency> </dependencies>
application.yml
server: port: 9090 spring: data: elasticsearch: cluster-nodes: localhost:9300 cluster-name: es-wyf repositories: enabled: true
PhoneModel
@Data @Accessors(chain = true) @Document(indexName = "springboot_elasticsearch_example_phone", type = "com.fengwenyi.springbootelasticsearchexamplephone.model.PhoneModel") public class PhoneModel implements Serializable { private static final long serialVersionUID = -5087658155687251393L; /* ID */ @Id private String id; /* 名稱 */ private String name; /* 顏色,用英文分號(;)分隔 */ private String colors; /* 賣點,用英文分號(;)分隔 */ private String sellingPoints; /* 價格 */ private String price; /* 產量 */ private Long yield; /* 銷售量 */ private Long sale; /* 上市時間 */ //@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date marketTime; /* 數據抓取時間 */ //@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime; }
PhoneRepository
public interface PhoneRepository extends ElasticsearchRepository<PhoneModel, String> { }
PhoneController
@RestController @RequestMapping(value = "/phone") @CrossOrigin public class PhoneController { @Autowired private ElasticsearchTemplate elasticsearchTemplate; }
後面接口,都會在這裏寫。
個人數據是抓的 「華爲」 和 「小米」 官網
首先使用 httpclient
下載html,而後使用 jsoup
進行解析。
以 華爲 爲例:
private void huawei() throws IOException { CloseableHttpClient httpclient = HttpClients.createDefault(); // 建立httpclient實例 HttpGet httpget = new HttpGet("https://consumer.huawei.com/cn/phones/?ic_medium=hwdc&ic_source=corp_header_consumer"); // 建立httpget實例 CloseableHttpResponse response = httpclient.execute(httpget); // 執行get請求 HttpEntity entity=response.getEntity(); // 獲取返回實體 //System.out.println("網頁內容:"+ EntityUtils.toString(entity, "utf-8")); // 指定編碼打印網頁內容 String content = EntityUtils.toString(entity, "utf-8"); response.close(); // 關閉流和釋放系統資源 // System.out.println(content); Document document = Jsoup.parse(content); Elements elements = document.select("#content-v3-plp #pagehidedata .plphidedata"); for (Element element : elements) { // System.out.println(element.text()); String jsonStr = element.text(); List<HuaWeiPhoneBean> list = JSON.parseArray(jsonStr, HuaWeiPhoneBean.class); for (HuaWeiPhoneBean bean : list) { String productName = bean.getProductName(); List<ColorModeBean> colorsItemModeList = bean.getColorsItemMode(); StringBuilder colors = new StringBuilder(); for (ColorModeBean colorModeBean : colorsItemModeList) { String colorName = colorModeBean.getColorName(); colors.append(colorName).append(";"); } List<String> sellingPointList = bean.getSellingPoints(); StringBuilder sellingPoints = new StringBuilder(); for (String sellingPoint : sellingPointList) { sellingPoints.append(sellingPoint).append(";"); } // System.out.println("產品名:" + productName); // System.out.println("顏 色:" + color); // System.out.println("買 點:" + sellingPoint); // System.out.println("-----------------------------------"); PhoneModel phoneModel = new PhoneModel() .setName(productName) .setColors(colors.substring(0, colors.length() - 1)) .setSellingPoints(sellingPoints.substring(0, sellingPoints.length() - 1)) .setCreateTime(new Date()); phoneRepository.save(phoneModel); } } }
全文搜索來講,仍是相對來講,比較簡單,直接貼代碼吧:
/** * 全文搜索 * @param keyword 關鍵字 * @param page 當前頁,從0開始 * @param size 每頁大小 * @return {@link Result} 接收到的數據格式爲json */ @GetMapping("/full") public Mono<Result> full(String keyword, int page, int size) { // System.out.println(new Date() + " => " + keyword); // 校驗參數 if (StringUtils.isEmpty(page)) page = 0; // if page is null, page = 0 if (StringUtils.isEmpty(size)) size = 10; // if size is null, size default 10 // 構造分頁類 Pageable pageable = PageRequest.of(page, size); // 構造查詢 NativeSearchQueryBuilder NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder() .withPageable(pageable) ; if (!StringUtils.isEmpty(keyword)) { // keyword must not null searchQueryBuilder.withQuery(QueryBuilders.queryStringQuery(keyword)); } /* SearchQuery 這個很關鍵,這是搜索條件的入口, elasticsearchTemplate 會 使用它 進行搜索 */ SearchQuery searchQuery = searchQueryBuilder.build(); // page search Page<PhoneModel> phoneModelPage = elasticsearchTemplate.queryForPage(searchQuery, PhoneModel.class); // return return Mono.just(Result.success(phoneModelPage)); }
官網文檔也是這麼用的,因此相對來講,這仍是很簡單的,不過拆詞 和 搜索策略 搜索速度 可能在實際使用中要考慮。
先看代碼,後面咱們再來分析:
/** * 高級搜索,根據字段進行搜索 * @param name 名稱 * @param color 顏色 * @param sellingPoint 賣點 * @param price 價格 * @param start 開始時間(格式:yyyy-MM-dd HH:mm:ss) * @param end 結束時間(格式:yyyy-MM-dd HH:mm:ss) * @param page 當前頁,從0開始 * @param size 每頁大小 * @return {@link Result} */ @GetMapping("/_search") public Mono<Result> search(String name, String color, String sellingPoint, String price, String start, String end, int page, int size) { // 校驗參數 if (StringUtils.isEmpty(page) || page < 0) page = 0; // if page is null, page = 0 if (StringUtils.isEmpty(size) || size < 0) size = 10; // if size is null, size default 10 // 構造分頁對象 Pageable pageable = PageRequest.of(page, size); // BoolQueryBuilder (Elasticsearch Query) BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); if (!StringUtils.isEmpty(name)) { boolQueryBuilder.must(QueryBuilders.matchQuery("name", name)); } if (!StringUtils.isEmpty(color)) { boolQueryBuilder.must(QueryBuilders.matchQuery("colors", color)); } if (!StringUtils.isEmpty(color)) { boolQueryBuilder.must(QueryBuilders.matchQuery("sellingPoints", sellingPoint)); } if (!StringUtils.isEmpty(price)) { boolQueryBuilder.must(QueryBuilders.matchQuery("price", price)); } if (!StringUtils.isEmpty(start)) { Date startTime = null; try { startTime = DateTimeUtil.stringToDate(start, DateTimeFormat.yyyy_MM_dd_HH_mm_ss); } catch (ParseException e) { e.printStackTrace(); } boolQueryBuilder.must(QueryBuilders.rangeQuery("createTime").gt(startTime.getTime())); } if (!StringUtils.isEmpty(end)) { Date endTime = null; try { endTime = DateTimeUtil.stringToDate(end, DateTimeFormat.yyyy_MM_dd_HH_mm_ss); } catch (ParseException e) { e.printStackTrace(); } boolQueryBuilder.must(QueryBuilders.rangeQuery("createTime").lt(endTime.getTime())); } // BoolQueryBuilder (Spring Query) SearchQuery searchQuery = new NativeSearchQueryBuilder() .withPageable(pageable) .withQuery(boolQueryBuilder) .build() ; // page search Page<PhoneModel> phoneModelPage = elasticsearchTemplate.queryForPage(searchQuery, PhoneModel.class); // return return Mono.just(Result.success(phoneModelPage)); }
無論spring如何封裝,查詢方式都同樣,以下圖:
好吧,咱們懷着這樣的心態去看下源碼。
org.springframework.data.elasticsearch.core.query.SearchQuery
這個是咱們搜索須要用到對象
public NativeSearchQueryBuilder withQuery(QueryBuilder queryBuilder) { this.queryBuilder = queryBuilder; return this; }
OK,根據源碼,咱們須要構造這個 QueryBuilder,那麼問題來了,這個是個什麼東西,咱們要如何構造,繼續看:
org.elasticsearch.index.query.QueryBuilder
注意包名。
啥,怎麼又跑到 elasticsearch。
你想啊,你寫的東西,會讓別人直接操做嗎?
答案是不會的,咱們只會提供API,全部,無論Spring如何封裝,也只會經過API去調用。
好吧,今天先到這裏,下一個專題,咱們再討論關於搜索問題。
Spring Boot結合Elasticsearch,實現手機信息搜索小例子
<iframe height=498 width=510 src='http://player.youku.com/embed...' frameborder=0 'allowfullscreen'></iframe>
若是沒法播放,請點擊這裏