RestHighLevelClient

客户端介绍

elasticsearch 官网中提供了各种语言的客户端:

在这里插入图片描述

选择 Java REST Client

选择 Java High Level Rest Client 版本,这里有使用的API

在这里插入图片描述


es依赖

    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>6.2.4</version>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
        <version>6.2.4</version>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>6.2.4</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.70</version>
    </dependency>

索引库及映射

创建索引库的同时创建type及其映射关系,但是这些操作不建议使用java客户端完成,原因如下:

  • 索引库和映射往往是初始化时完成,不需要频繁操作,不如提前配置好

  • 官方提供的创建索引库及映射API非常繁琐,需要通过字符串拼接json结构:

    request.mapping(
            "{\n" +
            "  \"properties\": {\n" +
            "    \"message\": {\n" +
            "      \"type\": \"text\"\n" +
            "    }\n" +
            "  }\n" +
            "}", 
            XContentType.JSON);
    

因此,这些操作建议还是使用 Rest 风格API去实现。

以这样一个商品数据为例来创建索引库:

新增实体类:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
    private Long id;
    private String title; //标题
    private String category;// 分类
    private String brand; // 品牌
    private Double price; // 价格
    private String images; // 图片地址
}
  • id:可以认为是主键,将来判断数据是否重复的标示,不分词,可以使用keyword类型
  • title:搜索字段,需要分词,可以用text类型
  • category:商品分类,这个是整体,不分词,可以使用keyword类型
  • brand:品牌,与分类类似,不分词,可以使用keyword类型
  • price:价格,这个是double类型
  • images:图片,用来展示的字段,不搜索,index为false,不分词,可以使用keyword类型

映射配置:

PUT /item
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  },
  "mappings": {
    "_doc": {
      "properties": {
        "id": {
          "type": "keyword"
        },
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "category": {
          "type": "keyword"
        },
        "brand": {
          "type": "keyword"
        },
        "images": {
          "type": "keyword",
          "index": false
        },
        "price": {
          "type": "double"
        }
      }
    }
  }
}

查看添加结果

在这里插入图片描述


文档操作

初始化http客户端:RestHighLevelClient

完成任何操作都需要通过 RestHighLevelClient 客户端

入门示例

@RunWith(SpringRunner.class)
@SpringBootTest
class EsDemoApplicationTests {

    RestHighLevelClient client;

    /**
     * 初始化连接
     */
    @Before
    void init() {
        //初始化:高级客户端
        client = new RestHighLevelClient(RestClient.builder(
            new HttpHost("192.168.85.135", 9201, "http"),
            new HttpHost("192.168.85.135", 9202, "http"),
            new HttpHost("192.168.85.135", 9203, "http")
        ));
    }
    
    @After
    void close() throws IOException {
        client.close();
    }
}

应用级案例

application.yml 配置文件

# es集群名称
elasticsearch.clusterName=single-node-cluster
# es用户名
elasticsearch.userName=elastic
# es密码
elasticsearch.password=elastic
# es 是否启用用户密码
elasticsearch.passwdEnabled=true
# es host ip 地址(集群):本次使用的是单机模式
elasticsearch.hosts=43.142.243.124:9200
# es 请求方式
elasticsearch.scheme=http
# es 连接超时时间
elasticsearch.connectTimeOut=1000
# es socket 连接超时时间
elasticsearch.socketTimeOut=30000
# es 请求超时时间
elasticsearch.connectionRequestTimeOut=500
# es 连接保持活跃时间(ms)
elasticsearch.keepAliveStrategy=180000
# es 最大连接数
elasticsearch.maxConnectNum=100
# es 每个路由的最大连接数
elasticsearch.maxConnectNumPerRoute=100

