当前Spring Boot很是流行,包括我自己,也是在用Spring Boot集成其他框架进行项目开发,所以这一节,我们一起来探讨Spring Boot整合ElasticSearch的问题。
本文主要讲以下内容:
第一部分,通读文档
第二部分,Spring Boot整合ElasticSearch
第三部分,基本的CRUD操作
第四部分,搜索
第五部分,例子
还没有学过Elasticsearch的朋友,可以先学这个系列的第一节(这个系列共三节),如果你有不明白或者不正确的地方,可以给我评论、留言或者私信。
Spring Data Elasticsearch 官方文档,这是当前最新的文档。
文档一开始就介绍 CrudRepository ,比如,继承 Repository,其他比如JpaRepository、MongoRepository是继承CrudRepository。也对其中的方法做了简单说明,我们一起来看一下:
public interface CrudRepository extends Repository {// Saves the given entity. S save(S entity);// Returns the entity identified by the given ID. Optional findById(ID primaryKey);// Returns all entities. Iterable 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 { List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); // Enables the distinct flag for the query List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); List findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); // Enabling ignoring case for an individual property List findByLastnameIgnoreCase(String lastname); // Enabling ignoring case for all suitable properties List findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); // Enabling static ORDER BY for a query List findByLastnameOrderByFirstnameAsc(String lastname); List 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 }}
你也可以自己写接口,并且去实现它。
说完理论,作为我,应该在实际的代码中如何运用呢?
官方也提供了很多示例代码,我们一起来看看。
@Controllerclass PersonController { @Autowired PersonRepository repository; @RequestMapping(value = "/persons", method = RequestMethod.GET) HttpEntity> persons(Pageable pageable, PagedResourcesAssembler assembler) { Page 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 stream = elasticsearchTemplate.stream(searchQuery, SampleEntity.class);List 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 {}
基础操作的代码,都是在 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();}
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 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 iterable = blogRepository.findAll(); List 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();}
为了方便测试,我们先构造数据
搜索标题中的关键字
BlogRepositor
List 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 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,没有问题。
@Autowiredprivate 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 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 片段
org.springframework.boot spring-boot-starter-parent 2.1.3.RELEASE jitpack.io https://jitpack.io org.springframework.boot spring-boot-starter-data-elasticsearch org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test com.github.fengwenyi JavaLib 1.0.7.RELEASE org.springframework.boot spring-boot-starter-webflux com.alibaba fastjson 1.2.56 org.apache.httpcomponents httpclient 4.5.7 org.jsoup jsoup 1.10.2
application.yml
server: port: 9090spring: 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 {}
PhoneController
@RestController@RequestMapping(value = "/phone")@CrossOriginpublic 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 list = JSON.parseArray(jsonStr, HuaWeiPhoneBean.class); for (HuaWeiPhoneBean bean : list) { String productName = bean.getProductName(); List colorsItemModeList = bean.getColorsItemMode(); StringBuilder colors = new StringBuilder(); for (ColorModeBean colorModeBean : colorsItemModeList) { String colorName = colorModeBean.getColorName(); colors.append(colorName).append(";"); } List 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 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 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 search(String name, String color, String sellingPoint, String price, String start, String end, int page, int size) { // 校验参数 if (StringUtils.isEmpty(page) || page 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去调用。
好吧,今天先到这里。