结合spring boot和mysql快速实现elasticsearch7简单功能

结合spring boot和mysql快速实现elasticsearch功能

本文基于spring boot 2.2.x版本,mysql 8.x 版本,spring data jpa 以及elasticsearch7.x 版本

目的是介绍以下三个部分

  1. elasticsearch如何与spring boot结合
  2. elasticsearch的java high level rest api的CRUD操作
  3. mysql如何与elasticsearch结合

通过以下流程可以迅速实现web的搜索功能

在此之前,你需要对elasticsearch有基础的了解,比如运行配置,比如Index和document的概念,mapping的含义等

所有代码在这里github地址

准备工作

1. pom配置

<dependencies>
    
    <dependency>
        <groupId>org.elasticsearch.clientgroupId>
        <artifactId>elasticsearch-rest-high-level-clientartifactId>
        <version>7.5.1version>
    dependency>
    <dependency>
        <groupId>org.elasticsearch.clientgroupId>
        <artifactId>elasticsearch-rest-clientartifactId>
        <version>7.5.1version>
    dependency>
    
    <dependency>
        <groupId>org.elasticsearchgroupId>
        <artifactId>elasticsearchartifactId>
        <version>7.5.1version>
    dependency>
    
    
    <dependency>
        <groupId>com.fasterxml.jackson.coregroupId>
        <artifactId>jackson-databindartifactId>
        <version>2.10.3version>
    dependency>
    
    <dependency>
        <groupId>org.modelmapper.extensionsgroupId>
        <artifactId>modelmapper-jacksonartifactId>
        <version>2.3.6version>
    dependency>

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-jpaartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-thymeleafartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-devtoolsartifactId>
        <scope>runtimescope>
        <optional>trueoptional>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintagegroupId>
                <artifactId>junit-vintage-engineartifactId>
            exclusion>
        exclusions>
    dependency>
dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-maven-pluginartifactId>
        plugin>
    plugins>
build>


2. elasticsearch的配置

直接下载使用默认配置即可,关于集群配置部分,不是本文的目的,运行localhost:9200查看是否返回一个json

由于elasticsearch没有springboot 的starter包,而社区实现的spring data elasticsearch还停留在6.8版本,不能紧跟最新版本,因此使用spring的配置方式。

创建config,使用java注解方式进行配置

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfig {
    @Bean
    public RestHighLevelClient restHighLevelClient() {
        return new RestHighLevelClient(RestClient.builder(
                new HttpHost("localhost", 9200, "http"),
                new HttpHost("localhost", 9300, "http")
        ));
    }
}

elastic由于历史问题,虽然现在推荐high level rest api,但其仍然基于low level rest api构建,因此创建客户端需要new low client,配置方式如上

3. mysql 8.x 和 jpa 的配置

网上都是使用5.7版本,我们要紧跟风潮,这里简单介绍8.x版本的配置方式

先看application.properties的内容

server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/yaologos?serverTimezone=GMT%2B8&
spring.datasource.username=yao
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.thymeleaf.cache=false
spring.jpa.show-sql=true
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=validate

spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

这里对mysql进行了配置,直接按照这个模式复制即可

然后是jpa的配置,同样使用注解方式配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
// 下面是扫描repositories包
@EnableJpaRepositories(basePackages = "com.yaologos.searchhouse.repositories")
public class JPAConfig {
    // 创建实体类管理
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        HibernateJpaVendorAdapter japVendor = new HibernateJpaVendorAdapter();
        japVendor.setGenerateDdl(false);

        LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactory.setDataSource(dataSource);
        entityManagerFactory.setJpaVendorAdapter(japVendor);
        // 扫描entity包
        entityManagerFactory.setPackagesToScan("com.yaologos.searchhouse.entity");
        return entityManagerFactory;
    }

    // 创建事务管理器
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }
}

至此spring data jpa的配置就完成了

4. 总结

配置主要集中在config包和application.properties中,其他均使用默认的配置方式

第一步、创建数据库表和elastic索引

从工作流程来看,Elasticsearch就是根据数据库表创建一个对应的索引,然后进行CRUD操作,因此先从准备工作开始

创建数据库表

以一个数据库表为例

表名 类型 含义
id int 唯一id
username varchar 用户名
password varchar 密码
phone_number int 手机号
create_time date 创建时间
description varchar 用户描述

如何使用java对数据库中的数据进行操作呢?我们会创建一个实体类(entity)来对应每个表,这样对每个java对象进行操作,然后将值传到数据库中,就实现了java对数据的操作。

数据库对应的java对象为

import javax.persistence.*;
import java.util.Date;

@Entity // jpa注解
@Table(name = "user") // 对应的表名
public class User {

    @Id // 表明主键
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String password;
    private String description;
    // java对象的字段名可能与mysql表不同,因此进行解释
    @Column(name = "create_time")
    private Date createDate;
    
    @Column(name = "phone_number")
    private String phoneNumber;
}

创建elastic索引

我们需要将表转换为elasticsearch的index索引,索引结构如下

注意:这里我们发现数据库中user表的password字段在这并未出现,因为我们查找user用户,并不会根据密码进行查询