es连接配置类

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * restHighLevelClient 客户端配置类
 */
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticsearchConfig {

    // es host ip 地址(集群)
    private String hosts;
    // es用户名
    private String userName;
    // es密码
    private String password;
    // es 是否启用用户密码
    private boolean passwdEnabled;
    // es 请求方式
    private String scheme;
    // es集群名称
    private String clusterName;
    // es 连接超时时间
    private int connectTimeOut;
    // es socket 连接超时时间
    private int socketTimeOut;
    // es 请求超时时间
    private int connectionRequestTimeOut;
    // es 连接保持活跃时间
    private int keepAliveStrategy;
    // es 最大连接数
    private int maxConnectNum;
    // es 每个路由的最大连接数
    private int maxConnectNumPerRoute;

    @Bean(name = "restHighLevelClient")
    public RestHighLevelClient restHighLevelClient() {
        // 拆分地址。单节点配一个地址即可
        List<HttpHost> hostLists = new ArrayList<>();
        hosts = hosts.replace("http://", "");
        String[] hostList = hosts.split(",");
        for (String addr : hostList) {
            String host = addr.split(":")[0];
            String port = addr.split(":")[1] == null ? "9200": addr.split(":")[1];
            hostLists.add(new HttpHost(host, Integer.parseInt(port), scheme));
        }
        // 转换成 HttpHost 数组
        HttpHost[] httpHost = hostLists.toArray(new HttpHost[]{});

        // 构建连接对象
        RestClientBuilder builder = RestClient.builder(httpHost);

        // 连接延时配置
        builder.setRequestConfigCallback(requestConfigBuilder -> {
            requestConfigBuilder
                .setConnectTimeout(connectTimeOut)
                .setSocketTimeout(socketTimeOut)
                .setConnectionRequestTimeout(connectionRequestTimeOut);
            return requestConfigBuilder;
        });


        builder.setHttpClientConfigCallback(httpClientBuilder -> {
            httpClientBuilder
                // 连接数配置
                .setMaxConnTotal(maxConnectNum)
                .setMaxConnPerRoute(maxConnectNumPerRoute)
                // 连接保持活跃时间配置
                .setKeepAliveStrategy((HttpRequest, HttpResponse) -> keepAliveStrategy);
            // 设置用户名、密码
            if (passwdEnabled) {
                CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
                credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userName, password));
                httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
            }
            return httpClientBuilder;
        });

        return new RestHighLevelClient(builder);
    }
}

注:

  • KeepAliveStrategy :

    HTTP 规范没有确定一个持久连接可能或应该保持活动多长时间。

    一些HTTP服务器使用非标准的头部信息 Keep-Alive 来告诉客户端它们想在服务器端保持连接活动的周期秒数。

    如果这个信息可用,HttpClient 就会利用这个它。

    如果头部信息 Keep-Alive 在响应中不存在,HttpClient 假设连接无限期的保持活动。

    然而许多现实中的 HTTP 服务器配置了在特定不活动周期之后丢掉持久连接来保存系统资源,往往这是不通知客户端的。


创建索引及映射

索引的 mappings

mapping_test.json

{
    "properties": {
        "brandName": {
            "type": "keyword"
        }, 
        "categoryName": {
            "type": "keyword"
        }, 
        "createTime": {
            "type": "date", 
            "format": "yyyy-MM-dd HH:mm:ss"
        }, 
        "id": {
            "type": "long"
        }, 
        "price": {
            "type": "double"
        }, 
        "saleNum": {
            "type": "integer"
        }, 
        "status": {
            "type": "integer"
        }, 
        "stock": {
            "type": "integer"
        }, 
        "spec": {
            "type": "text", 
            "analyzer": "ik_max_word", 
            "search_analyzer": "ik_smart"
        }, 
        "title": {
            "type": "text", 
            "analyzer": "ik_max_word", 
            "search_analyzer": "ik_smart"
        }
    }
}
import com.example.test.service.es.IndexTestService;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.IndicesClient;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.indices.GetIndexResponse;
import org.elasticsearch.cluster.metadata.MappingMetaData;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Map;

/**
 * 索引服务类
 */
@Service
public class IndexTestServiceImpl implements IndexTestService {
    
    @value("classpath:json/mapping_test.json")
    private resoure mappingTest;

    @Autowired
    RestHighLevelClient restHighLevelClient;
    
    // 分片数的配置名
    private String shardNumName = "number_of_shards";
    
    // 副本数的配置名
    private String replicaNumName = "number_of_replicas";
    
    // 索引名
    private String index = "goods"

    @Override
    public boolean indexCreate() throws Exception {
        // 1、创建 创建索引request 参数:索引名
        CreateIndexRequest indexRequest = new CreateIndexRequest(index);
        // 2、设置索引的settings
        indexRequest.settings(Settings.builder().put(shardNumName, 3).put(replicaNumName, 1));
        // 3、设置索引的mappings(表结构)
        String mappingJson = IOUtils.toString(mappingTest.getInputStream(), Charset.forName("UTF-8"));
        indexRequest.mapping(mappingJson, XContentType.JSON);
        // 4、 设置索引的别名
        // 5、 发送请求
        // 5.1 同步方式发送请求
        // 请求服务器
        IndicesClient indicesClient = restHighLevelClient.indices();
        CreateIndexResponse response = indicesClient.create(indexRequest, RequestOptions.DEFAULT);

        return response.isAcknowledged();
    }


