本文基于spring boot 2.2.x版本,mysql 8.x 版本,spring data jpa 以及elasticsearch7.x 版本
目的是介绍以下三个部分
通过以下流程可以迅速实现web的搜索功能
在此之前,你需要对elasticsearch有基础的了解,比如运行配置,比如Index和document的概念,mapping的含义等
所有代码在这里github地址
<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>
直接下载使用默认配置即可,关于集群配置部分,不是本文的目的,运行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,配置方式如上
网上都是使用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的配置就完成了
配置主要集中在config包和application.properties中,其他均使用默认的配置方式
从工作流程来看,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;
}
我们需要将表转换为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
我们实现了java与数据库表的数据交互,也实现了java与elasticsearch的数据交互,那么只需要将二者的java对象进行转化,就可以在java中实现了mysql与elasticsearch的交互
mysql对应的java类是User,elasticsearch对应的类是UserSearchIndex
这两个类几乎一样,除了password字段,直接使用modelmapper包进行转换即可,关于java对象的转换这里不再叙述,也可以每次转换都自己手写。
这里我们创建一个接口对应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;
}
}
搜索页面比较简单
主页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>
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";
}
}