Elasticsearch实战篇——Spring Boot整合E
当前Spring Boot很是流行,包括我自己,也是在用Spring Boot集成其他框架进行项目开发,所以这一节,我们一起来探讨Spring Boot整合ElasticSearch的问题。
步骤如下:
第一步,通读文档。
第二步,用三种方式实现基本CRUD操作。
第三步,分析实战中可能用到的查询。
第四步,搜索专题。
还没有学过Elasticsearch的朋友,可以先学这个系列的第一节(这个系列共三节),如果你有不明白或者不正确的地方,可以给我评论、留言或者私信。
第一步,通读文档
Spring Data Elasticsearch 官方文档,这是当前最新的文档。
关于repository
文档一开始就介绍 CrudRepository
,比如,继承 Repository
,其他比如 JpaRepository
、MongoRepository
是继承CrudRepository
。也对其中的方法做了简单说明,我们一起来看一下:
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
他是怎样的吧。
这说明什么?
-
用法和JPA一样;
-
再这他除了有CRUD的基本功能之外,还有分页和排序。
清楚了这之后,是不是应该考虑该如何使用了呢?
如何用?
没错,接下来,开始说如何用,也写了很多示例代码。相对来说,还是比较简单,这里就贴一下代码就行了吧。
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());
}
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。
Repository
BlogRepository
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"
}
嘿,成功了。那接下来,我们一下查询方法测试一下。
查询
- 根据ID查询
@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();
}
测试一下:
测试根据ID查询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"
}
]
}
根据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"
}
查询一下:
修改数据成功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"
}
我们再查一下:
删除数据成功- 删除所有数据
@DeleteMapping("/delete")
public Result deleteById() {
blogRepository.deleteAll();
return Result.success();
}
构造数据
为了方便测试,我们先构造数据
构造查询数据Repository查询操作
搜索标题中的关键字
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,没有问题。
ElasticsearchTemplate
@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,暂时先到这里,关于搜索,我们后面会专门开一个专题,学习搜索。
Jest
搜索中,发现Jest也可以操纵Elasticsearch,目前官方有star数1500+。网址:https://github.com/spring-projects/spring-data-elasticsearch。
另外,看到有人提供了Spring Boot整合的代码,如下:
@Service
public class JestClientService implements Serializable {
private static final long serialVersionUID = 1L;
JestClient client=null;
@Value(“${jest.elasticsearch.host}”)
String host;
@Value(“${jest.elasticsearch.port}”)
String port;
@Value(“${jest.elasticsearch.index}”)
String indexName;
/**
*
*/
public JestClient getClient() {
if (this.client==null){
GsonFireBuilder fireBuilder = new GsonFireBuilder();
fireBuilder.enableExposeMethodResult();
GsonBuilder builder = fireBuilder.createGsonBuilder();
builder.excludeFieldsWithoutExposeAnnotation();
final Gson gson = builder.setDateFormat(AbstractJestClient.ELASTIC_SEARCH_DATE_FORMAT).create();
System.out.println(“Establishing JEST Connection to Elasticsearch over HTTP: “+”http://”+this.host+”:”+this.port);
JestClientFactory factory = new JestClientFactory();
factory.setHttpClientConfig(new HttpClientConfig
.Builder(“http://”+this.host+”:”+this.port)
.multiThreaded(true)
.readTimeout(20000)
.gson(gson)
.build());
this.client = factory.getObject();
}
return this.client;
}
}