    /**
     * 获取表结构
     * GET goods/_mapping
     */
    @Override
    public Map<String, Object> getMapping(String indexName) throws Exception {
        IndicesClient indicesClient = restHighLevelClient.indices();

        // 创建get请求
        GetIndexRequest request = new GetIndexRequest(indexName);
        // 发送get请求
        GetIndexResponse response = indicesClient.get(request, RequestOptions.DEFAULT);
        // 获取表结构
        Map<String, MappingMetaData> mappings = response.getMappings();
        Map<String, Object> sourceAsMap = mappings.get(indexName).getSourceAsMap();
        return sourceAsMap;
    }

    /**
     * 删除索引库
     */
    @Override
    public boolean indexDelete(String indexName) throws Exception {
        IndicesClient indicesClient = restHighLevelClient.indices();
        // 创建delete请求方式
        DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexName);
        // 发送delete请求
        AcknowledgedResponse response = indicesClient.delete(deleteIndexRequest, RequestOptions.DEFAULT);

        return response.isAcknowledged();
    }

    /**
     * 判断索引库是否存在
     */
    @Override
    public boolean indexExists(String indexName) throws Exception {
        IndicesClient indicesClient = restHighLevelClient.indices();
        // 创建get请求
        GetIndexRequest request = new GetIndexRequest(indexName);
        // 判断索引库是否存在
        boolean result = indicesClient.exists(request, RequestOptions.DEFAULT);

        return result;
    }
}

新增文档:index

文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/6.8/java-rest-high-document-index.html

示例:

// 新增文档
@Test
void add() throws IOException {
    // 准备文档
    Item item = new Item(1L, "小米手机9", "手机",
            "小米", 3499.00, "http://image.leyou.com/13123.jpg");
    // 将对象转换为Json
    String json = JSON.toJSONString(item);
    // 创建索引请求 参数为: 索引库名   类型名  文档ID
    IndexRequest request = new IndexRequest("item", "_doc", item.getId().toString());
    // 将Json格式的数据放入到请求中
    request.source(json, XContentType.JSON);
    // 发送请求
    IndexResponse response = client.index(request);
    // 打印结果
    System.out.println("结果为:" + response);
}

响应:

response = IndexResponse[index=item,type=docs,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={"total":2,"successful":2,"failed":0}]

查看文档:get

示例:

// 根据ID获取文档
@Test
void get() throws IOException {
    // 创建get请求对象 参数为: 索引库名   类型名  文档ID
    GetRequest request = new GetRequest("item","_doc","1");
    // 发送请求
    GetResponse response = client.get(request);
    // 解析结果 结果为Json
    String source = response.getSourceAsString();
    // 将Json数据转换为对象 参数为: Json字符串  类的字节码
    Item item = JSON.parseObject(source, Item.class);
    // 打印结果
    System.out.println(item);
}

更新文档:update

示例:

// 根据ID更新文档
@Test
void update() throws IOException{
    // 准备文档
    Item item = new Item(1L, "小米手机9", "手机",
            "小米", 3699.00, "http://image.leyou.com/13123.jpg");
    // 将对象转换为Json
    String json = JSON.toJSONString(item);
    // 创建Update请求对象 参数为: 索引库名   类型名  文档ID
    UpdateRequest request = new UpdateRequest("item","_doc","1");
    // 将Json格式的数据放入到请求中
    request.doc(json,XContentType.JSON);
    // 发送请求
    UpdateResponse response = client.update(request);
    // 打印结果
    System.out.println("结果为:" + response);
}

删除文档:delete

示例:

// 根据ID删除文档
@Test
void delete() throws IOException {
    // 创建Delete请求对象 参数为: 索引库名   类型名  文档ID
    DeleteRequest request = new DeleteRequest("item","_doc","1");
    // 发送请求
    DeleteResponse response = client.delete(request);
    // 打印结果
    System.out.println("结果为:" + response);
}

批量新增:bulk

