使用springboot操作es
- 写在前面
- 搭建项目环境和选择合适版本
- 具体的代码实现(1)继承ProductInfoRepository
- 具体的代码实现(2)使用ElasticsearchRestTemplate操作
- 问题总结
- 最后放个demo
写在前面
对于elasticsearch的搭建,前面写了一篇文章有简单描述如何搭建es,本次主要介绍如何在项目里使用,主要使用ElasticsearchRepository和ElasticsearchRestTemplate操作es。
搭建项目环境和选择合适版本
首先选择合适的项目组件版本,因为es版本和springboot版本有对应,如果不合适会报错。
这里以es7.6.x版本、springboot2.7.12,java8、maven3.8写出来的demo。
首先引入maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
主要还是第一个dependency配置,就是操作es的。
具体的代码实现(1)继承ProductInfoRepository
1.config类,主要用于配置扫描操作es所用的repository
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.es.demo.repository")
public class ElasticsearchConfig {
}
2.对应的实体类ProductInfo
@Data
@Document(indexName = "product-info")
public class ProductInfo implements Serializable {
@Id
private Integer id;
@Field(type = FieldType.Text, searchAnalyzer = "ik_max_word", analyzer = "ik_smart")
private String productName;
@Field(type = FieldType.Text, searchAnalyzer = "ik_max_word", analyzer = "ik_smart")
private String description;
@Field(type = FieldType.Keyword)
private Date createTime;
@Field(type = FieldType.Keyword)
private BigDecimal price;
@Field(type = FieldType.Integer)
private Integer num;
}
3.创建repository包,这里防止所有的es对应的索引操作类。所有类继承org.springframework.data.elasticsearch.repository.ElasticsearchRepository,这里以ProductInfo为例:
@Component
public interface ProductInfoRepository extends ElasticsearchRepository<ProductInfo, Integer> {
}
第一个泛型是操作的索引类,第二个是唯一标识(id)对应的类型,
在这个类里按照一定的规则可以直接写一些方法名,不用写实现,ElasticsearchRepository会自动帮我们实现。具体我没有写,你们可以去网上搜一下怎么写。
4.因为ProductInfoRepository中实现了基本的增删改查,所以在这里可以直接使用
如何使用:
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductInfoRepository productInfoRepository;
@Override
public Boolean save(ProductInfo... productInfo) {
productInfoRepository.saveAll(Arrays.asList(productInfo));
return true;
}
@Override
public Boolean delete(Integer id) {
productInfoRepository.deleteById(id);
return null;
}
@Override
public ProductInfo getById(Integer id) {
Optional<ProductInfo> byId = productInfoRepository.findById(id);
return byId.orElse(null);
}
@Override
public List<ProductInfo> getAll() {
List<ProductInfo> list = new ArrayList<>();
productInfoRepository.findAll().forEach(list::add);
return list;
}
具体的代码实现(2)使用ElasticsearchRestTemplate操作
1.对于一些复杂的查询我是直接使用ElasticsearchRestTemplate这个类来操作的。写了一个公共类,类似mybatisplus的service抽象,具体service接口和实现类可以直接继承。也是看别的开源项目这么写自己总结出来的。
首先是接口层BasicEsService:
public interface BasicEsService<T> {
/**
* 保存数据
*
* @param indexEnum 索引
* @param ts 数据
*/
void save(IndexEnum indexEnum, T... ts);
/**
* 删除数据
*
* @param indexEnum 索引
* @param id id
*/
void delete(IndexEnum indexEnum, String id);
/**
* 查询单个数据
*
* @param indexEnum
* @param id
* @param clazz
* @return
*/
T getById(IndexEnum indexEnum, String id, T clazz);
/**
* 查询所有数据,es限制最多返回10000条
*
* @param indexEnum
* @param clazz
* @return
*/
List<T> getAllData(IndexEnum indexEnum, T clazz);
/**
* 动态sql查询
*
* @param sql
* @param clazz
* @return
*/
List<Map<String, Object>> query(String sql, T clazz);
/**
* 范围查询
*
* @param pageNo
* @param pageSize
* @param indexEnum
* @param order
* @param rangeParam
* @return
*/
Page<T> rangeQuery(Integer pageNo, Integer pageSize, IndexEnum indexEnum, Map<String, String> order, Map<String, Object[]> rangeParam, T clazz);
/**
* 分页查询,默认十条,支持多字段模糊匹配,多字段排序
*
* @param pageNo 从0开始
* @param pageSize 每页记录数
* @param keyword 关键词
* @param clazz 转换的对象
* @param order 排序集合
* @param fields 搜索的列
* @return
*/
Page<T> pageList(Integer pageNo, Integer pageSize, String keyword, T clazz, Map<String, String> order, String... fields);
然后是实现类BasicEsServiceImpl
@Service
public class BasicEsServiceImpl<T> implements BasicEsService<T> {
@Autowired
protected ElasticsearchRestTemplate elasticsearchRestTemplate;
@Override
public void save(IndexEnum indexEnum, T... ts) {
T[] save = elasticsearchRestTemplate.save(ts, IndexCoordinates.of(indexEnum.getIndex()));
return;
}
@Override
public void delete(IndexEnum indexEnum, String id) {
String delete = elasticsearchRestTemplate.delete(id, IndexCoordinates.of(indexEnum.getIndex()));
}
@Override
public T getById(IndexEnum indexEnum, String id, T clazz) {
elasticsearchRestTemplate.get(id, clazz.getClass(), IndexCoordinates.of(indexEnum.getIndex()));
return null;
}
@Override
public List<T> getAllData(IndexEnum indexEnum, T clazz) {
Query query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchAllQuery()).build();
SearchHits<T> searchHits = (SearchHits<T>) elasticsearchRestTemplate.search(query, clazz.getClass(), IndexCoordinates.of(indexEnum.getIndex()));
return searchHits.get().map(SearchHit::getContent).collect(Collectors.toList());
}
@Override
public List<Map<String, Object>> query(String sql, T clazz) {
throw new AbstractMethodError();
}
@Override
public Page<T> rangeQuery(Integer pageNo, Integer pageSize, IndexEnum indexEnum, Map<String, String> order, Map<String, Object[]> rangeParam, T clazz) {
//范围查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
rangeParam.keySet().forEach(field -> {
boolQueryBuilder.must(new RangeQueryBuilder(field).gt(rangeParam.get(field)[0]).lt(rangeParam.get(field)[1]));
});
PageRequest of = PageRequest.of(pageNo, pageSize);
List<FieldSortBuilder> sortBuilderList = order.keySet().stream()
.map(field -> SortBuilders.fieldSort(field).order(SortOrder.valueOf(order.get(field))))
.collect(Collectors.toList());
Query searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.withPageable(of)
.withSorts((SortBuilder<?>) sortBuilderList)
.build();
SearchHits<T> searchHits = (SearchHits<T>) elasticsearchRestTemplate.search(searchQuery, clazz.getClass());
SearchPage<T> searchHits1 = SearchHitSupport.searchPageFor(searchHits, searchQuery.getPageable());
return new PageImpl<>(searchHits.get().map(SearchHit::getContent).collect(Collectors.toList()), searchHits1.getPageable(), searchHits1.getTotalElements());
}
@Override
public Page<T> pageList(Integer pageNo, Integer pageSize, String keyword, T clazz, Map<String, String> order, String... fields) {
//分页,页码从0开始
PageRequest of = PageRequest.of(pageNo, pageSize);
List<FieldSortBuilder> collect = order.keySet().stream()
.map(field -> SortBuilders.fieldSort(field).order(SortOrder.valueOf(order.get(field))))
.collect(Collectors.toList());
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
Arrays.asList(fields).forEach(e -> boolQueryBuilder.should(QueryBuilders.fuzzyQuery(e, keyword)));
Query searchQuery = new NativeSearchQueryBuilder()
//条件
.withQuery(boolQueryBuilder)
//分页
.withPageable(of)
//排序
.withSorts((SortBuilder<?>) collect)
.build();
SearchHits<T> searchHits = (SearchHits<T>) elasticsearchRestTemplate.search(searchQuery, clazz.getClass());
SearchPage<T> searchHits1 = SearchHitSupport.searchPageFor(searchHits, searchQuery.getPageable());
return new PageImpl<>(searchHits.get().map(SearchHit::getContent).collect(Collectors.toList()), searchHits1.getPageable(), searchHits1.getTotalElements());
}
}
因为ElasticsearchRestTemplate这个操作需要传入具体的索引名称,所以我创建了一个公共枚举类存放es索引名称
IndexEnum
@Getter
@AllArgsConstructor
public enum IndexEnum {
PRODUCT_INFO("product-info", "_doc");
private String index;
private String type;
}
使用的话就直接继承就行了
接口层
public interface ProductService extends BasicEsService<ProductInfo>{
}
实现层
@Service
@Slf4j
public class ProductServiceImpl extends BasicEsServiceImpl<ProductInfo> implements ProductService {
}
后面如果有公共的方法也可以抽出来放在公共类里。
问题总结
1.问题1:启动报错org.elasticsearch.client.ResponseException: method [PUT], host [http://10.0.180.100:9200], URI [/productInfo], status line [HTTP/1.1 406 Not Acceptable]
org.elasticsearch.client.ResponseException: method [PUT], host [http://10.0.180.100:9200], URI [/productInfo], status line [HTTP/1.1 406 Not Acceptable]
这是因为springboot和es版本不对应导致的,我最开始用的是springboot3和es7.6,后来把springboot版本降到了2.7.x。
问题2:“type”:“invalid_index_name_exception”
org.elasticsearch.client.ResponseException: method [PUT], host [http://10.0.180.100:9200], URI [/productInfo?master_timeout=30s&timeout=30s], status line [HTTP/1.1 400 Bad Request]
{"error":{"root_cause":[{"type":"invalid_index_name_exception","reason":"Invalid index name [productInfo], must be lowercase","index_uuid":"_na_","index":"productInfo"}],"type":"invalid_index_name_exception","reason":"Invalid index name [productInfo], must be lowercase","index_uuid":"_na_","index":"productInfo"},"status":400}
这是因为我索引名称用了大写字母,而es规定索引名称不能使用大写,所以修改配置
@Document(indexName = “productInfo”),替换成了@Document(indexName = “product-info”)
最后放个demo
最后在放一个我写的demo地址,有兴趣可以看下
https://gitee.com/wdvc/es-demo.git
唉,就这样吧,如果有问题请指出来我马上修改