环境:
SpringBoot 2.1.4.RELEASE
Elasticsearch 6.6.2
spring-boot-starter-data-elasticsearch
pom引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
application配置
# Elasticsearch配置
spring.data.elasticsearch.cluster-name=elasticsearch-cluster
# 单节点
spring.data.elasticsearch.cluster-nodes=192.168.183.220:9300
# 多节点
#spring.data.elasticsearch.cluster-nodes=192.168.183.220:9300,192.168.183.220:9301,192.168.183.220:9302
# 引用spring-boot-starter-actuator的话
# 关闭对es健康检查,不然启动报错
management.health.elasticsearch.enabled=false
# 或者设置rest访问地址
# spring.elasticsearch.rest.uris=http://192.168.183.220:9200
索引配置Bean
package com.lyf.domain.elastic;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Data
@NoArgsConstructor
@Document(indexName = "goods",type = "_doc", shards = 5, replicas = 0)
public class GoodsDoc {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title; //标题
@Field(type = FieldType.Keyword)
private String category;// 分类
@Field(type = FieldType.Keyword)
private String brand; // 品牌
@Field(type = FieldType.Double)
private Double price; // 价格
@Field(index = false, type = FieldType.Keyword)
private String images; // 图片地址
public GoodsDoc(Long id, String title, String category, String brand, Double price, String images) {
this.id = id;
this.title = title;
this.category = category;
this.brand = brand;
this.price = price;
this.images = images;
}
}
shards分片
和replicas副本数
自行控制
ElasticsearchTemplate使用
可以便捷:
- 创建索引index
- 创建映射mapping
- 删除索引index
- 设置别名alias
- 等等..
项目在启动时,会自动创建Bean里面的定义的索引,基本使用如下:
package com.lyf.service;
@Service
public class ElasticService {
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
public void init(){
elasticsearchTemplate.createIndex(GoodsDoc.class);
elasticsearchTemplate.putMapping(GoodsDoc.class);
}
public void delIndex(){
elasticsearchTemplate.deleteIndex(GoodsDoc.class);
}
public void alias(String index, String alias){
AliasQuery aliasQuery = new AliasQuery();
aliasQuery.setIndexName(index);
aliasQuery.setAliasName(alias);
elasticsearchTemplate.addAlias(aliasQuery);
}
}
持久层操作
对es做增删改查
定义dao, 继承ElasticsearchRepository
方便语义化操作
package com.lyf.dao;
import com.lyf.domain.elastic.GoodsDoc;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
@Mapper
public interface ElasticRepository extends ElasticsearchRepository<GoodsDoc, Long> {
}
定义service操作
package com.lyf.service;
@Service
public class ElasticService {
@Autowired
private ElasticRepository elasticRepository;
public void add(GoodsDoc goodsDoc){
elasticRepository.save(goodsDoc);
}
public void addBatch(List<GoodsDoc> goodsDocList){
elasticRepository.saveAll(goodsDocList);
}
public void del(Long id){
elasticRepository.deleteById(id);
}
public GoodsDoc find(Long id){
return elasticRepository.findById(id).get();
}
public List<GoodsDoc> all(String name, int sort){
Direction direction = sort == 0 ? Sort.Direction.ASC : Sort.Direction.DESC;
Iterable<GoodsDoc> all = elasticRepository.findAll(Sort.by(direction,name));
return copyIterator(all);
}
// 高级查询
// 分页+排序
public Map pageQuery(String name, String value, Integer page, Integer size, int sort){
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本的分词查询
queryBuilder.withQuery(QueryBuilders.termQuery(name, value));
// 排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(sort==0 ? SortOrder.ASC : SortOrder.DESC));
// 设置分页参数 es默认分页从0开始
queryBuilder.withPageable(PageRequest.of(page-1, size));
// 执行搜索,获取结果
Page<GoodsDoc> search = elasticRepository.search(queryBuilder.build());
Map result = new HashMap();
result.put("total", search.getTotalElements());
result.put("pages", search.getTotalPages());
result.put("list", copyIterator(search.getContent()));
return result;
}
}
copyIterator
方法,把Iterable
对象变成List
public static <T> List<T> copyIterator(Iterable<T> iter) {
List<T> copy = new ArrayList<T>();
iter.forEach(item->copy.add(item));
return copy;
}
语义化方法
Spring Data
的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByTitle
,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。
当然,方法名称要符合一定的约定:
Keyword | Sample | Elasticsearch Query String |
---|---|---|
And |
findByNameAndPrice |
{"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Or |
findByNameOrPrice |
{"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Is |
findByName |
{"bool" : {"must" : {"field" : {"name" : "?"}}}} |
Not |
findByNameNot |
{"bool" : {"must_not" : {"field" : {"name" : "?"}}}} |
Between |
findByPriceBetween |
{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
LessThanEqual |
findByPriceLessThan |
{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
GreaterThanEqual |
findByPriceGreaterThan |
{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Before |
findByPriceBefore |
{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
After |
findByPriceAfter |
{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Like |
findByNameLike |
{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}} |
StartingWith |
findByNameStartingWith |
{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}} |
EndingWith |
findByNameEndingWith |
{"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}} |
Contains/Containing |
findByNameContaining |
{"bool" : {"must" : {"field" : {"name" : {"query" : "**?**","analyze_wildcard" : true}}}}} |
In |
findByNameIn(Collection<String>names) |
{"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}} |
NotIn |
findByNameNotIn(Collection<String>names) |
{"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}} |
Near |
findByStoreNear |
Not Supported Yet ! |
True |
findByAvailableTrue |
{"bool" : {"must" : {"field" : {"available" : true}}}} |
False |
findByAvailableFalse |
{"bool" : {"must" : {"field" : {"available" : false}}}} |
OrderBy |
findByAvailableTrueOrderByNameDesc |
{"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}} |
dao新增方法
/**
* 根据价格区间查询
* @param price1
* @param price2
* @return
*/
List<Item> findByPriceBetween(double price1, double price2);
service使用
public List<GoodsDoc> queryByPriceBetween(Double price1, Double price2){
return elasticRepository.findByPriceBetween(price1, price2);
}
定义controller
package com.lyf.controller;
import com.lyf.domain.elastic.GoodsDoc;
import com.lyf.util.Result;
import com.lyf.model.ResultInfo;
import com.lyf.service.ElasticService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("es")
public class ElasticController {
@Autowired
private ElasticService elasticService;
@RequestMapping("init")
public ResultInfo init(Integer id){
elasticService.init();
return Result.success();
}
@RequestMapping("rm")
public ResultInfo delIndex(){
elasticService.delIndex();
return Result.success();
}
@RequestMapping("alias")
public ResultInfo alias(String index, String alias){
elasticService.alias(index, alias);
return Result.success();
}
@RequestMapping("add")
public ResultInfo add(GoodsDoc goodsDoc){
elasticService.add(goodsDoc);
return Result.success();
}
@RequestMapping("addBatch")
public ResultInfo addBatch(@RequestBody List<GoodsDoc> goodsDocList){
elasticService.addBatch(goodsDocList);
return Result.success();
}
@RequestMapping("del")
public ResultInfo del(Long id){
elasticService.del(id);
return Result.success();
}
@RequestMapping("find")
public ResultInfo find(Long id){
return Result.success( elasticService.find(id));
}
@RequestMapping("all")
public ResultInfo all(String name, @RequestParam(defaultValue = "0") int sort){
return Result.success( elasticService.all(name, sort));
}
@RequestMapping("queryByPriceBetween")
public ResultInfo queryByPriceBetween(Double price1, Double price2){
return Result.success( elasticService.queryByPriceBetween(price1, price2));
}
@RequestMapping("page")
public ResultInfo page(String name, String key,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(defaultValue = "0") int sort){
return Result.success( elasticService.pageQuery(name, key, page, size, sort));
}
}
ResultInfo.java
package com.lyf.model;
import com.lyf.base.Constant.CodeEnum;
import java.io.Serializable;
public class ResultInfo implements Serializable {
private static final long serialVersionUID = -6660878670189339288L;
private Integer code = CodeEnum.SUCCESS.getCode();
private String msg = CodeEnum.SUCCESS.getMsg();
private Object result; // 返回结果
public ResultInfo() {
}
public ResultInfo(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public ResultInfo(String msg) {
this.msg = msg;
}
public ResultInfo(Integer code) {
this.code = code;
}
public ResultInfo(Integer code, String msg, Object result) {
this.code = code;
this.msg = msg;
this.result = result;
}
public ResultInfo(Object result) {
this.result = result;
}
public ResultInfo(String msg, Object result) {
this.msg = msg;
this.result = result;
}
public ResultInfo(Integer code, Object result) {
this.code = code;
this.result = result;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
}
Result.java
工具类
package com.lyf.util;
import com.lyf.model.ResultInfo;
public class Result {
public static ResultInfo success(Integer code, String msg, Object result){
ResultInfo resultInfo = new ResultInfo();
resultInfo.setCode(code);
resultInfo.setMsg(msg);
resultInfo.setResult(result);
return resultInfo;
}
public static ResultInfo success(String msg, Object result){
ResultInfo resultInfo = new ResultInfo();
resultInfo.setMsg(msg);
resultInfo.setResult(result);
return resultInfo;
}
public static ResultInfo success(Object result){
ResultInfo resultInfo = new ResultInfo();
resultInfo.setResult(result);
return resultInfo;
}
public static ResultInfo success(Integer code, String msg){
ResultInfo resultInfo = new ResultInfo();
resultInfo.setCode(code);
resultInfo.setMsg(msg);
return resultInfo;
}
public static ResultInfo success(String msg){
ResultInfo resultInfo = new ResultInfo();
resultInfo.setMsg(msg);
return resultInfo;
}
public static ResultInfo success(){
ResultInfo resultInfo = new ResultInfo();
return resultInfo;
}
public static ResultInfo error(Integer code, String msg){
ResultInfo resultInfo = new ResultInfo();
resultInfo.setCode(code);
resultInfo.setMsg(msg);
return resultInfo;
}
}
测试请求如下
- 初始化索引
GET http://localhost:8765/es/init
- 添加数据
GET http://localhost:8765/es/add?id=1&title=小米手机7&category=手机&brand=小米&price=3499.00&images=http://image.leyou.com/13123.jpg
GET http://localhost:8765/es/add?id=2&title=小米手机8&category=手机&brand=小米&price=3299.00&images=http://image.leyou.com/13124.jpg
GET http://localhost:8765/es/add?id=3&title=华为手机8&category=手机&brand=华为&price=4299.00&images=http://image.leyou.com/321.jpg
- 查询数据
GET http://localhost:8765/es/find?id=1
- 按价格倒序返回所有数据
GET http://localhost:8765/es/all?name=price&sort=1
- 返回指定价格区间数据
GET http://localhost:8765/es/queryByPriceBetween?price1=3000&price2=4000
- 返回分页数据
GET http://localhost:8765/es/page?name=title&key=小米&sort=1
- 删除索引
GET http://localhost:8765/es/rm
- 批量添加数据
POST http://localhost:8765/es/addBatch
Content-Type: application/json
[
{
"id": 1,
"title": "小米手机7",
"category": "手机",
"brand": "小米",
"price": 3499.0,
"images": "http://image.leyou.com/13123.jpg"
},
{
"id": 2,
"title": "小米手机8",
"category": "手机",
"brand": "小米",
"price": 3299.0,
"images": "http://image.leyou.com/13124.jpg"
},
{
"id": 3,
"title": "华为手机8",
"category": "手机",
"brand": "华为",
"price": 4299.0,
"images": "http://image.leyou.com/321.jpg"
}
]
- 索引起别名
GET http://localhost:8765/es/alias?index=goods&alias=abc
搭建过程和集成springboot中遇到的问题和bug,看下一篇博文 bug收集
minimum_should_match 最少值匹配
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html
参考: