本教程仅做个人工作笔记,可能不适用于他人的工作/学习
实际生产中,除了考虑产品的性能跟用户体验之外,生产成本也是要考虑的,如果系统是小系统,想对某一个表(200w)做全文检索的话,可以考虑使用mysql自带的full_text索引,没必要使用elasticsearch,毕竟开发相关功能、维护还有服务器的费用,都是相当可观的一笔支出
以下是在mysql中的测试,数据是260w左右
-- 无设置索引 260万条数据查询大概4秒/新增索引之后消耗0.01秒
SELECT * FROM full_text WHERE title = '中国药学杂志'
-- 设置索引后模糊查询还是消耗4秒多,说明百分号前置的情况下索引不生效
SELECT * FROM full_text WHERE title LIKE '%中国药学杂志'
-- 如果将百分号后置,则能使用索引查询,消耗0.01秒
SELECT * FROM full_text WHERE title LIKE '中国药学杂志%'
-- 如果前后都放置%符号,也是不走索引查询的
SELECT * FROM full_text WHERE title LIKE '%中国药学杂志%'
-- 当表中有数据时,新增索引消耗了12秒
ALTER TABLE full_text ADD INDEX index_title (title)
适当的设置索引还是可以很大程度解决查询慢的问题的(前提是充分理解业务,将表设计好)
当然如果很有钱或者是想用ELK全套的,另说
添加相关依赖
根据你的elasticsearch版本选择合适的maven依赖,添加到项目中,截至今天2019/12/27,maven上最新的spring-data-elasticsearch的版本是3.2.3.RELEASE
,笔者使用es版本是6.6,maven依赖如下
org.springframework.boot
spring-boot-starter-data-elasticsearch
2.0.0.RELEASE
创建实体类(Mappings)
Elasticsearch一个核心就是mappings
,【Elasticsearch的mappings】
es的java高级客户端可以通过拼接json去创建一个mappings,代码相对繁琐一点,spring-data-elasticsearch提供了一种用注释创建mappings的方案,首先我们创建一个基本的实体类,如
public class Metadata implements Serializable {
private static final long serialVersionUID = 1L;
private Long recordId;
private Integer metadataType;
private Long bookRecNo;
private String title;
}
稍微做一下改动
import org.springframework.data.elasticsearch.annotations.Field;
@Data
@Document(indexName = "your_index_name", type = "metadata")
public class Metadata implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@JsonSerialize(using = ToStringSerializer.class)
private Long recordId;
@Field(type = FieldType.Integer)
private Integer metadataType;
@Field(type = FieldType.Long)
@JsonSerialize(using = ToStringSerializer.class)
private Long bookRecNo;
@Field(type = FieldType.Text, analyzer = "ik_smart", fielddata = true)
private String title;
}
解释一下
type
是es文档中字段的类型,如Long、Text、Keyword等;analyzer是指定分词器;当一个字段需要用来做聚合时,比如排序,则需要将fielddata设置为true,注意,fielddata=true
时,会消耗索引内存,所以不需要将每一个的fielddata设置为true,其默认为false;另外需要注意一点,如果一个字段我们需要他做一些类似SQL中group by的操作,我们需要将其的type设置为Keyword,即@Field(type = FieldType.Keyword)
ElasticsearchRepository
创建一个接口继承ElasticsearchRepository
用来对索引进行基本的数据操作,代码如下:
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
@Component
public interface DataRepository extends ElasticsearchRepository {
}
配置文件 yml
spring-data-elasticsearch链接ES服务器的操作不用像java高级客户端那样在代码中实现,而是通过配置文件连接ES的节点,通过实体类的注解以及Repository来指定跟操作对应索引(index、type),以下是springboot项目application.yml关于ES的配置
spring:
data:
elasticsearch:
cluster-name: my-es-server
cluster-nodes: 127.0.0.1:9300
repositories:
enabled: true
以上工作完成后,可以通过代码对索引进行一些基本的操作了
建议先去官方文档大致了解一下 Document Api
对索引的操作需要用到org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
它可以对索引进行一些基本的操作,也可以对索引中的数据进行操作,然后对数据操作,笔者使用的是上面提到的ElasticsearchRepository
@Autowired
ElasticsearchTemplate template;
@Override
public void createIndex() {
//直接使用带注解的实体类创建索引,会有默认的setting跟mappings
boolean index = template.createIndex(Metadata.class);
//使用带注解的实体类跟自定义settings设置创建索引
boolean index1 = template.createIndex(Metadata.class, null);
//使用带注解的实体类创建索引,会有默认的setting跟mappings
boolean indexName = template.createIndex("indexName");
//使用带注解的实体类跟自定义settings设置创建索引
boolean indexName1 = template.createIndex("indexName", null);
}
@Override
public void createMappings() {
/**
* 在创建索引后,如果没有给一个确切的mappings是可以的,但是他的字段会根据你后面给什么数据而创建默认的mappings
*/
boolean b = template.putMapping(Metadata.class);
boolean b1 = template.putMapping("indexName", "typeName", Metadata.class);
}
@Override
public void getMappingsInfo() {
//根据实体类获取
Map mapping = template.getMapping(Metadata.class);
//指定index、type获取
Map mapping1 = template.getMapping("indexName", "indexType");
}
@Override
public void getSettingsInfo() {
//获取settings信息
Map setting = template.getSetting(Metadata.class);
Map indexName = template.getSetting("indexName");
}
@Override
public void deleteIndex() {
//删除索引信息
boolean b = template.deleteIndex(Metadata.class);
boolean indexName = template.deleteIndex("IndexName");
}
附上一个es索引的信息
{
"state": "open",
"settings": {
"index": {
"refresh_interval": "1s",
"number_of_shards": "5",
"provided_name": "data_collect",
"creation_date": "1576738381899",
"store": {
"type": "fs"
},
"number_of_replicas": "1",
"uuid": "kPp7n73uRYWO5U30BAbIMQ",
"version": {
"created": "6060099"
}
}
},
"mappings": {
"metadata": {
"properties": {
"recordId": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"metadataType": {
"type": "integer"
},
"title": {
"fielddata": true,
"analyzer": "ik_smart",
"type": "text"
},
"bookRecNo": {
"type": "long"
}
}
}
},
"aliases": [],
"primary_terms": {
"0": 1,
"1": 1,
"2": 1,
"3": 1,
"4": 1
},
"in_sync_allocations": {
"0": [
"0ACHmsl-SpObHXqLjLRxGw"
],
"1": [
"QZch3H8ESYaPMDIWxBz3kg"
],
"2": [
"KFGEePccSZ-zfKuzXLvb0w"
],
"3": [
"dPTdMlgzQDaE_tUBXCtnrQ"
],
"4": [
"ZYRQp9biQ-2MuDAyVeJboA"
]
}
}
先看代码
@Autowired
DataRepository repository;
@Override
public void addDocument() {
//单个添加
Metadata metadata = new Metadata();
Metadata save = repository.save(metadata);
//批量添加
ArrayList list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Metadata data = new Metadata();
list.add(data);
i = i + 1;
}
Iterable dataList = repository.saveAll(list);
}
@Override
public void deleteDocument() {
//删除所有
repository.deleteAll();
//根据id删除
repository.deleteById(1L);
//传输一个实体类(不为空),查询到实体类之后删除其对应的文档,底层还是根据id删除
repository.delete(new Metadata());
//批量删除
ArrayList list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Metadata data = new Metadata();
list.add(data);
i = i + 1;
}
repository.deleteAll(list);
}
修改的话,只要实体类的id不变,那么直接使用save的方法,修改后es上该文档的version会+1;
建议先去官方文档大致了解一下 Query DSL
先看一下基本的代码流程
@Autowired
DataRepository repository;
@Override
public Page getFromElastic(Map map) {
//检索条件集合,后面需要实现的检索条件都存放在这里
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//分页
PageRequest pageRequest = PageRequest.of(Integer.valueOf(map.get("page").toString()),
Integer.valueOf(map.get("limit").toString()));
//排序
SortBuilder sort = SortBuilders.fieldSort("publishDate").order(SortOrder.DESC);
String sortBy = (String) map.get("sortBy");
if (!StringUtil.isNullOrEmpty(sortBy)) {
sort = SortBuilders.fieldSort(sortBy).unmappedType("Date");
String sortRule = (String) map.get("sortRule");
if (!StringUtil.isNullOrEmpty(sortRule) && "ASC".equals(sortRule)) {
sort.order(SortOrder.ASC);
}
}
//聚合
TermsAggregationBuilder aggJobYear = AggregationBuilders
.terms("jobYear").field("jobYear").showTermDocCountError(true);
//整合构建
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery)//检索条件
.withSort(sort)//排序条件
.withPageable(pageRequest)//分页条件
.addAggregation(aggJobYear)//聚合(分面即SQL的group by)条件
.build();
Page search = repository.search(query);
return search;
}
检索结果
{
"content": [//数据省略],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"offset": 0,
"pageSize": 10,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"facets": [{
"name": "jobYear",
"type": "term",
"terms": [{
"term": "2019",
"count": 551
}],
"total": 1,
"other": 0,
"missing": 0
}],
"aggregations": {
"asMap": {
"jobYear": {
"name": "jobYear",
"metaData": null,
"buckets": [{
"docCount": 551,
"docCountError": 0,
"aggregations": {
"asMap": {},
"fragment": true
},
"key": 2019,
"keyAsString": "2019",
"keyAsNumber": 2019,
"fragment": true
}],
"docCountError": 0,
"writeableName": "lterms",
"sumOfOtherDocCounts": 0,
"type": "lterms",
"fragment": true,
"mapped": true
}
},
"fragment": true
},
"scrollId": null,
"maxScore": 1,
"totalElements": 1135,
"totalPages": 114,
"number": 0,
"size": 10,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"first": true,
"numberOfElements": 10,
"last": false,
"empty": false
}
大致的检索就如上面那样,关注点放在boolQuery中,其他的基本不变,但是这种检索存在问题,聚合addAggregation的问题,如果用来做分面的字段是Text类型(对应java中的String),检索结果的json序列化会有转化的问题,笔者尝试了很多方法都解决不了,网上相关的解决方案也不多,笔者的做法是将普通的检索跟分面操作分开,然后将两个结果手动封装后再返回给接口调用方;
接下来着重看一下QueryBuilders的各种操作,可以打印一下boolQuery
,查看对应的DSL;比如上面的操作对应的DSL语句是:
{
"bool" : {
"adjust_pure_negative" : true,
"boost" : 1.0
}
}
分页、排序不在赘述
实现类似SQL的Like查询
实现对指定字段的精确查询