整合springboot_ElasticSearch实战篇 - Spring Boot 整合 ElasticSearch

本文详细介绍了如何使用SpringBoot与Elasticsearch进行整合,从通读官方文档开始,逐步讲解了SpringBoot中集成Elasticsearch的配置、基本的CRUD操作、搜索功能以及示例代码。通过实例展示了如何进行索引创建、数据插入、查询、更新和删除,还涉及到了高级搜索的实现。同时,文章以手机搜索为例,演示了全文搜索和高级搜索的实现过程。
摘要由CSDN通过智能技术生成

当前Spring Boot很是流行,包括我自己,也是在用Spring Boot集成其他框架进行项目开发,所以这一节,我们一起来探讨Spring Boot整合ElasticSearch的问题。

本文主要讲以下内容:

第一部分,通读文档

第二部分,Spring Boot整合ElasticSearch

第三部分,基本的CRUD操作

第四部分,搜索

第五部分,例子

还没有学过Elasticsearch的朋友,可以先学这个系列的第一节(这个系列共三节),如果你有不明白或者不正确的地方,可以给我评论、留言或者私信。

第一步,通读文档

Spring Data Elasticsearch 官方文档,这是当前最新的文档。

关于repository

文档一开始就介绍 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 他是怎样的吧。

32b7553d9a2605d6ca1bf49bb1c5ef71.png

这说明什么?

  • 用法和JPA一样;
  • 再这他除了有CRUD的基本功能之外,还有分页和排序。

清楚了这之后,是不是应该考虑该如何使用了呢?

如何用?

没错,接下来,开始说如何用,也写了很多示例代码。相对来说,还是比较简单,这里就贴一下代码就行了吧。

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());}

第二部分,Spring Boot整合ElasticSearch

添加依赖
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
添加配置
spring:  data:    elasticsearch:      cluster-nodes: localhost:9300      cluster-name: es-wyf

这样就完成了整合,接下来我们用两种方式操作。

Model

我们先写一个的实体类,借助这个实体类呢来完成基础的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 {}

第三部分,CRUD

基础操作的代码,都是在 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"}

嘿,成功了。那接下来,我们一下查询方法测试一下。

查询
  • 根据ID查询
@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();}

测试一下:

89f1a53adde0da412066b018861a2076.png

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"        }    ]}
根据ID修改
@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"}

查询一下:

d995585d3f9f821162fc993e98654dd6.png

ok,成功!

删除
  • 根据ID删除
@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"}

我们再查一下:

c3ab5069dbfc3a266244d4fbe5ee9159.png
  • 删除所有数据
@DeleteMapping("/delete")public Result deleteById() {    blogRepository.deleteAll();    return Result.success();}

第四部分,搜索

构造数据

为了方便测试,我们先构造数据

03c0d5872fb95be89274403a2351cb3a.png
Repository查询操作

搜索标题中的关键字

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);

值得一提的是,官方文档示例代码可能是为了好看,出现问题。

官网文档给的错误示例:

1134cd69643838f2cdcec854e3738588.png

官网示例代码:

d3886140efd434a7e9a1d2827661b2f7.png

官方示例代码

另外,?0 代指变量的意思。

@GetMapping("/rep/search/title/custom")public Result repSearchTitleCustom(String keyword) {    if (StringUtils.isEmpty(keyword))        return Result.error();    return Result.success(blogRepository.findByTitleCustom(keyword));}

测试一下:

a20dc382b2eef8e6284ae1aeb89b7d47.png

ok,没有问题。

ElasticsearchTemplate
@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,暂时先到这里,关于搜索,我们后面会专门开一个专题,学习搜索。

第五部分,例子

我们写个什么例子,想了很久,那就写一个搜索手机的例子吧!

界面截图

我们先看下最后实现的效果吧

主页效果:

56017539c2fd0006c9346f82e8758054.png

分页效果:

e5ac1cbcfc556c884051041033f06c5f.png

我们搜索 “小米”:

11c8c605a2a7c90bdcb157ddff8145e1.png

我们搜索 “1999”:

2fe1e691b4dca865bb480559c0d95e3b.png

我们搜索 “黑色”:

f9d653853a84ac3083c6cd8fa42b1432.png

高级搜索页面:

c6c62f8b54f997fa4286bcfbfb3fb3a9.png

我们使用高级搜索,搜索:“小米”、“1999”:

00320c2c2e0cac3e53d0af5c3da68b62.png

高级搜索 “小米”、“1999” 结果:

1bc04ae58b1c90f0d9b0270b76af54d7.png

上面的并且关系生效了吗?我们试一下搜索 “华为”,“1999”:

3a9b4d99d15658d2b45a68cdff4262d5.png

最后,我们尝试搜索时间段:

d49f02d5be367fe482f52caec633b640.png

看一下,搜索结果吧:

f1611e8bb05906443a895dba2a3bed3b.png

说实话,这个时间搜索结果,我不是很满意,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如何封装,查询方式都一样,如下图:

b67332bab1df0ffd03a84c51e969d1ba.png

好吧,我们怀着这样的心态去看下源码。

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去调用。

a8f4b89016da39be2908d35223e21251.png

好吧,今天先到这里。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值