PUT /user
{
    "mappings": {
    "_doc": {
      "properties": {
          "id": {
            "type": "long"
          },
          "username": {
            "type": "keyword"
          },
          "createTime": {
            "type": "date",
            "format": "strict_date_optional_time||epoch_millis"
          }
          "description": {
            "type": "text",
            "index": "analyzed"
          },
          "phoneNumber": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

注意keyword就是搜索关键词,不可以进行分词,

date下面的format表示默认匹配,一般new Date()或mysql的Date的格式可以自动转化为elastic的时间格式

text类型表示可以被分词,也可以指定分词器

那么elastic如何与java交互呢?

elastic仅支持json格式的数据传输。

这样我们可以创建一个与mapping对应的java对象UserSearchIndex,然后将其转为json,传输到elasticsearch中

public class UserSearchIndex{
    // 与mapping对应的属性名
    private Long id;
    private String name;
    private String description;
    private String phoneNumber;
    private Date createDate;
}

这样只需要实例化类,然后转为json,就可以导入elasticsearch了

如何将对象转为json有很多方案,比如jackson,fastjson等,本文使用的是jackson

数据库如何与elasticsearch进行交互

我们实现了java与数据库表的数据交互,也实现了java与elasticsearch的数据交互,那么只需要将二者的java对象进行转化,就可以在java中实现了mysql与elasticsearch的交互

mysql对应的java类是User,elasticsearch对应的类是UserSearchIndex

这两个类几乎一样,除了password字段,直接使用modelmapper包进行转换即可,关于java对象的转换这里不再叙述,也可以每次转换都自己手写。

第二步、CRUD操作以及查询操作

这里我们创建一个接口对应elasticsearch的各种操作

public interface SearchService {
    boolean index(String username);
    boolean remove(String username);
	// 这里注意,查询返回的数据往往是多条,因此返回的是一个列表
    List<String> query(String keyword);
}

这里实现三个方法

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yaologos.searchhouse.entity.User;
import com.yaologos.searchhouse.entity.UserSearch;
import com.yaologos.searchhouse.service.SearchService;
import com.yaologos.searchhouse.service.UserService;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;

import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.modelmapper.ModelMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;


@Service
public class SearchServiceImpl implements SearchService {
    private static final Logger logger = LoggerFactory.getLogger(SearchServiceImpl.class);

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private UserService userService;

    @Autowired
    private ModelMapper modelMapper;

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean index(String username) {
        // 从数据库读取用户信息,然后填充至UserSearch类中,转为json,提交给elastic
        User user = userService.findUserByName(username);

        if (user == null){
            logger.error("Not Found User类");
            return false;
        }

        UserSearch userSearch = modelMapper.map(user, UserSearch.class);
        try {
            IndexRequest request = new IndexRequest("user").id(String.valueOf(user.getId()))
                    .source(objectMapper.writeValueAsBytes(userSearch), XContentType.JSON);
            IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
            if (response.status().getStatus() != 201){
                logger.error("索引未创建成功");
                return false;
            }
        } catch (Exception e){
            e.printStackTrace();
        }
        return true;

    }

    @Override
    public boolean remove(String username) {
        User user = userService.findUserByName(username);
        if (user == null){
            logger.error("未查找到信息");
            return false;
        }

        DeleteRequest request = new DeleteRequest("user",String.valueOf(user.getId()));
        try {
            DeleteResponse response = restHighLevelClient.delete(request,RequestOptions.DEFAULT);
            if (response.status().getStatus() == 200){
                logger.info("删除成功");
            } else {
                logger.info("删除失败");
                return false;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }

    @Override
    public List<String> query(String keyword){
        // 创建查询请求
        SearchRequest request = new SearchRequest("user");
        // 构建查询参数,比如查询数量,查询耗费时间上限等
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.timeout(new TimeValue(20, TimeUnit.SECONDS));
        // 排序,根据id字段排序
        searchSourceBuilder.sort(new FieldSortBuilder("id").order(SortOrder.DESC));
        // 查询类型,这里使用查询所有document,使用query进行提交
        MatchQueryBuilder queryBuilder = new MatchQueryBuilder("description",keyword);
        searchSourceBuilder.query(queryBuilder);
        // 将查询参数注入查询请求
        request.source(searchSourceBuilder);
        List<String> results = new ArrayList<>();
        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().getStatus() != 200){
                logger.error("查询失败!");
                return results;
            } else {
                logger.info("查询到数量: " + response.getHits().getTotalHits().value);

                for (SearchHit searchHit:response.getHits()){
                    String sourceAsString = searchHit.getSourceAsString();
                    results.add(sourceAsString);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return results;
    }
}

第三步、controller以及前端页面

前端页面

搜索页面比较简单

主页searchIndex.html


<html lang="en" xmlns:th="http://www.thymeleaf.org">>
<head>
    <meta charset="UTF-8">
    <title>搜索主页title>
head>
<body>
    <div class="search name">
        <form action="/search" method="post">
            <input type="text" name="keyword">
            <input type="submit" name="搜索一下">
        form>
    div>
body>

html>

结果页 result.html


<html lang="en" xmlns:th="http://www.thymeleaf.org">>
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
    <ul>
        <tr th:each="result:${results}">
            <li>
                <span th:text="${result}">span>????
            li>
        tr>
    ul>
body>
html>

Controller控制器

import com.yaologos.searchhouse.service.SearchService;
import com.yaologos.searchhouse.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@Controller
public class HelloController {

    @Autowired
    private UserService userService;

    @Autowired
    private SearchService searchService;

    @GetMapping("/searchIndex")
    public String searchIndex(){
        return "searchIndex";
    }

    /**
     * 使用web方式创建elastic文档,这里使用最简单的方式
     *
     * @param username 前端传输的用户名
     * @return 是否创建成功
     */
    @ResponseBody
    @PostMapping("/createDoc")
    public boolean translate(@RequestParam(required = false, name = "username") String username){
        return searchService.index(username);
    }

    @ResponseBody
    @PostMapping("/deleteDoc")
    public boolean delete(@RequestParam("name") String username){
        return searchService.remove(username);
    }

    @PostMapping("/search")
    public String search(@RequestParam("keyword") String keyword, Model model) {
        List<String> results = searchService.query(keyword);
        model.addAttribute("results", results);
        return "resultPage";
    }
}

你可能感兴趣的:(elasticsearch)