分布式搜索引擎
1.了解elasticsearch
1.1 elasticsearch的作用
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容。
例如:
在GitHub搜索代码
在电商网站搜索商品
在百度搜索答案
1.2 ELK技术栈
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:
而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。
1.3 elasticsearch和luncene
elasticsearch底层是基于lucene来实现的。
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:Apache Lucene - Welcome to Apache Lucene/ 。
1.4 总结
什么是elasticsearch?
一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
什么是elastic stack(ELK)?
是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch
什么是Lucene?
是Apache的开源搜索引擎类库,提供了搜索引擎的核心API。
2.倒排索引
倒排索引的概念是基于MySQL这样的正向索引而言的。
2.1正向索引
那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:
如果是根据id查询,那么直接走索引,查询速度非常快。
但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:
1)用户搜索数据,条件是title符合"%手机%"
2)逐行获取数据,比如id为1的数据
3)判断数据中的title是否符合用户搜索条件
4)如果符合则放入结果集,不符合则丢弃。回到步骤1
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
2.2 倒排索引
倒排索引中有两个非常重要的概念:
文档(
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息词条(
Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理,流程如下:
将每一个文档的数据利用算法分词,得到一个个词条
创建表,每行数据包括词条、词条所在文档id、位置等信息
因为词条唯一性,可以给词条创建索引,例如hash表结构索引
如图:
倒排索引的搜索流程如下(以搜索"华为手机"为例):
1)用户输入条件"华为手机"
进行搜索。
2)对用户输入内容分词,得到词条:华为
、手机
。
3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。
4)拿着文档id到正向索引中查找具体文档。
如图:
虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。
2.3 正向和倒排总结
那么为什么一个叫做正向索引,一个叫做倒排索引呢?
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
是不是恰好反过来了?
那么两者方式的优缺点是什么呢?
正向索引:
优点:
可以给多个字段创建索引
根据索引字段搜索、排序速度非常快
缺点:
根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引:
优点:
根据词条搜索、模糊搜索时,速度非常快
缺点:
只能给词条创建索引,而不是字段
无法根据字段做排序
3 es的一些概念
3.1 文档和字段
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:
而Json文档中往往包含很多的字段(Field),类似于数据库中的列。
3.2 索引和映射
索引(Index),就是相同类型的文档的集合。
例如:
所有用户文档,就可以组织在一起,称为用户的索引;
所有商品的文档,可以组织在一起,称为商品的索引;
所有订单的文档,可以组织在一起,称为订单的索引;
因此,我们可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
映射注释概述使用MappingElasticsearchConverter元数据来驱动对象到文档的映射。元数据取自可以注释的实体属性
可以使用以下注释:
@Document:在类级别应用以指示该类是映射到数据库的候选对象。其中比较重要的属性是:indexName:存储此实体的索引的名称。这可以包含一个 SpEL 模板表达式,如"log-#{T(java.time.LocalDate).now().toString()}"type:映射类型。如果未设置,则使用类的小写简单名称。(自 4.0 版起已弃用)shards:索引的分片数。replicas:索引的副本数。refreshIntervall:索引的刷新间隔。用于创建索引。默认值为“1s”。indexStoreType:索引的索引存储类型。用于创建索引。默认值为“1s”。createIndex: 标记是否在存储库引导时创建索引。默认值为true。versionType:版本管理的配置。默认值为外部。
@Id:应用于字段级别以标记用于标识目的的字段。
@Transient:默认情况下,所有字段在存储或检索时都映射到文档,此注释不包括该字段。@PersistenceConstructor:标记一个给定的构造函数–甚至是一个包保护的构造函数–再从数据库中实例化对象时使用。构造函数参数按名称映射到检索到的Document中的键值。
@Field:应用于字段级别并定义字段的属性,大部分属性映射到各自的Elasticsearch Mapping定义(以下列表不完整,查看注释Javadoc以获得完整参考):name:将在 Elasticsearch 文档中表示的字段名称,如果未设置,则使用 Java 字段名称。type:字段类型,可以是Text、Keyword、Long、Integer、Short、Byte、Double、Float、Half_Float、Scaled_Float、Date、Date_Nanos、Boolean、Binary、Integer_Range、Float_Range、Long_Range、Double_Range、Date_Range、Ip_Range、Object 之一, 嵌套, Ip, TokenCount, Percolator, Flattened, Search_As_You_Type。format以及Date类型pattern的定义。store: 标记原始字段值是否应该存储在 Elasticsearch 中,默认值为false。analyzer, searchAnalyzer,normalizer用于指定自定义分析器和规范器。
@GeoPoint: 将字段标记为geo_point数据类型。如果字段是类的实例,则可以省略GeoPoint。
3.3 mysql与elasticsearch
我们统一的把mysql与elasticsearch的概念做一下对比:
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
是不是说,我们学习了elasticsearch就不再需要mysql了呢?
并不是如此,两者各自有自己的擅长支出:
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch:擅长海量数据的搜索、分析、计算
因此在企业中,往往是两者结合使用:
对安全性要求较高的写操作,使用mysql实现
对查询性能要求较高的搜索需求,使用elasticsearch实现
两者再基于某种方式,实现数据的同步,保证一致性(可以使用mq、binlog、logstash、beats,根据项目要求选择即可)
4 索引库操作
4.1mapping映射属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
数值:long、integer、short、byte、double、float、
布尔:boolean
日期:date
对象:object
index:是否创建索引,默认为true
analyzer:使用哪种分词器
properties:该字段的子字段
{ "age": 21, "weight": 72.1, "isMarried": false, "info": "江苏汇运晶达", "email": "huiyun@qq.com", "score": [99.1,99.5,98.9], "name": { "firstName": "汇", "lastName": "运" } }
对应的每个字段映射(mapping):
age:类型为 integer;参与搜索,因此需要index为true;无需分词器
weight:类型为float;参与搜索,因此需要index为true;无需分词器
isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器
info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器
name:类型为object,需要定义多个子属性
name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
4.2索引库的CRUD
创建索引库:PUT /索引库名
查询索引库:GET /索引库名
删除索引库:DELETE /索引库名
添加字段:PUT /索引库名/_mapping
4.3 文档操作
文档操作有哪些?
创建文档:POST /{索引库名}/_doc/文档id { json文档 }
查询文档:GET /{索引库名}/_doc/文档id
删除文档:DELETE /{索引库名}/_doc/文档id
修改文档:
全量修改:PUT /{索引库名}/_doc/文档id { json文档 }
增量修改:POST /{索引库名}/_update/文档id { "doc": {字段}}
5 springboot整合ES
目前所使用过的有两种方式,当然还有其它的。
5.1 Rest client
Rest client分为:Java Low Level Rest Client和Java High Level Rest Client .
Rest Client:官方推荐使用,所以我们采用这个方式,这个分为两个Low Level REST Client和High Level REST Client,Low Level REST Client是早期出的API比较简陋了,还需要自己去拼写Query DSL,High Level REST Client使用起来更好用,更符合面向对象的感觉。
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
ElasticSearchConfig.java
@Configuration
public class ElasticSearchConfig {
@Bean
public RestHighLevelClient restHighLevelClient(){
return new RestHighLevelClient(
RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));
}
}
Mapping.java
public class UserIndexConstants {
public static final String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"name\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"age\": {\n" +
" \"type\": \"keyword\"\n" +
" }\n" +
" }\n" +
"}";
}
RestHighTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class RestHighTest {
@Resource
private RestHighLevelClient restHighLevelClient;
/**************************************创建索引*****************************************/
@Test
public void CreateIndex() throws IOException{
//创建索引的请求
CreateIndexRequest request = new CreateIndexRequest("index_user");
//添加索引的mapping规则
request.source(UserIndexConstants.MAPPING_TEMPLATE,XContentType.JSON);
//发送请求
restHighLevelClient.indices().create(request,RequestOptions.DEFAULT);
}
/*************************************测试索引是否存在**********************************/
@Test
public void ExitIndex() throws IOException{
// 获取索引的请求
GetIndexRequest request = new GetIndexRequest("index_user");
// 执行请求
restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
}
/*************************************测试删除索引*************************************/
@Test
public void DeleteIndex() throws IOException {
// 删除索引的请求
DeleteIndexRequest nan_index = new DeleteIndexRequest("index_user");
// 执行删除的请求
restHighLevelClient.indices().delete(nan_index,RequestOptions.DEFAULT);
}
/*************************************测试文档的添加*************************************/
@Test
public void testCreateDoc() throws IOException {
// 准备好数据
User user = new User("小杰", 23);
// 创建好index请求
IndexRequest indexRequest = new IndexRequest("index_user");
// 设置索引
indexRequest.id("1");
// 设置超时时间(默认)
indexRequest.timeout(TimeValue.timeValueSeconds(5));
// 往请求中添加数据
indexRequest.source(JSON.toJSONString(user), XContentType.JSON);
//执行添加请求
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
System.out.println(indexResponse);
}
/*************************************测试文档的修改*******************************/
// 测试文档的更新(id存在就是更新,id不存在就是添加)(用下面这个方法更新时,是全局更新,就是说里面的字段全部被覆盖新对象里没有的字段就没有了)
@Test
public void testUpdateDoc() throws IOException {
User user = new User("xiaoer", 18);
IndexRequest indexRequest = new IndexRequest("index_user");
indexRequest.id("2");
indexRequest.source(JSON.toJSONString(user),XContentType.JSON);
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
System.out.println(indexResponse);
}
/*************************************文档数据的更新(局部更新)***********************/
// 测试文档的更新(id存在就是更新,id不存在就是添加)(用下面这个方法更新时,是局部更新,就是说只会覆盖其中有的字段)
@Test
public void testUpdateBetterDoc() throws IOException {
//准备好修改的数据
User user = new User("xiaoer", 18);
// 创建更新请求
UpdateRequest updateRequest = new UpdateRequest("index_user", "1");
// 把要更新的数据装进去
updateRequest.doc(JSON.toJSONString(user),XContentType.JSON);
// 执行更新语句
UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(updateResponse);
}
/*************************************文档数据的删除************************/
@Test
public void testDelDoc() throws IOException {
DeleteRequest deleteRequest = new DeleteRequest("index_user","2");
DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println(deleteResponse);
}
/*************************************根据id获取文档数据(简单)************************/
@Test
public void testGetDoc() throws IOException {
GetRequest getRequest = new GetRequest("index_user");
getRequest.id("1");
GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
System.out.println(getResponse);
}
/*************************************批量添加文档数据************************/
// 测试文档批量添加(添加会了,批量删除、更新、修改是一样的)
@Test
public void testBulkAdd() throws IOException {
// 准备要添加的数据
List<User> users = new ArrayList<>();
users.add(new User("xiaofei",25));
users.add(new User("xiaohua",22));
users.add(new User("xiaoer",23));
users.add(new User("xiaoge",22));
users.add(new User("xiaomei",23));
// 创建批量请求
BulkRequest bulkRequest = new BulkRequest();
// 利用循环将每一个add请求添加到bulkRequest请求中
for (int i = 0; i < users.size(); i++) {
IndexRequest indexRequest = new IndexRequest("index_user").id(""+i);
indexRequest.source(JSON.toJSONString(users.get(i)),XContentType.JSON);
bulkRequest.add(indexRequest);
}
// 执行批量请求
BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println(bulkResponse);
}
/****************************文档数据的查询(带条件)重点(后面的都很重要)******************/
// 测试文档列表的查询(带条件)
// 这是ElasticSearch最重要的地方
@Test
public void testGetListDoc() throws IOException {
SearchRequest searchRequest = new SearchRequest("index_user");
// 构建搜索builder
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 构建查询条件(查询所有)
// MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
// 构建查询条件(精确匹配)
TermsQueryBuilder termsQueryBuilder = QueryBuilders.termsQuery("name", "xiaomei");
// 把查询条件设置给搜索builder
searchSourceBuilder.query(termsQueryBuilder);
// 设置分页查询(跟sql语句的limit一样)
searchSourceBuilder.from(0); // 开始下标(当前页码-1)*每页显示条数
searchSourceBuilder.size(3); // 要查多少个
// 设置排序规则
searchSourceBuilder.sort("age",SortOrder.DESC);
// 把所有条件设置给查询请求
searchRequest.source(searchSourceBuilder);
// 开始查询
SearchResponse searchResponse =
restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 查看结果(结果很多,所以循环)
SearchHits hits = searchResponse.getHits();
for (SearchHit hit: hits) {
System.out.println(hit.getSourceAsString());
}
}
/**********字段过滤(当我们想要查询的结果中的字段不是所有的,只有表中的部分字段)*************/
// 测试过滤查询出来的字段(也就是当我们不想把表中所有的字段查出来)
@Test
public void testFilterDoc() throws IOException {
// 构建搜索查询请求
SearchRequest searchRequest = new SearchRequest().indices("index_user");
// 构建查询条件builder
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 构建真正的查询条件(这里是全部查询)
MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
// 把查询条件设置给builder
searchSourceBuilder.query(matchAllQueryBuilder);
// 设置过滤字段
String[] excludes = {};
String[] includes = {"name"};
searchSourceBuilder.fetchSource(includes,excludes);
// 把所有的查询条件builder设置给查询请求
searchRequest.source(searchSourceBuilder);
//执行请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//打印结果
SearchHits hits = searchResponse.getHits();
for (SearchHit hit:hits) {
System.out.println(hit.getSourceAsString());
}
}
/*******************************多条件查询(也叫组合查询)*****************************/
// 多条件查询,也叫组合查询
@Test
public void testBoolqueryDoc() throws IOException {
// 构建查询请求
SearchRequest searchRequest = new SearchRequest("index_user");
// 构建搜索条件builder
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 构建多条件builder
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
// 在多条件builder中设置满足条件
// must 就是必须满足这个条件(相当于mysql中column = 值)
// mustnot 就是必须不满足这个条件(相当于mysql中column!=值)
// should 就是或者的意思(相当于mysql中的or)
boolQueryBuilder.must(QueryBuilders.termsQuery("name","xiaofei"));
// boolQueryBuilder.must(QueryBuilders.termsQuery("age","24"));
boolQueryBuilder.should(QueryBuilders.termsQuery("age","24"));
boolQueryBuilder.should(QueryBuilders.termsQuery("age","28"));
// 把多条件查询条件放到builder中
searchSourceBuilder.query(boolQueryBuilder);
// 把所有搜索条件设置到查询请求中
searchRequest.source(searchSourceBuilder);
// 执行请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 打印结果
SearchHits hits = searchResponse.getHits();
for(SearchHit hit :hits){
System.out.println(hit.getSourceAsString());
}
}
/*******************************范围查询(就是某个字段在什么范围内)*****************************/
// 范围查询
@Test
public void testRangeDoc() throws IOException {
// 构建查询请求
SearchRequest searchRequest = new SearchRequest("index_user");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 设置到范围的字段
RangeQueryBuilder rangeQueryBuilder = new RangeQueryBuilder("age");
// 设置范围()gte就是大于等于
rangeQueryBuilder.gte(24);
rangeQueryBuilder.lte(30);
// 把范围查询设置到条件中
searchSourceBuilder.query(rangeQueryBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
for(SearchHit hit :hits){
System.out.println(hit.getSourceAsString());
}
}
/*******************************模糊查询*************************************/
// 模糊查询
@Test
public void testLikeDoc() throws IOException {
SearchRequest searchRequest = new SearchRequest("index_user");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("name", "xiao").fuzziness(Fuzziness.TWO);
searchSourceBuilder.query(fuzzyQueryBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
for(SearchHit hit :hits){
System.out.println(hit.getSourceAsString());
}
}
/*******************************高亮查询*************************************/
//高亮查询
@Test
public void testHighLightDoc() throws IOException {
// 构建搜索请求
SearchRequest searchRequest = new SearchRequest("index_user");
// 构建搜索条件构造器(也就是总的搜索条件)
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 构建单独的一个高亮构建器
HighlightBuilder highlightBuilder = new HighlightBuilder();
// 设置高亮字段
highlightBuilder.preTags("<font color='red'>"); //前缀
highlightBuilder.postTags("</font>"); // 后缀
highlightBuilder.field("name");
// 把单独的高亮构建器设置给总构建器
searchSourceBuilder.highlighter(highlightBuilder);
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
// 把总的搜索条件给到搜索请求中
searchRequest.source(searchSourceBuilder);
// 执行请求
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 打印结果
SearchHits hits = searchResponse.getHits();
for(SearchHit hit :hits){
System.out.println(hit.getSourceAsString());
// 获取对应的高亮域
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
System.out.println(highlightFields);
// 获取对应的高亮字段
HighlightField highlightField = highlightFields.get("name");
if(highlightField != null) {
// 拿到高亮字段的文本域
Text[] texts = highlightField.getFragments();
String name = "";
for (Text text : texts) {
name += text;
// 打印高亮字段
System.out.println(name);
}
}
}
}
/******************************最大值、平均值、最小值查询*************************************/
// 最大值、平均值、最小值
@Test
public void testAggraDoc() throws IOException {
SearchRequest searchRequest = new SearchRequest("index_user");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 构建一个最大值builder
MaxAggregationBuilder maxAggregationBuilder = AggregationBuilders.max("MAXAGE").field("age");
// 把最大值builder设置给总查询条件
searchSourceBuilder.aggregation(maxAggregationBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
for(SearchHit hit : hits){
System.out.println(hit.getSourceAsString());
}
}
/******************************分组查询*************************************/
// 分组查询
@Test
public void testAggraGroupDoc() throws IOException {
SearchRequest searchRequest = new SearchRequest("index_user");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 构建一个分组builder
// terms里面的参数是给分组取的名字、后面field是要分组的字段
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("AGEGROUP").field("age");
// 把分组builder设置给总查询条件
searchSourceBuilder.aggregation(termsAggregationBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
for(SearchHit hit : hits){
System.out.println(hit.getSourceAsString());
}
}
}
用过es的朋友应该都了解es的分页模式总共有两种,一种是from,size模式,也就是常说的浅分页,这种分页模式使用起来很简单,和mysql的十分类似,但是这种方式的最大弊端是,在集群模式下,如果查询的分页数很深,很容易造成查询效率极慢甚至内存的异出的问题,解决这个问题就要提到深分页,深分页的大概思路是每一页查询会给你返回一个scrollId,类似于一个游标,记录本次查询的位置,下次使用这个有游标再去查下一页的数据,所有不会出现跨度大,查询数据多导致溢出的问题,但是它也有一个弊端就是不支持跳页。还有就是es默认的查询(也就是不加from,size)是会返回第一页10条数据,当然这里可以设置size的长度最大为10000。
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder().size(10000);
浅分页
private List<SearchHit> docSearch(Date time, String title) {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder builder = QueryBuilders.boolQuery();
builder.must(QueryBuilders.matchAllQuery())
.filter(QueryBuilders.rangeQuery("publish_time").gt(time.getTime()));
try {
searchSourceBuilder.query(builder)
.sort("id", SortOrder.ASC)
.sort("publish_time", SortOrder.ASC)
.from(0)
.size(10);
SearchRequest searchRequest = new SearchRequest("索引名称").source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] hits = searchResponse.getHits().getHits();
if (hits.length > 0) {
return Arrays.asList(hits);
} else {
return null;
}
} catch (IOException e) {
log.error("doc分页查询异常:{} ", e);
}
return null;
}
from+size查询在10000-50000条数据(1000到5000页)以内的时候还是可以的,但是如果数据过多的话,就会出现深分页问题。为了这个问题,es提出了scroll滚动查询方式scroll滚动搜索,会在第一次搜索的时候,保存一个当下的快照。之后只会基于该快照提供数据搜索。在这个期间数据如果发生变动,是不会让用户看到的。推荐非实时处理大量数据的情况可以使用不适用于有跳页的情景 .
@Autowired
private RestHighLevelClient restHighLevelClient;
public void scrollDemo() {
//构造查询条件
SearchRequest searchRequest = new SearchRequest("索引库");
SearchSourceBuilder builder = new SearchSourceBuilder();
//设置查询超时时间
Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
builder.query(QueryBuilders.rangeQuery("字段").gte("开始时间").lte("结束时间"));
//设置最多一次能够取出10000笔数据,从第10001笔数据开始,将开启滚动查询 PS:滚动查询也属于这一次查询,只不过因为一次查不完,分多次查
builder.size(10000);
searchRequest.source(builder);
//将滚动放入
searchRequest.scroll(scroll);
SearchResponse searchResponse = null;
try {
searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
log.error("查询索引库失败", e.getMessage(), e);
}
SearchHits hits = searchResponse.getHits();
//记录要滚动的ID
String scrollId = searchResponse.getScrollId();
//TODO 对结果集的处理
//滚动查询部分,将从第10001笔数据开始取
SearchHit[] hitsScroll = hits.getHits();
while (hitsScroll != null && hitsScroll.length > 0 ) {
//构造滚动查询条件
SearchScrollRequest searchScrollRequest = new SearchScrollRequest(scrollId);
searchScrollRequest.scroll(scroll);
try {
//响应必须是上面的响应对象,需要对上一层进行覆盖
searchResponse = restHighLevelClient.scroll(searchScrollRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
log.error("滚动查询失败",e.getMessage(),e);
}
scrollId = searchResponse.getScrollId();
hits = searchResponse.getHits();
hitsScroll = hits.getHits();
//TODO 同上面完全一致的结果集处理
}
//清除滚动,否则影响下次查询
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
ClearScrollResponse clearScrollResponse = null;
try {
clearScrollResponse = restHighLevelClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
log.error("滚动查询清除失败",e.getMessage(),e);
}
//清除滚动是否成功
boolean succeeded = clearScrollResponse.isSucceeded();
}
5.2 Spring Data Repository
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
application.properties
spring:
application:
name: search
data:
elasticsearch:
cluster-name: my-application
cluster-nodes: 192.168.10.12:9300
定义一个User实体类
@Document(indexName = "content", type = "doc")
@Data
public class User {
@Id
@Field(type = FieldType.Auto)
private String id;
@Field(analyzer = "ik_max_word",type = FieldType.Text)
private String name;
@Firld(type = FieldType.Keyword)
private Integer age;
}
自定义UserRepository并集成ElasticsearchRepository
public interface UserRepository extends ElasticsearchRepository<User, String> {
User findByName(String name);
}
定义UserService.java
public interface UserService {
void save(User user);
void update(User user);
User getById(String id);
User getByName(String name);
void delete(String id);
}
定义UserService实现类UserServiceImp.java
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserRepository userRepository;
@Override
public User getById(String id) {
return userRepository.findById(id).get();
}
@Override
public void save(User user) {
userRepository.save(user);
}
@Override
public User getByName(String name) {
return userRepository.findByName(name);
}
@Override
public void delete(String id) {
userRepository.deleteById(id);
}
@Override
public void update(User user) {
userRepository.delete(user);
userRepository.save(user);
}
}