示例:

    // 批量插入
    @Test
    void bulkInsert() throws IOException {
        // 准备文档数据:
        List<Item> list = new ArrayList<>();
        list.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.leyou.com/13123.jpg"));
        list.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.leyou.com/13123.jpg"));
        list.add(new Item(3L, "华为META10", "手机", "华为", 4499.00, "http://image.leyou.com/13123.jpg"));
        list.add(new Item(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.leyou.com/13123.jpg"));
        list.add(new Item(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.leyou.com/13123.jpg"));
        // 创建批量新增请求
        BulkRequest request = new BulkRequest();
        // 遍历集合
        for (Item item : list) {
            // 将索引请求添加到批量请求对象中
            request.add(new IndexRequest("item", "_doc", item.getId().toString())
                    .source(JSON.toJSONString(item), XContentType.JSON));
        }
        // 发送请求
        BulkResponse response = client.bulk(request);
        // 打印结果
        System.out.println("结果为:" + response);
    }

文档搜索:search

查询所有:matchAllQuery

示例:

    /**
     * 查询文档
     */
    @Test
    public void searchDoc() throws IOException {
        // 创建查询请求对象 指定查询的索引名称
        SearchRequest request = new SearchRequest("item");
        // 指定查询的源
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 构建查询条件
        QueryBuilder query = QueryBuilders.matchAllQuery(); // 查询的是所有的文档

        // 添加查询条件
        sourceBuilder.query(query);

        // 添加查询源
        request.source(sourceBuilder);
        // 发送请求
        SearchResponse response = client.search(request);
        // 分析响应结果
        // 返回命中的数据对象
        SearchHits searchHits = response.getHits();
        // 获取命中的文档个数
        long totalHits = searchHits.totalHits;
        System.out.println("命中的文档个数为: " + totalHits);
        // 获取命中的数据
        SearchHit[] hits = searchHits.getHits();
        // 遍历数组
        for (SearchHit hit : hits) {
            String sourceAsString = hit.getSourceAsString();
            // 转换成对象
            Item item = JSON.parseObject(sourceAsString, Item.class);
            System.out.println(item);
        }
    }

上面的代码中,搜索条件是通过 sourceBuilder.query(QueryBuilders.matchAllQuery())来添加的。这个 query() 方法接受的参数是: QueryBuilder 接口类型。

QueryBuilder 接口提供了很多实现类,分别对应不同类型的查询,例如:term查询、match查询、range查询、boolean查询等。若要使用各种不同查询,只需传递不同的参数给 sourceBuilder.query() 方法即可。而这些实现类并不需要去 new ,官方提供了 QueryBuilders 工厂来构建各种实现类

在这里插入图片描述


关键字搜索:matchQuery

其实搜索类型的变化,仅仅是利用QueryBuilders构建的查询对象不同而已,其他代码基本一致:

// 使用match查询,参数为 1 查询的字段 2 查询的关键字
QueryBuilder query = QueryBuilders.matchQuery("title","小米");

关键字完全匹配:termQuery

// 使用term查询,参数为 1 查询的字段 2 查询的关键字
QueryBuilder query = QueryBuilders.termQuery("title","小米手机");

范围查询:rangeQuery

支持下面的范围关键字:

方法说明
gt(Object from)大于
gte(Object from)大于等于
lt(Object from)小于
lte(Object from)小于等于

示例:

// 使用rangeQuery查询,参数为 查询的字段
QueryBuilder query = QueryBuilders.rangeQuery("price").gte(2000).lt(4000);  // 参数为:查询的字段 后面是链式的调用

响应:

item = Item(id=2, title=坚果手机R1, category=手机, brand=锤子, price=3699.0, 
            images=http://image.leyou.com/13123.jpg)
item = Item(id=5, title=荣耀V10, category=手机, brand=华为, price=2799.0, 
            images=http://image.leyou.com/13123.jpg)
item = Item(id=1, title=小米手机7, category=手机, brand=小米, price=3299.0, 
            images=http://image.leyou.com/13123.jpg)

过滤 :fetchSource

默认情况下,索引库中所有数据都会返回,如果想只返回部分字段,可以通过fetchSource来控制。

示例:

    /**
     * 查询文档
     */
    @Test
    public void searchDoc() throws IOException {
        // 创建查询请求对象 指定查询的索引名称
        SearchRequest request = new SearchRequest("item");
        // 指定查询的源
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 构建查询条件
        // QueryBuilder query = QueryBuilders.matchAllQuery(); // 查询的是所有的文档

        // 使用match查询,参数为 1 查询的字段 2 查询的关键字
        // QueryBuilder query = QueryBuilders.matchQuery("title","小米");

        // 使用term查询,参数为 1 查询的字段 2 查询的关键字
        // QueryBuilder query = QueryBuilders.termQuery("title","小米手机");

        // 使用rangeQuery查询,参数为 1 查询的字段
        QueryBuilder query = QueryBuilders.rangeQuery("price").gte(3000).lte(4000);

        // 添加查询条件
        sourceBuilder.query(query);

        // 添加过滤
        String[] includes = {"id", "title", "price"};
        String[] excludes = {};
        sourceBuilder.fetchSource(includes, excludes);

        // 添加查询源
        request.source(sourceBuilder);
        // 发送请求
        SearchResponse response = client.search(request);
        // 分析响应结果
        // 返回命中的数据对象
        SearchHits searchHits = response.getHits();
        // 获取命中的文档个数
        long totalHits = searchHits.totalHits;
        System.out.println("命中的文档个数为: " + totalHits);
        // 获取命中的数据
        SearchHit[] hits = searchHits.getHits();
        // 遍历数组
        for (SearchHit hit : hits) {
            String sourceAsString = hit.getSourceAsString();
            // 转换成对象
            Item item = JSON.parseObject(sourceAsString, Item.class);
            System.out.println(item);
        }
    }

排序:sort

示例:

// 排序
sourceBuilder.sort("price", SortOrder.DESC);

分页:from、size

示例:

// 分页
int current = 1;
int size = 2;
int start = (current - 1) * size;
sourceBuilder.from(start);
sourceBuilder.size(size);
// 搜索
SearchResponse response = client.search(request);

高亮:HighlightBuilder

示例:

	@Test
    public void testHighlight() throws IOException{
        // 创建搜索对象
        SearchRequest request = new SearchRequest();
        // 指定查询的源
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 添加查询条件,通过QueryBuilders获取各种查询
        sourceBuilder.query(QueryBuilders.matchQuery("title", "小米手机"));

        // 高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder()	//创建高亮构建器对象
                .field("title") // 指定高亮字段
                .preTags("<em style='color:red'>")  // 添加高亮前缀
                .postTags("</em>"); // 添加高亮后缀
        sourceBuilder.highlighter(highlightBuilder);
        request.source(sourceBuilder);

        // 获取结果
        SearchResponse response = client.search(request);
        SearchHits hits = response.getHits();
        SearchHit[] hitList = hits.getHits();
        for (SearchHit hit : hitList) {
            // 获取高亮结果
            Map<String, HighlightField> fields = hit.getHighlightFields();
            // 取出标题
            HighlightField titleField = fields.get("title");
            // 拼接为字符串
            Text[] fragments = titleField.fragments();
            String title = fragments[0].string();
            // 获取其它字段,并转换成对象
            Item item = JSON.parseObject(hit.getSourceAsString(), Item.class);
            // 覆盖title
            item.setTitle(title);
            System.out.println(item);
        }
    }

关键代码:

  • 查询条件中添加高亮字段:
    • new HighlightBuilder() :创建高亮构建器
    • .field(“title”) :指定高亮字段
    • .preTags(“”) 和 .postTags(“”) :指定高亮的前置和后置标签
  • 解析高亮结果:
    • hit.getHighlightFields(); 获取高亮结果

聚合:aggregation

再来试试聚合,以brand字段来聚合,看看有哪些品牌,每个品牌有多少数量。

聚合关键是弄清楚这几点:

  • 聚合的字段是什么
  • 聚合的类型是什么
  • 给聚合起个名

与查询类似,聚合条件通过 sourceBuilder.aggregation() 方法来设置,而参数是一个接口:

  • AggregationBuilder ,这个接口也有大量的实现类,代表不同的聚合种类。

同样也不需要自己去new,官方提供了一个工厂帮助创建实例:

在这里插入图片描述

示例:

    /**
     * 聚合
     */
    @Test
    public void testAgg() throws IOException{
        // 创建搜索对象
        SearchRequest request = new SearchRequest();
        // 指定查询的源
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 添加查询条件,通过QueryBuilders获取各种查询
        sourceBuilder.query(QueryBuilders.matchAllQuery());
        // 添加排序
        sourceBuilder.sort("price", SortOrder.ASC);
        // 配置size为0,因为不需要数据,只要聚合结果
        sourceBuilder.size(0);
        // 添加聚合
        sourceBuilder.aggregation(AggregationBuilders.terms("brandAgg").field("brand"));
        request.source(sourceBuilder);
        // 获取结果
        SearchResponse response = client.search(request);
        // 获取聚合结果
        Aggregations aggregations = response.getAggregations();
        // 获取某个聚合
        Terms terms = aggregations.get("brandAgg");
        // 获取桶
        for (Terms.Bucket bucket : terms.getBuckets()) {
            // 获取key,这里是品牌名称
            System.out.println("品牌 : " + bucket.getKeyAsString());
            // 获取docCount,就是数量
            System.out.println("count: " + bucket.getDocCount());
        }
    }

响应:

品牌 : 华为
count: 2
品牌 : 小米
count: 2
品牌 : 锤子
count: 1

还可以在聚合中添加子聚合

示例:

	/**
     * 聚合
     */
    @Test
    public void testAgg() throws IOException{
        // 创建搜索对象
        SearchRequest request = new SearchRequest();
        // 指定查询的源
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // 添加查询条件,通过QueryBuilders获取各种查询
        sourceBuilder.query(QueryBuilders.matchAllQuery());
        // 添加排序
        sourceBuilder.sort("price", SortOrder.ASC);
        // 配置size为0,因为不需要数据,只要聚合结果
        sourceBuilder.size(0);

        // 添加聚合
        TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("brandAgg").field("brand");

        // 添加子聚合
        termsAggregationBuilder.subAggregation(AggregationBuilders.avg("avgPrice").field("price"));

        sourceBuilder.aggregation(termsAggregationBuilder);

        request.source(sourceBuilder);
        // 获取结果
        SearchResponse response = client.search(request);
        // 获取聚合结果
        Aggregations aggregations = response.getAggregations();
        // 获取某个聚合
        Terms terms = aggregations.get("brandAgg");
        // 获取桶
        for (Terms.Bucket bucket : terms.getBuckets()) {
            // 获取key,这里是品牌名称
            System.out.println("品牌 : " + bucket.getKeyAsString());
            // 获取docCount,就是数量
            System.out.println("count: " + bucket.getDocCount());

            // 获取子聚合
            Avg avgPrice = bucket.getAggregations().get("avgPrice");
            System.out.println("均价:" + avgPrice.getValue());
        }
    }

响应:

品牌 : 华为
count: 2
均价:3649.0
品牌 : 小米
count: 2
均价:3799.0
品牌 : 锤子
count: 1
均价:3699.0

Spring Data Elasticsearch

Spring Data Elasticsearch 是Spring提供的elasticsearch组件

Spring Data Elasticsearch 简介

Spring Data Elasticsearch(以后简称SDE)是Spring Data项目下的一个子模块。

查看 Spring Data的官网:https://spring.io/projects/spring-data

在这里插入图片描述

Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如MySQL),还是
非关系数据库(如Redis),或者类似Elasticsearch这样的索引数据库。从而简化开发人员的代码,提
高开发效率。
包含很多不同数据操作的模块:

在这里插入图片描述

在这里插入图片描述

Spring Data Elasticsearch的页面:https://spring.io/projects/spring-data-elasticsearch

在这里插入图片描述

特征:

  • 支持Spring的基于 @Configuration 的Java配置方式,或者XML配置方式
  • 提供了用于操作ES的便捷工具类 ElasticsearchTemplate 。包括实现文档到POJO之间的自动智能映射。
  • 利用Spring的数据转换服务实现的功能丰富的对象映射
  • 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
  • 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现)。当然,也支持人工定制查询

版本关系:

在这里插入图片描述

注意:如果使用Spring Boot 2.3.X版本,Spring Data Elasticsearch 使用的是4.0.X,在4版本后很多API与ElasticSearch 6.X兼容性不是很好,所以要将Spring Boot的版本控制在2.1.X-2.2.X。


配置 Spring Data Elasticsearch

官方文档:https://docs.spring.io/spring-data/elasticsearch/docs/3.0.1.RELEASE/reference/html/#reference

集成步骤:

  1. 依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>
    </dependencies>	
    
  2. 配置文件中添加 ES 地址

    Spring Data Elasticsearch 已经配置好了各种SDE配置,并且注册了一个 ElasticsearchTemplate 以供使用

    ElasticsearchTemplate 底层使用的不是 Elasticsearch 提供的 RestHighLevelClient,而是 TransportClient,并不采用 Http 协议通信,而是访问 elasticsearch 对外开放的 tcp 端口,所以这里设置的端口是:9300 ,而不是9200

    spring:
      data:
        elasticsearch:
          # ES 集群名称
          cluster-name: elasticsearch
          # 这里使用的是TransportClient 连接的是TCP端口
          cluster-nodes: localhost:9300,localhost:9301,localhost:9302
    

索引库操作(ElasticsearchTemplate)

创建索引库

添加测试类,这里需要注意 SpringBoot 2.1.X 的测试类上需要添加 @RunWith(SpringRunner.class),注入ElasticsearchTemplate

@RunWith(SpringRunner.class)
@SpringBootTest
public class EsDemoApplicationTests {
    @Autowired
	private ElasticsearchTemplate template;
}    

准备一个实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "goods",type = "_doc",shards = 3, replicas = 1)
public class Goods {
    @Id
    private Long id;
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String title; //标题
    @Field(type = FieldType.Keyword)
    private String category;// 分类
    @Field(type = FieldType.Keyword)
    private String brand; // 品牌
    @Field(type = FieldType.Double)
    private Double price; // 价格
    @Field(type = FieldType.Keyword, index = false)
    private String images; // 图片地址
}

用到的注解的说明:

  • @Document:声明索引库配置
    • indexName:索引库名称
    • shards:分片数量,默认5
    • replicas:副本数量,默认1
  • @Id:声明实体类的id
  • @Field:声明字段属性
    • type:字段的数据类型
    • analyzer:指定分词器类型
    • index:是否创建索引

创建索引库

    /**
     * 创建索引
     */
    @Test
    void testCreateIndex(){
        boolean b = template.createIndex(Goods.class);
        System.out.println("结果为:"+b);
    }

若不加@Document注解,直接运行会报错,如下所示:

在这里插入图片描述

加@Document注解再次运行测试,可以成功创建索引,看一下索引信息

在这里插入图片描述


创建映射

@Id 和 @Filed 注解用于配置映射关系,使用 putMapping() 方法即可创建映射:

/**
 * 创建类型映射
 */
@Test
public void testCreateMapping() {
    boolean b = template.putMapping(Goods.class);
    System.out.println("结果为:" + b);
}

查看索引信息

在这里插入图片描述


新增文档

    @Autowired
	private ElasticsearchTemplate template;

    @Test
    public void add(){
        Goods goods = new Goods(1L,"小米手机10Pro","手机","小米",5999.00,"/images/123.jpg");
        IndexQuery query = new IndexQuery();
        query.setObject(goods);
        String index = template.index(query);
        System.out.println(index);
    }

ElasticsearchRepository 接口

ElasticsearchRepository封装了基本的CRUD方法,可以通过继承 ElasticsearchRepository 来使用:

public interface GoodsRepository extends ElasticsearchRepository<Goods,Long> {
}

ElasticsearchRepository<T, ID> 中的T对应类型,ID对应主键类型。


新增和更新文档:save

新增单个文档

// 保存文档
@Test
public void testSave() {
    Goods item = new Goods(6L, "小米手机10Pro", " 手机", "小米", 4699.00, "http://image.leyou.com/13123.jpg");
    goodsRepository.save(item);
}

更新文档

// 更新文档
@Test
public void testUpdate() {
    Goods item = new Goods(6L, "小米手机10Pro", " 手机", "小米", 4699.00, "http://image.leyou.com/13123.jpg");
    goodsRepository.save(item);
}

批量新增

@Test
// 批量保存文档
public void addDocuments() {
    // 准备文档数据:
    List<Goods> list = new ArrayList<>();
    list.add(new Goods(1L, "小米手机7", "手机", "小米", 3299.00, "/13123.jpg"));
    list.add(new Goods(2L, "坚果手机R1", "手机", "锤子", 3699.00, "/13123.jpg"));
    list.add(new Goods(3L, "华为META10", "手机", "华为", 4499.00, "/13123.jpg"));
    list.add(new Goods(4L, "小米Mix2S", "手机", "小米", 4299.00, "/13123.jpg"));
    list.add(new Goods(5L, "荣耀V10", "手机", "华为", 2799.00, "/13123.jpg"));
    // 添加索引数据
    goodsRepository.saveAll(list);
}

查看文档:findById

根据id查询

// 根据id查询
@Test
public void testQueryById(){
    Optional<Goods> goodsOptional = goodsRepository.findById(3L);
    System.out.println(goodsOptional.orElse(null));
}

删除文档:deleteById

根据id删除

// 删除文档
@Test
public void testDelete(){
    goodsRepository.deleteById(6L);
}

查询所有:findAll

// 查询所有
@Test
public void testQueryAll(){
    Iterable<Goods> list = goodsRepository.findAll();
    list.forEach(System.out::println);
}

自定义查询

GoodsRepository提供的查询方法有限,但是它却提供了非常强大的自定义查询功能:只要遵循SpringData提供的语法,就可以任意定义方法声明:

public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
    /**
     * 根据价格区间查询
     * @param from 开始价格
     * @param to 结束价格
     * @return 符合条件的goods
     */
    List<Goods> findByPriceBetween(double from, double to);
    List<Goods> findByTitle(String title);
    List<Goods> findByBrand(String brand);
}

使用实例:

// 范围查询
@Test
public void testConditionSearch(){
    List<Goods> list = goodsRepository.findByPriceBetween(3000, 4000);
    list.forEach(System.out::println);
}
@Test
public void testTitle(){
    // List<Goods> goods = goodsRepository.findByBrand("米");
    List<Goods> goods = goodsRepository.findByBrand("小米");
    goods.forEach(System.out::println);
}

支持的一些语法示例:

KeywordSampleElasticsearch Query String
AndfindByNameAndPrice{"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
OrfindByNameOrPrice{"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
IsfindByName{"bool" : {"must" : {"field" : {"name" : "?"}}}}
NotfindByNameNot{"bool" : {"must_not" : {"field" : {"name" : "?"}}}}
BetweenfindByPriceBetween{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
LessThanEqualfindByPriceLessThan{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
GreaterThanEqualfindByPriceGreaterThan{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
BeforefindByPriceBefore{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
AfterfindByPriceAfter{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
LikefindByNameLike{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}
StartingWithfindByNameStartingWith{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}
EndingWithfindByNameEndingWith{"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}}
Contains/ContainingfindByNameContaining{"bool" : {"must" : {"field" : {"name" : {"query" : "**?**","analyze_wildcard" : true}}}}}
InfindByNameIn(Collectionnames){"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}}
NotInfindByNameNotIn(Collectionnames){"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}}
NearfindByStoreNearNot Supported Yet !
TruefindByAvailableTrue{"bool" : {"must" : {"field" : {"available" : true}}}}
FalsefindByAvailableFalse{"bool" : {"must" : {"field" : {"available" : false}}}}
OrderByfindByAvailableTrueOrderByNameDesc{"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}}

条件查询:matchQuery

@Test
public void testSearch(){
    // QueryBuilder query = QueryBuilders.matchAllQuery();
    QueryBuilder query = QueryBuilders.matchQuery("title","小米");
    Iterable<Goods> goods = goodsRepository.search(query);
    goods.forEach(System.out::println);
}

分页查询:search

@Test
public void testPage(){
    QueryBuilder query = QueryBuilders.matchAllQuery();
    // 设置分页 page是从0开始
    PageRequest pageable = PageRequest.of(0, 2, Sort.by(Sort.Direction.DESC, "price"));
    Page<Goods> goodsPage = goodsRepository.search(query, pageable);
    System.out.println("总数:" + goodsPage.getTotalElements());
    List<Goods> goods = goodsPage.getContent();
    goods.forEach(System.out::println);
}

ElasticsearchTemplate 接口

SDE也支持使用 ElasticsearchTemplate 进行原生查询,而查询条件的构建是通过一个名为 NativeSearchQueryBuilder 的类来完成的,不过这个类的底层还是使用的原生API中的 QueryBuilders 、 AggregationBuilders 、 HighlightBuilders 等工具。

高亮

要支持高亮,必须自定义结果处理器来实现

import com.itheima.es.entity.Goods;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregations;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import java.util.ArrayList;
import java.util.List;

public class GoodsSearchResultMapper implements SearchResultMapper {
    @Override
    public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
        SearchHits searchHits = response.getHits();
        long total = searchHits.getTotalHits();
        float maxScore = searchHits.getMaxScore();
        // 定义content
        List<T> content = new ArrayList<>();
        SearchHit[] hits = searchHits.getHits();
        // 遍历文档
        for (SearchHit hit : hits) {
            // 获取json格式数据
            String sourceAsString = hit.getSourceAsString();
            // 转换称为对象
            Goods goods = JSON.parseObject(sourceAsString, Goods.class);
            // 解析高亮字段
            String title = hit.getHighlightFields().get("title").fragments()[0].string();
            // 替换原有的title
            goods.setTitle(title);
            content.add((T) goods);
        }
        Aggregations aggregations = response.getAggregations();
        String scrollId = response.getScrollId();

        return new AggregatedPageImpl(content,pageable,total,aggregations,scrollId,maxScore);
    }
}

查询时需要传入自定义结果处理器

	/**
     * 查询结果高亮处理
     */
    @Test
    public void testHighlight() {
        // 构建查询条件
        QueryBuilder queryBuilder = QueryBuilders.matchQuery("title", "小米");
        // 定义高亮条件
        HighlightBuilder.Field field = new HighlightBuilder.Field("title")
                .preTags("<em style='color:red'>")
                .postTags("</em>");
        // 构建查询条件并设置高亮
        SearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(queryBuilder)
                .withHighlightFields(field)
                .build();
        AggregatedPage<Goods> aggregatedPage = template.queryForPage(query, Goods.class, new GoodsSearchResultMapper());
        List<Goods> goods = aggregatedPage.getContent();
        goods.forEach(System.out::println);
    }

查看结果:

在这里插入图片描述


聚合

示例:

    /**
     * 聚合
     */
    @Test
    void testAgg() {
        // 针对品牌字段做分组
        AbstractAggregationBuilder agg = AggregationBuilders.terms("brandAgg").field("brand");
        // 添加子聚合来实现平均值的计算
        agg.subAggregation(AggregationBuilders.avg("priceAvg").field("price"));
        SearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.matchAllQuery())
                .addAggregation(agg)
                .build();
        AggregatedPage<Goods> aggregatedPage = template.queryForPage(query, Goods.class);
        // 获取到品牌的聚合
        Terms brandAgg = (Terms) aggregatedPage.getAggregation("brandAgg");
        for (Terms.Bucket bucket : brandAgg.getBuckets()) {
            System.out.println("品牌:" + bucket.getKeyAsString());
            System.out.println("数量:" + bucket.getDocCount());
            Avg priceAvg = bucket.getAggregations().get("priceAvg");
            System.out.println("均价:" + priceAvg.getValue());
        }
    }

结果:

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