五、根据关键字分页搜索
在存在大量数据时,一般我们进行查询都需要进行分页查询。例如:我们指定页码、并指定每页显示多少条数据,然后Elasticsearch返回对应页码的数据。
1、使用from和size来进行分页
在执行查询时,可以指定from(从第几条数据开始查起)和size(每页返回多少条)数
据,就可以轻松完成分页。
l from = (page – 1) * size
POST /es_db/_doc/_search { "from": 0, "size": 2, "query": { "match": { "address": "广州天河" } } }
2、使用scroll方式进行分页
前面使用from和size方式,查询在1W-5W条数据以内都是OK的,但如果数据比较多的时候,会出现性能问题。Elasticsearch做了一个限制,不允许查询的是10000条以后的数据。如果要查询1W条以后的数据,需要使用Elasticsearch中提供的scroll游标来查询。
在进行大量分页时,每次分页都需要将要查询的数据进行重新排序,这样非常浪费性能。
使用scroll是将要用的数据一次性排序好,然后分批取出。性能要比from + size好得多。
使用scroll查询后,排序后的数据会保持一定的时间,后续的分页查询都从该快照取数据
即可。
2.1、第一次使用scroll分页查询
此处,我们让排序的数据保持1分钟,所以设置scroll为1m
GET /es_db/_search?scroll=1m { "query": { "multi_match":{ "query":"广州长沙张三", "fields":["address","name"] } }, "size":100 }
执行后,我们注意到,在响应结果中有一项:"_scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFldqc1c1Y3Y3Uzdpb3FaYTFMT094RFEAAAAAAABLkBZnY1dPTFI5SlFET1BlOUNDQ0RyZi1B" (注意,这个需要填你自己的 _scroll_id)
后续,我们需要根据这个_scroll_id来进行查询
2.2、第二次直接使用scroll id进行查询
GET _search/scroll?scroll=1m { "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFldqc1c1Y3Y3Uzdpb3FaYTFMT094RFEAAAAAAABLkBZnY1dPTFI5SlFET1BlOUNDQ0RyZi1B" }
六、Elasticsearch SQL
Elasticsearch SQL允许执行类SQL的查询,可以使用REST接口、命令行或者是JDBC,都可以使用SQL来进行数据的检索和数据的聚合。
Elasticsearch SQL特点:
本地集成
Elasticsearch SQL是专门为Elasticsearch构建的。每个SQL查询都根据底层存储对相
关节点有效执行。
没有额外的要求
不依赖其他的硬件、进程、运行时库,Elasticsearch SQL可以直接运行在Elasticsearch集群上
轻量且高效像SQL那样简洁、高效地完成查询
1、SQL与Elasticsearch对应关系
**SQL ** | **Elasticsearch ** |
column(列) | field(字段) |
row(行) | document(文档) |
table(表) | index(索引) |
schema(模式) | mapping(映射) |
database server(数据库服务器) | Elasticsearch集群实例 |
2、Elasticsearch SQL语法
SELECT select_expr [, ...] [ FROM table_name ] [ WHERE condition ] [ GROUP BY grouping_element [, ...] ] [ HAVING condition] [ ORDER BY expression [ ASC | DESC ] [, ...] ] [ LIMIT [ count ] ] [ PIVOT ( aggregation_expr FOR column IN ( value [ [ AS ] alias ] [, ...] ) ) ]
目前FROM只支持单表
3、职位查询案例
3.1、查询职位索引库中的一条数据
format:表示指定返回的数据类型 //1.查询职位信息 GET /_sql?format=txt { "query":"SELECT * FROM es_db limit 1" } #返回数据 address | age | name | remark | sex ---------------+---------------+---------------+---------------+--------------- 广州天河公园 |25 |张三 |java developer |1
除了txt类型,Elasticsearch SQL还支持以下类型,
**格式 ** | **描述 ** |
csv | 逗号分隔符 |
json | JSON格式tsv 制表符分隔符 |
txt | 类cli表示 |
yaml | YAML人类可读的格式 |
3.2、将SQL转换为DSL
GET /_sql/translate { "query":"SELECT * FROM es_db limit 1" }
结果如下:
{ "size" : 1, "_source" : { "includes" : [ "age", "remark", "sex" ], "excludes" : [ ] }, "docvalue_fields" : [ { "field" : "address" }, { "field" : "book" }, { "field" : "name" } ], "sort" : [ { "_doc" : { "order" : "asc" } } ] }
3.4、职位全文检索
3.4.1、需求
检索address包含广州和name中包含张三的用户。
3.4.2、MATCH函数
在执行全文检索时,需要使用到MATCH函数。
MATCH( field_exp, constant_exp [, options])
field_exp:匹配字段
constant_exp:匹配常量表达式
3.4.3、实现
GET /_sql?format=txt { "query":"select * from es_db where MATCH(address, '广州') or MATCH(name, '张三') limi t 10" } .4、通过Elasticsearch SQL方式实现分组统计 .4.2、基于Elasticsearch SQL方式实现 GET /_sql?format=txt { "query":"select age, count(*) as age_cnt from es_db group by age" }
这种方式要更加直观、简洁。
Elasticsearch SQL目前的一些限制
目前Elasticsearch SQL还存在一些限制。例如:不支持JOIN、不支持较复杂的子查询。所以,有一些相对复杂一些的功能,还得借助于DSL方式来实现。
七、Java API 操作 ES
相关依赖:
<dependencies> <!‐‐ ES的高阶的客户端API ‐‐> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch‐rest‐high‐level‐client</artifactId> <version>7.6.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j‐core</artifactId> <version>2.11.1</version> </dependency> <!‐‐ 阿里巴巴出品的一款将Java对象转换为JSON、将JSON转换为Java对象的库 ‐‐> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.14.3</version> <scope>test</scope> </dependency> </dependencies>
使用JavaAPI来操作ES集群
**初始化连接 **
**使用的是RestHighLevelClient去连接ES集群,后续操作ES中的数据 **
public JobFullTextServiceImpl() { // 建立与ES的连接 // 1. 使用RestHighLevelClient构建客户端连接。 // 2. 基于RestClient.builder方法来构建RestClientBuilder // 3. 用HttpHost来添加ES的节点 /* RestClientBuilder restClientBuilder = RestClient.builder( new HttpHost("192.168.21.130", 9200, "http") , new HttpHost("192.168.21.131", 9200, "http") , new HttpHost("192.168.21.132", 9200, "http")); */ RestClientBuilder restClientBuilder = RestClient.builder( new HttpHost("127.0.0.1", 9200, "http")); restHighLevelClient = new RestHighLevelClient(restClientBuilder); }
添加职位数据到ES中
使用IndexRequest对象来描述请求
可以设置请求的参数:设置ID、并设置传输ES的数据——注意因为ES都是使用JSON(DSL)来去操作数据的,所以需要使用一个FastJSON的库来将对象转换为JSON字符串进行操作
@Override public void add(JobDetail jobDetail) throws IOException { //1. 构建IndexRequest对象,用来描述ES发起请求的数据。 IndexRequest indexRequest = new IndexRequest(JOB_IDX); //2. 设置文档ID。 indexRequest.id(jobDetail.getId() + ""); //3. 使用FastJSON将实体类对象转换为JSON。 String json = JSONObject.toJSONString(jobDetail); //4. 使用IndexRequest.source方法设置文档数据,并设置请求的数据为JSON格式。 indexRequest.source(json, XContentType.JSON); //5. 使用ES High level client调用index方法发起请求,将一个文档添加到索引中。 restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT); }
查询/删除/搜索/分页
增/删/修改
@Override public void add(JobDetail jobDetail) throws IOException { //1. 构建IndexRequest对象,用来描述ES发起请求的数据。 IndexRequest indexRequest = new IndexRequest(JOB_IDX); //2. 设置文档ID。 indexRequest.id(jobDetail.getId() + ""); //3. 使用FastJSON将实体类对象转换为JSON。 String json = JSONObject.toJSONString(jobDetail); //4. 使用IndexRequest.source方法设置文档数据,并设置请求的数据为JSON格式。 indexRequest.source(json, XContentType.JSON); //5. 使用ES High level client调用index方法发起请求,将一个文档添加到索引中。 restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT); } @Override public JobDetail findById(long id) throws IOException { // 1. 构建GetRequest请求。 GetRequest getRequest = new GetRequest(JOB_IDX, id + ""); // 2. 使用RestHighLevelClient.get发送GetRequest请求,并获取到ES服务器的响应。 GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT); // 3. 将ES响应的数据转换为JSON字符串 String json = getResponse.getSourceAsString(); // 4. 并使用FastJSON将JSON字符串转换为JobDetail类对象 JobDetail jobDetail = JSONObject.parseObject(json, JobDetail.class); // 5. 记得:单独设置ID jobDetail.setId(id); return jobDetail; } @Override public void update(JobDetail jobDetail) throws IOException { // 1. 判断对应ID的文档是否存在 // a) 构建GetRequest GetRequest getRequest = new GetRequest(JOB_IDX, jobDetail.getId() + ""); // b) 执行client的exists方法,发起请求,判断是否存在 boolean exists = restHighLevelClient.exists(getRequest, RequestOptions.DEFAULT); if(exists) { // 2. 构建UpdateRequest请求 UpdateRequest updateRequest = new UpdateRequest(JOB_IDX, jobDetail.getId() + ""); // 3. 设置UpdateRequest的文档,并配置为JSON格式 updateRequest.doc(JSONObject.toJSONString(jobDetail), XContentType.JSON); // 4. 执行client发起update请求 restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT); } } @Override public void deleteById(long id) throws IOException { // 1. 构建delete请求 DeleteRequest deleteRequest = new DeleteRequest(JOB_IDX, id + ""); // 2. 使用RestHighLevelClient执行delete请求 restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT); }
全文检索
@Override public List<JobDetail> searchByKeywords(String keywords) throws IOException { // 1.构建SearchRequest检索请求 // 专门用来进行全文检索、关键字检索的API SearchRequest searchRequest = new SearchRequest(JOB_IDX); // 2.创建一个SearchSourceBuilder专门用于构建查询条件 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 3.使用QueryBuilders.multiMatchQuery构建一个查询条件(搜索title、jd),并配置到SearchSourceBuilder MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keywords, "title", "jd"); // 将查询条件设置到查询请求构建器中 searchSourceBuilder.query(multiMatchQueryBuilder); // 4.调用SearchRequest.source将查询条件设置到检索请求 searchRequest.source(searchSourceBuilder); // 5.执行RestHighLevelClient.search发起请求 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); SearchHit[] hitArray = searchResponse.getHits().getHits(); // 6.遍历结果 ArrayList<JobDetail> jobDetailArrayList = new ArrayList<>(); for (SearchHit documentFields : hitArray) { // 1)获取命中的结果 String json = documentFields.getSourceAsString(); // 2)将JSON字符串转换为对象 JobDetail jobDetail = JSONObject.parseObject(json, JobDetail.class); // 3)使用SearchHit.getId设置文档ID jobDetail.setId(Long.parseLong(documentFields.getId())); jobDetailArrayList.add(jobDetail); } return jobDetailArrayList; }
分页查询
@Override public Map<String, Object> searchByPage(String keywords, int pageNum, int pageSize) throws IOException { // 1.构建SearchRequest检索请求 // 专门用来进行全文检索、关键字检索的API SearchRequest searchRequest = new SearchRequest(JOB_IDX); // 2.创建一个SearchSourceBuilder专门用于构建查询条件 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 3.使用QueryBuilders.multiMatchQuery构建一个查询条件(搜索title、jd),并配置到SearchSourceBuilder MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keywords, "title", "jd"); // 将查询条件设置到查询请求构建器中 searchSourceBuilder.query(multiMatchQueryBuilder); // 每页显示多少条 searchSourceBuilder.size(pageSize); // 设置从第几条开始查询 searchSourceBuilder.from((pageNum - 1) * pageSize); // 4.调用SearchRequest.source将查询条件设置到检索请求 searchRequest.source(searchSourceBuilder); // 5.执行RestHighLevelClient.search发起请求 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); SearchHit[] hitArray = searchResponse.getHits().getHits(); // 6.遍历结果 ArrayList<JobDetail> jobDetailArrayList = new ArrayList<>(); for (SearchHit documentFields : hitArray) { // 1)获取命中的结果 String json = documentFields.getSourceAsString(); // 2)将JSON字符串转换为对象 JobDetail jobDetail = JSONObject.parseObject(json, JobDetail.class); // 3)使用SearchHit.getId设置文档ID jobDetail.setId(Long.parseLong(documentFields.getId())); jobDetailArrayList.add(jobDetail); } // 8. 将结果封装到Map结构中(带有分页信息) // a) total -> 使用SearchHits.getTotalHits().value获取到所有的记录数 // b) content -> 当前分页中的数据 long totalNum = searchResponse.getHits().getTotalHits().value; HashMap hashMap = new HashMap(); hashMap.put("total", totalNum); hashMap.put("content", jobDetailArrayList); return hashMap; }
使用scroll分页方式查询 (深分页)
- 第一次查询,不带scroll_id,所以要设置scroll超时时间
- 超时时间不要设置太短,否则会出现异常
- 第二次查询,SearchSrollRequest
@Override public Map<String, Object> searchByScrollPage(String keywords, String scrollId, int pageSize) throws IOException { SearchResponse searchResponse = null; if(scrollId == null) { // 1.构建SearchRequest检索请求 // 专门用来进行全文检索、关键字检索的API SearchRequest searchRequest = new SearchRequest(JOB_IDX); // 2.创建一个SearchSourceBuilder专门用于构建查询条件 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 3.使用QueryBuilders.multiMatchQuery构建一个查询条件(搜索title、jd),并配置到SearchSourceBuilder MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keywords, "title", "jd"); // 将查询条件设置到查询请求构建器中 searchSourceBuilder.query(multiMatchQueryBuilder); // 设置高亮 HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("title"); highlightBuilder.field("jd"); highlightBuilder.preTags("<font color='red'>"); highlightBuilder.postTags("</font>"); // 给请求设置高亮 searchSourceBuilder.highlighter(highlightBuilder); // 每页显示多少条 searchSourceBuilder.size(pageSize); // 4.调用SearchRequest.source将查询条件设置到检索请求 searchRequest.source(searchSourceBuilder); //-------------------------- // 设置scroll查询 //-------------------------- searchRequest.scroll(TimeValue.timeValueMinutes(5)); // 5.执行RestHighLevelClient.search发起请求 searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); } // 第二次查询的时候,直接通过scroll id查询数据 else { SearchScrollRequest searchScrollRequest = new SearchScrollRequest(scrollId); searchScrollRequest.scroll(TimeValue.timeValueMinutes(5)); // 使用RestHighLevelClient发送scroll请求 searchResponse = restHighLevelClient.scroll(searchScrollRequest, RequestOptions.DEFAULT); } //-------------------------- // 迭代ES响应的数据 //-------------------------- SearchHit[] hitArray = searchResponse.getHits().getHits(); // 6.遍历结果 ArrayList<JobDetail> jobDetailArrayList = new ArrayList<>(); for (SearchHit documentFields : hitArray) { // 1)获取命中的结果 String json = documentFields.getSourceAsString(); // 2)将JSON字符串转换为对象 JobDetail jobDetail = JSONObject.parseObject(json, JobDetail.class); // 3)使用SearchHit.getId设置文档ID jobDetail.setId(Long.parseLong(documentFields.getId())); jobDetailArrayList.add(jobDetail); // 设置高亮的一些文本到实体类中 // 封装了高亮 Map<String, HighlightField> highlightFieldMap = documentFields.getHighlightFields(); HighlightField titleHL = highlightFieldMap.get("title"); HighlightField jdHL = highlightFieldMap.get("jd"); if(titleHL != null) { // 获取指定字段的高亮片段 Text[] fragments = titleHL.getFragments(); // 将这些高亮片段拼接成一个完整的高亮字段 StringBuilder builder = new StringBuilder(); for(Text text : fragments) { builder.append(text); } // 设置到实体类中 jobDetail.setTitle(builder.toString()); } if(jdHL != null) { // 获取指定字段的高亮片段 Text[] fragments = jdHL.getFragments(); // 将这些高亮片段拼接成一个完整的高亮字段 StringBuilder builder = new StringBuilder(); for(Text text : fragments) { builder.append(text); } // 设置到实体类中 jobDetail.setJd(builder.toString()); } } // 8. 将结果封装到Map结构中(带有分页信息) // a) total -> 使用SearchHits.getTotalHits().value获取到所有的记录数 // b) content -> 当前分页中的数据 long totalNum = searchResponse.getHits().getTotalHits().value; HashMap hashMap = new HashMap(); hashMap.put("scroll_id", searchResponse.getScrollId()); hashMap.put("content", jobDetailArrayList); return hashMap; }
高亮查询
- 配置高亮选项
// 设置高亮 HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("title"); highlightBuilder.field("jd"); highlightBuilder.preTags("<font color='red'>"); highlightBuilder.postTags("</font>");
- 需要将高亮的字段拼接在一起,设置到实体类中
// 1)获取命中的结果 String json = documentFields.getSourceAsString(); // 2)将JSON字符串转换为对象 JobDetail jobDetail = JSONObject.parseObject(json, JobDetail.class); // 3)使用SearchHit.getId设置文档ID jobDetail.setId(Long.parseLong(documentFields.getId())); jobDetailArrayList.add(jobDetail); // 设置高亮的一些文本到实体类中 // 封装了高亮 Map<String, HighlightField> highlightFieldMap = documentFields.getHighlightFields(); HighlightField titleHL = highlightFieldMap.get("title"); HighlightField jdHL = highlightFieldMap.get("jd"); if(titleHL != null) { // 获取指定字段的高亮片段 Text[] fragments = titleHL.getFragments(); // 将这些高亮片段拼接成一个完整的高亮字段 StringBuilder builder = new StringBuilder(); for(Text text : fragments) { builder.append(text); } // 设置到实体类中 jobDetail.setTitle(builder.toString()); } if(jdHL != null) { // 获取指定字段的高亮片段 Text[] fragments = jdHL.getFragments(); // 将这些高亮片段拼接成一个完整的高亮字段 StringBuilder builder = new StringBuilder(); for(Text text : fragments) { builder.append(text); } // 设置到实体类中 jobDetail.setJd(builder.toString()); }