ElasticSearch—RestAPI

135 阅读11分钟

一、Java操作索引库(了解)

1. 使用步骤

  1. 导入ES的客户端依赖坐标
  2. 把RestHighLevelClient对象放到IoC容器里:在配置类里或者在引导类里添加
  3. 操作RestAPI
  1. 导入es的客户端依赖坐标

注意:es客户端依赖的版本号,必须要和es的版本号一致

    <!--es客户端-->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.4.0</version>
    </dependency>
  1. 把RestHighLevelClient对象放到IoC容器里:在配置类里或者在引导类里添加
@SpringBootApplication
@MapperScan("com.sdf.mapper")
public class HotelApplication {
    public static void main(String[] args) {
        SpringApplication.run(HotelApplication.class, args);
    }
    //把RestHighLevelClient对象放到IoC容器里
    @Bean
    public RestHighLevelClient esClient(){
        return new RestHighLevelClient(RestClient.builder(
                new HttpHost("192.168.119.129",9200)
        ));
    }
}
  1. 操作RestAPI

操作RestAPI步骤

  1. 创建RestHighLevelClient对象

  2. 使用RestHighLevelClient操作es

  3. 关闭RestHighLevelClient

2. 操作索引库

RestHighLevelClient对象提供了操作索引库的API方法,常用的有:

  • 创建:restClient.indices().create(CreateIndexRequest request, RequestOptions options)

  • 删除:restClient.indices().delete(DeleteIndexRequest request, RequestOptions options)

  • 是否存在:restClient.-().exists(GetIndexRequest request, RequestOptions options)

以上方法的参数:

  • CreateIndexRequest :创建索引库的请求对象
  • DeleteIndexRequest :删除索引库的请求对象
  • GetIndexRequest :查询索引库的请求对象
  • RequestOptions:创建索引库的请求选项,使用固定值RequestOptions.DEFAULT即可

注意事项

  • 使用RestAPI创建索引库非常繁琐,建议在Kibana里使用DSL语句执行操作,而不是用Java代码创建索引库

3. 示例

@SpringBootTest
public class Demo01IndexTest {
    @Autowired
    private RestHighLevelClient esClient;
    @Test
    //创建索引库
    public void testCreateIndex() throws IOException {
        String jsonStr = "{\n" +
                "  \"mappings\": {\n" +
                "    \"properties\": {\n" +
                "      \"id\":{\n" +
                "        \"type\": \"long\"\n" +
                "      },\n" +
                "      \"name\":{\n" +
                "        \"type\": \"text\",\n" +
                "        \"analyzer\": \"ik_max_word\"\n" +
                "      },\n" +
                "      \"price\":{\n" +
                "        \"type\": \"integer\"\n" +
                "      },\n" +
                "      \"stock\":{\n" +
                "        \"type\": \"integer\"\n" +
                "      },\n" +
                "      \"image\":{\n" +
                "        \"type\": \"keyword\",\n" +
                "        \"index\": false\n" +
                "      }\n" +
                "    }\n" +
                "  }\n" +
                "}";

        //注意,使用的是:org.elasticsearch.client.indices.CreateIndexRequest
        CreateIndexRequest request = new CreateIndexRequest("product")
                .source(jsonStr, XContentType.JSON);

        esClient.indices().create(request, RequestOptions.DEFAULT);
    }

    //删除索引库
    @Test
    public void testDeleteIndex() throws IOException {
        DeleteIndexRequest request = new DeleteIndexRequest("product");
        esClient.indices().delete(request, RequestOptions.DEFAULT);
    }
    //索引库是否存在
    @Test
    public void testExistsIndex() throws IOException {
        GetIndexRequest request = new GetIndexRequest("product");
        boolean exists = esClient.indices().exists(request, RequestOptions.DEFAULT);
        System.out.println("exists = " + exists);
    }
}

二、Java操作文档

1. API说明

使用Java操作文档,和在kibana里编写DSl非常类似。只是由Java代码来执行相同的操作

RestHighLevelClient提供了操作文档的API如下:

  • 新增与修改:client.index(IndexRequest request, RequestOptions options)
  • 查看:client.get(GetRequest request, RequestOptions options)
  • 删除:client.delete(DeleteRequest request, RequestOptions options)

以上方法的参数:

  • IndexRequest:新增与修改文档的请求对象
  • GetRequest:查询文档的请求对象
  • DeleteRequest:删除文档的请求对象
  • RequestOptions:请求选项参数,使用固定值RequestOptions.DEFAULT即可

2. 示例

public class Demo02Document {
    @Autowired
    private RestHighLevelClient esClient;
    /**
     * 保存文档
     * 如果id不存在,是新增
     * 如果id已存在,是修改
     */
    @Test
    public void testSaveDocument() throws IOException {
        Product product = new Product(1L, "华为 Mate40 pro", "手机数码", "华为", 5200D, "http://images.huawei.com/pro40.jpg");
        String productJson = JSON.toJSONString(product);

        //1. 准备Request对象,设置索引库名称
        IndexRequest indexRequest = new IndexRequest("product")
                //设置文档id
                .id(product.getId().toString())
                //设置文档数据
                .source(productJson, XContentType.JSON);

        //2. 发送请求
        client.index(indexRequest, RequestOptions.DEFAULT);
    }

    /**
     * 查看文档
     */
    @Test
    public void testFindDocument() throws IOException {
        //1. 准备Request
        GetRequest getRequest = new GetRequest("product", "1");

        //2. 发送请求
        GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);

        //3. 处理响应结果
        String docJson = response.getSourceAsString();
        Product product = JSON.parseObject(docJson, Product.class);
        System.out.println(product);
    }

    /**
     * 删除文档
     */
    @Test
    public void testDeleteDocument() throws IOException {
        //1. 准备Request
        DeleteRequest deleteRequest = new DeleteRequest("product", "1");

        //2. 发送请求
        client.delete(deleteRequest, RequestOptions.DEFAULT);
    }
}

3. 批量操作文档

3.1 说明

批量操作的核心在于将多个普通请求封装在一个批量请求中,一次性发送到服务器,下面仅仅演示批量新增(其他自己尝试)

操作的步骤:

  1. 准备BulkRequest对象
  2. 把每个文档数据,封装到一个IndexRequest对象里。把IndexRequest添加到BulkRequest对象中
  3. 使用client.bulk(bulkRequest对象, RequestOptions.DEFAULT)

3.2 示例

/**
 * 批量操作文档
 */
@Test
public void testBulkDocument() throws IOException {
    // 模拟数据
    List<Product> list = new ArrayList<Product>();
    list.add(new Product(11L, "小米手机", "手机数码", "小米", 3299.00, "http://image.huawei.com/13123.jpg"));
    list.add(new Product(12L, "锤子手机", "手机数码", "锤子", 3699.00, "http://image.huawei.com/13123.jpg"));
    list.add(new Product(13L, "联想手机", "手机数码", "联想", 4499.00, "http://image.huawei.com/13123.jpg"));
    list.add(new Product(14L, "红米手机", "手机数码", "小米", 4299.00, "http://image.huawei.com/13123.jpg"));
    list.add(new Product(15L, "iPhone X", "手机数码", "苹果", 8299.00, "http://image.huawei.com/13123.jpg"));
    list.add(new Product(16L, "MacBook pro", "电脑", "苹果", 14999.00, "http://image.huawei.com/13123.jpg"));
    list.add(new Product(17L, "AirPods", "手机数码", "苹果", 4299.00, "http://image.huawei.com/13123.jpg"));
    list.add(new Product(18L, "外星人", "电脑", "DELL", 21299.00, "http://image.huawei.com/13123.jpg"));
    list.add(new Product(19L, "联想小新", "电脑", "联想", 6299.00, "http://image.huawei.com/13123.jpg"));

    //1. 创建Request
    BulkRequest bulkRequest = new BulkRequest("product");

    //2. 添加数据
    for (Product product : list) {
        //2.1 把一个Product对象,封装到一个IndexRequest对象里
        IndexRequest indexRequest = new IndexRequest().id(product.getId().toString());
        indexRequest.source(JSON.toJSONString(product), XContentType.JSON);
        //2.2 把一个IndexRequest对象添加到BulkRequest对象里e
        bulkRequest.add(indexRequest);
    }

    //3. 发送请求
    client.bulk(bulkRequest, RequestOptions.DEFAULT);
}

三、Java搜索文档(重点)

文档搜索的基本步骤是:

  1. 创建SearchRequest对象
  2. 准备request.source(),也就是DSL。
  3. QueryBuilders来构建查询条件
  4. 传入request.source()query()方法
  5. 发送请求,得到结果
  6. 解析结果(参考JSON结果,从外到内,逐层解析)

1.快速入门

API说明

发起请求得到响应

image-20210721203950559.png

  • SearchRequest:用于封装检索参数

    searchRequest对象.source()提供了构造检索参数的一系列方法:

    searchRequest对象.source()
        .query(检索条件)  //可以使用QueryBuilders构造各种不同类型的查询条件
        .sort(排序)
        .from(起始索引).size(查询数量)
        .highlighter(高亮);
    

    QueryBuilders提供了一系列方法,用于构造不同类型的查询条件,常用的有:

    • QueryBuilders.matchAllQuery():查询全部
    • QueryBuilders.matchQuery("字段", 值):match查询条件
    • QueryBuilders.multiMatchQuery(值, "字段1", "字段2", ...):multi_match查询条件
    • QueryBuilders.termQuery("字段", 值):term查询条件
    • QueryBuilders.rangeQuery("字段").gte(值1).lte(值2):range查询条件
    • QueryBuilders.geoDistanceQuery("字段").distance(距离,长度单位):geo_distance查询条件
    • QueryBuilders.functionScoreQuery():算分函数查询
    • QueryBuilders.boolQuery().must(..).should(..).mustNot(..).filter(..):bool查询条件
  • client.search(SearchRequest request, RequestOptions options):发起请求进行检索

处理响应得到结果

image-20210721214221057.png

client.search()方法返回的结果是一个JSON,结构包含:

  • hits:命中的结果
    • total:总条数,其中的value是具体的总条数值
    • max_score:所有结果中得分最高的文档的相关性算分
    • hits:搜索结果的文档数组,其中的每个文档都是一个json对象
      • _source:文档中的原始数据,也是json对象

因此,我们解析响应结果,就是逐层解析JSON,流程如下:

  • SearchHits:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果
    • searchHits对象.getTotalHits().value:获取总条数信息
    • searchHits对象.getHits():获取SearchHit数组,也就是文档数组
      • searchHit对象.getSourceAsString():获取文档结果中的_source,即原始的json文档数据

示例

完整代码示例:

public class Demo01 {
    @Autowired
    private RestHighLevelClient client;

    @Test
    public void testMatchAll() throws IOException {
        //1. 构造SearchRequest对象。构造参数:索引库名
        SearchRequest request = new SearchRequest("hotel");

        //2. 设置查询信息
        request.source().query(QueryBuilders.matchAllQuery());

        //3. 发起请求,得到响应
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //4. 处理响应,得到结果
        SearchHits result = response.getHits();
        //  总数量
        long total = result.getTotalHits().value;
        System.out.println("总数量:" + total);
        //  列表
        SearchHit[] hits = result.getHits();
        for (SearchHit hit : hits) {
            System.out.println(hit.getSourceAsString());
        }
    }
}

2.match查询

API说明

构造查询条件时:

  • QueryBuilders.matchQuery("字段", 值):match查询条件
  • QueryBuilders.multiMatchQuery(值, "字段1", "字段2", ...):multi_match查询条件
//==========match构造查询条件==========
QueryBuilders.matchQuery("字段名", "值")

示例

@Test
public void testMatch() throws IOException {
    //1. 构造SearchRequest对象。构造参数:索引库名
    SearchRequest request = new SearchRequest("hotel");

    //2. 设置查询信息
    //	match
    request.source().query(QueryBuilders.matchQuery("all", "北京酒店"));
    //  multi_match。字段过多的话,会影响性能,不推荐使用
    //request.source().query(QueryBuilders.multiMatchQuery("北京酒店", "name", "brand"));

    //3. 发起请求,得到响应
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    //4. 处理响应,得到结果
    SearchHits result = response.getHits();
    //  总数量
    long total = result.getTotalHits().value;
    System.out.println("总数量:" + total);
    //  列表
    SearchHit[] hits = result.getHits();
    for (SearchHit hit : hits) {
        System.out.println(hit.getSourceAsString());
    }
}

3.精确查询

API说明

构造查询条件时:

  • QueryBuilders.termQuery("字段", 值):term查询条件
  • QueryBuilders.rangeQuery("字段").gte(值1).lte(值2):range查询条件
//==========构造精确查询条件==========
QueryBuilders.termQuery("字段名", "值");
QueryBuilders.rangeQuery("字段名").gte(最小值).lt(最大值);

示例

@Test
public void testTerm() throws IOException {
    //1. 构造SearchRequest对象。构造参数:索引库名
    SearchRequest request = new SearchRequest("hotel");

    //2. 设置查询信息
    //  term
    // request.source().query(QueryBuilders.termQuery("brand", "如家"));
    //  range
    request.source().query(QueryBuilders.rangeQuery("price").gte(300).lte(500));

    //3. 发起请求,得到响应
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    //4. 处理响应,得到结果
    SearchHits result = response.getHits();
    //  总数量
    long total = result.getTotalHits().value;
    System.out.println("总数量:" + total);
    //  列表
    SearchHit[] hits = result.getHits();
    for (SearchHit hit : hits) {
        System.out.println(hit.getSourceAsString());
    }
}

4.布尔查询

API说明

构造查询条件时:

  • QueryBuilders.boolQuery().must(..).should(..).mustNot(..).filter(..):bool查询条件
//==========构造bool查询条件==========
QueryBuilders.boolQuery()
    .must(主搜索条件)
    .should(偏好条件)
    .filter(过滤条件)
    .mustNot(剔除条件);

示例代码

@Test
public void testBoolQuery() throws IOException {
    //1. 构造SearchRequest对象。构造参数:索引库名
    SearchRequest request = new SearchRequest("hotel");

    //2. 设置查询信息
    //  bool查询
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
        //must条件
        .must(QueryBuilders.matchQuery("all", "北京酒店"))
        //should条件
        .should(QueryBuilders.termQuery("starName", "五星"))
        //must_not条件
        .mustNot(QueryBuilders.rangeQuery("score").lte(40))
        //filter条件
        .filter(QueryBuilders.termQuery("brand", "希尔顿"));
    request.source().query(boolQuery);

    //3. 发起请求,得到响应
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    //4. 处理响应,得到结果
    SearchHits result = response.getHits();
    //  总数量
    long total = result.getTotalHits().value;
    System.out.println("总数量:" + total);
    //  列表
    SearchHit[] hits = result.getHits();
    for (SearchHit hit : hits) {
        System.out.println(hit.getSourceAsString());
    }
}

5. 算分函数【了解】

image-20220629113515690.png

@Test
public void testFunctionScore() throws IOException {
    SearchRequest request = new SearchRequest("hotel");

    //设置查询条件
    FunctionScoreQueryBuilder scoreQueryBuilder = QueryBuilders.functionScoreQuery(
        //基础查询
        QueryBuilders.matchQuery("all", "北京酒店"),
        //对应DSL里functions数组
        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
            new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                //filter,过滤出来要重新算分的数据
                QueryBuilders.termQuery("brand", "希尔顿"),
                //设置算分函数,使用权重值
                ScoreFunctionBuilders.weightFactorFunction(10)
            )
        }
    );
    //处分函数的加权模式:Multiply,相乘。数据的原始得分 乘 权重值。如果不设置加权模式,默认就是相乘
    scoreQueryBuilder.boostMode(CombineFunction.MULTIPLY);

    request.source().query(scoreQueryBuilder);


    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    SearchHits result = response.getHits();
    //      获取总数量
    long total = result.getTotalHits().value;
    System.out.println("总数量:" + total);
    //      获取数据列表
    SearchHit[] hits = result.getHits();
    for (SearchHit hit : hits) {
        //获取文档对象的原始数据
        String docJson = hit.getSourceAsString();
        HotelDoc doc = JSON.parseObject(docJson, HotelDoc.class);
        System.out.println("查询得到的数据:" + doc);

        System.out.println("匹配度得分:" + hit.getScore());
    }
}

6.排序、分页

API说明

搜索结果的排序和分页是与query同级的参数,因此同样是使用request.source()来设置。

searchRequest对象.source()
    .query(检索条件) 
    .sort(排序)
    .from(起始索引).size(查询数量)
    .highlighter(高亮);

//=========构造排序条件=========
//  简单排序
request.source()
    .sort("排序字段", SortOrder.排序规则)
    .sort("排序字段", SortOrder.排序规则);
//  距离排序
SortBuilder sortBuilder = SortBuilders
    .geoDistanceSort("坐标字段名", 圆心点纬度, 圆心点经度)
    .order(SortOrder.ASC)
    .unit(DistanceUnit.距离单位);
request.source().sort(sortBuilder);

//=======构造分页条件===========
//  分页的from值 = (页码-1)*每页几条
request.source().from(分页的起始索引).size(分页的每页几条);

image-20210721221121266.png

示例

@Test
public void testSortAndPage() throws IOException {
    //1. 构造SearchRequest对象。构造参数:索引库名
    SearchRequest request = new SearchRequest("hotel");

    //2. 设置查询信息
    request.source()
        .query(QueryBuilders.matchAllQuery())
        //按照price升序排列
        .sort(SortBuilders.fieldSort("price").order(SortOrder.ASC))
        //从索引0开始,查询5条
        .from(0).size(5);

    //3. 发起请求,得到响应
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    //4. 处理响应,得到结果
    SearchHits result = response.getHits();
    //  总数量
    long total = result.getTotalHits().value;
    System.out.println("总数量:" + total);
    //  列表
    SearchHit[] hits = result.getHits();
    for (SearchHit hit : hits) {
        System.out.println(hit.getSourceAsString());
    }
}

7.高亮

高亮的代码与之前代码差异较大,有两点:

  • 查询的DSL:其中除了查询条件,还需要添加高亮条件,同样是与query同级。
  • 结果解析:结果除了要解析_source文档数据,还要解析高亮结果

API说明

构建高亮请求
searchRequest对象.source()
    .query(检索条件) 
    .sort(排序)
    .from(起始索引).size(查询数量)
    .highlighter(高亮); // 使用HighLightBuilder设置高亮信息

//======构造高亮条件============
HighlightBuilder hb = new HighlightBuilder()
    .field("高亮字段名").preTags("前置标签").postTags("后置标签")
    .requireFieldMatch(false);

构造高亮请求如下:

image-20210721221744883.png

注意:高亮查询必须使用文本检索查询,并且要有搜索关键字,将来才可以对关键字高亮。

处理高亮结果

高亮的结果与查询的文档结果默认是分离的,并不在一起。

因此解析高亮的代码需要额外处理:

image-20210721222057212.png

代码说明:

  1. 从结果中获取原始文档json:

    hit.getSourceAsString(),这部分是非高亮的结果,json格式的字符串。还需要反序列为HotelDoc对象

  2. 获取高亮结果,替换掉原本的非高亮值:

    1. 获取高亮Map:hit.getHighlightFields()

      返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值

    2. 从高亮Map中,根据高亮字段名称,获取高亮字段值对象HighlightField

    3. HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了

    4. 用高亮的结果替换HotelDoc中的非高亮结果

示例

@Test
public void testHighLight() throws IOException {
    //1. 构造SearchRequest对象。构造参数:索引库名
    SearchRequest request = new SearchRequest("hotel");

    //2. 设置查询信息
    request.source()
            .query(QueryBuilders.matchQuery("all", "如家"))
            // name中关键字高亮显示
            .highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));

    //3. 发起请求,得到响应
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);

    //4. 处理响应,得到结果
    SearchHits result = response.getHits();
    //  总数量
    long total = result.getTotalHits().value;
    System.out.println("总数量:" + total);
    //  列表
    SearchHit[] hits = result.getHits();
    for (SearchHit hit : hits) {
        // 获取原始文档数据(没有高亮的)
        HotelDoc doc = JSON.parseObject(hit.getSourceAsString(), HotelDoc.class);
        // 获取高亮值,把doc里非高亮值替换掉
        Map<String, HighlightField> highlightFieldMap = hit.getHighlightFields();
        if (highlightFieldMap != null) {
            HighlightField nameField = highlightFieldMap.get("name");
            if (nameField != null) {
                String nameHighLightValue = nameField.getFragments()[0].string();
                doc.setName(nameHighLightValue);
            }
        }

        System.out.println(doc);
    }
}

8. 聚合

API语法

聚合条件与query条件同级别,因此需要使用request.source()来指定聚合条件。

聚合条件的语法:

image-20210723173057733.png

聚合的结果也与查询结果不同,API也比较特殊。不过同样是JSON逐层解析:

image-20210723173215728.png

示例

@SpringBootTest
@RunWith(SpringRunner.class)
public class Demo01AggTest {
    @Autowired
    private RestHighLevelClient esClient;

    /**
     * 聚合为桶
     */
    @Test
    public void test01() throws IOException {
        SearchRequest request = new SearchRequest("hotel");

        //查询文档数量为0
        TermsAggregationBuilder aggregationBuilder = AggregationBuilders
                //聚合类型:terms聚合,聚合名称:brandAgg
                .terms("brandAgg")
                //聚合字段
                .field("brand")
                //桶的数量
                .size(20)
                //聚合排序
                .order(BucketOrder.count(true));
        
        request.source()
                .size(0)
                .aggregation(aggregationBuilder);

        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);

        //获取所有聚合结果
        Aggregations aggregations = response.getAggregations();
        //  获取名称为brandAgg的聚合结果。是terms聚合,要把返回值转换成Terms
        Terms brandAgg = aggregations.get("brandAgg");
        //  获取所有桶
        List<? extends Terms.Bucket> buckets = brandAgg.getBuckets();
        for (Terms.Bucket bucket : buckets) {
            String key = bucket.getKeyAsString();
            long docCount = bucket.getDocCount();
            System.out.println("key = " + key + ", docCount = " + docCount);
        }
    }

    /**
     * 桶内度量
     */
    @Test
    public void test02() throws IOException {
        SearchRequest request = new SearchRequest("hotel");

        TermsAggregationBuilder aggregationBuilder = AggregationBuilders
                .terms("brandAgg")
                .field("brand")
                .size(100)
                .subAggregation(AggregationBuilders.stats("statAgg").field("price"))
                .order(BucketOrder.aggregation("statAgg.avg", true));

        request.source()
                .size(0)
                .aggregation(aggregationBuilder);

        SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);

        Aggregations aggregations = response.getAggregations();
        Terms brandAgg = aggregations.get("brandAgg");
        List<? extends Terms.Bucket> buckets = brandAgg.getBuckets();
        buckets.forEach(bucket->{
            String key = bucket.getKeyAsString();
            long docCount = bucket.getDocCount();
            Stats statAgg = bucket.getAggregations().get("statAgg");
            long count = statAgg.getCount();
            double sum = statAgg.getSum();
            double max = statAgg.getMax();
            double min = statAgg.getMin();
            double avg = statAgg.getAvg();

            System.out.println(String.format("key:%s, docCount:%s, sum:%s, max:%s, min:%s, avg:%s", key, docCount, sum, max, min, avg));
        });
    }
}