Elasticsearch入门教程-下

71 阅读26分钟

前期准备

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

配置客户端

@Configuration
public class RestClientConfig extends AbstractElasticsearchConfiguration {
    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo("116.205.231.12:9200")
                .build();
        return RestClients.create(clientConfiguration).rest();
    }
}

也可以直接在配置文件中配置

spring.elasticsearch.uris=116.205.231.12:9200

索引操作

创建索引

@Test
public void createIndex() throws IOException {
  // 创建索引 - 请求对象
  CreateIndexRequest request = new CreateIndexRequest("user");
  // 发送请求,获取响应
  CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
  boolean acknowledged = response.isAcknowledged();
  // 响应状态
  System.out.println("操作状态 = " + acknowledged);
}

操作结果:

image-20231029195917213

image-20231029195807347

代码分为三步:

  • 1)创建Request对象。
    • 因为是创建索引库的操作,因此Request是CreateIndexRequest
  • 2)添加请求参数
    • 其实就是Json格式的Mapping映射参数。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。
  • 3)发送请求
    • client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。例如创建索引、删除索引、判断索引是否存在等

查看索引

// 查询索引 - 请求对象
GetIndexRequest request = new GetIndexRequest("user");
// 发送请求,获取响应
GetIndexResponse response = client.indices().get(request,RequestOptions.DEFAULT);
System.out.println("aliases:"+response.getAliases());
System.out.println("mappings:"+response.getMappings());
System.out.println("settings:"+response.getSettings());

操作结果:

image-20231029200009699

删除索引

// 删除索引 - 请求对象
DeleteIndexRequest request = new DeleteIndexRequest("user");
// 发送请求,获取响应
AcknowledgedResponse response = client.indices().delete(request,RequestOptions.DEFAULT);
// 操作结果
System.out.println("操作结果 : " + response.isAcknowledged());

操作结果:

image-20231029200811442

文档操作

新增文档

创建数据模型

@Data
public class User {
 	private String name;
	private Integer age;
	private String sex;
}

创建数据,添加到文档中

// 新增文档 - 请求对象
IndexRequest request = new IndexRequest();

// 设置索引及唯一性标识
request.index("user").id("1001");

// 创建数据对象
User user = new User();
user.setName("zhangsan");
user.setAge(30);
user.setSex("男");
ObjectMapper objectMapper = new ObjectMapper();
String productJson = objectMapper.writeValueAsString(user);

// 添加文档数据,数据格式为 JSON 格式
request.source(productJson,XContentType.JSON);

// 客户端发送请求,获取响应对象
IndexResponse response = client.index(request, RequestOptions.DEFAULT);

////3.打印结果信息
System.out.println("_index:" + response.getIndex());
System.out.println("_id:" + response.getId());
System.out.println("_result:" + response.getResult());

操作结果:

image-20231029201321939

代码示例图:

image-20231029201705236

可以看到与索引库操作的API非常类似,同样是三步走:

  • 1)创建Request对象,这里是IndexRequest,因为添加文档就是创建倒排索引的过程
  • 2)准备请求参数,本例中就是Json文档
  • 3)发送请求

修改文档

// 修改文档 - 请求对象
UpdateRequest request = new UpdateRequest();

// 配置修改参数
request.index("user").id("1001");

// 设置请求体,对数据进行修改
request.doc(XContentType.JSON, "sex", "女");

// 客户端发送请求,获取响应对象
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);

System.out.println("_index:" + response.getIndex());
System.out.println("_id:" + response.getId());
System.out.println("_result:" + response.getResult());

执行结果:

image-20231029201514426

代码示例如图:

image-20231029201537477

与之前类似,也是三步走:

  • 1)准备Request对象。这次是修改,所以是UpdateRequest
  • 2)准备参数。也就是JSON文档,里面包含要修改的字段
  • 3)更新文档。这里调用client.update()方法

查询文档

//1.创建请求对象
GetRequest request = new GetRequest().index("user").id("1001");

//2.客户端发送请求,获取响应对象
GetResponse response = client.get(request, RequestOptions.DEFAULT);

////3.打印结果信息
System.out.println("_index:" + response.getIndex());
System.out.println("_type:" + response.getType());
System.out.println("_id:" + response.getId());
System.out.println("source:" + response.getSourceAsString());

执行结果为:

image-20231029201824550

代码示例图:

image-20231029201901776

可以看到,响应结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,反序列化为Java对象即可。

其它代码与之前类似,流程如下:

  • 1)准备Request对象。这次是查询,所以是GetRequest
  • 2)发送请求,得到结果。因为是查询,这里调用client.get()方法
  • 3)解析结果,就是对JSON做反序列化

删除文档

//创建请求对象
DeleteRequest request = new DeleteRequest().index("user").id("1");

//客户端发送请求,获取响应对象
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);

//打印信息
System.out.println(response.toString());

执行结果为:

image-20231029202008287

批量操作

  • 批量新增:
//创建批量新增请求对象
BulkRequest request = new BulkRequest();

request.add(new IndexRequest().index("user").id("1001").source(XContentType.JSON, "name","zhangsan"));
request.add(new IndexRequest().index("user").id("1002").source(XContentType.JSON, "name","lisi"));
request.add(new IndexRequest().index("user").id("1003").source(XContentType.JSON, "name","wangwu"));

//客户端发送请求,获取响应对象
BulkResponse responses = client.bulk(request, RequestOptions.DEFAULT);

//打印结果信息
System.out.println("took:" + responses.getTook());
System.out.println("items:" + responses.getItems());

执行结果为:

image-20231029202145942

  • 批量删除:
//创建批量删除请求对象
BulkRequest request = new BulkRequest();

request.add(new DeleteRequest().index("user").id("1001"));
request.add(new DeleteRequest().index("user").id("1002"));
request.add(new DeleteRequest().index("user").id("1003"));

//客户端发送请求,获取响应对象
BulkResponse responses = client.bulk(request, RequestOptions.DEFAULT);

//打印结果信息
System.out.println("took:" + responses.getTook());
System.out.println("items:" + responses.getItems());

执行结果为:

image-20231029202224960

DSL查询

查询所有索引数据

// 创建搜索请求对象
SearchRequest request = new SearchRequest();
request.indices("student");

// 构建查询的请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

// 查询所有数据
sourceBuilder.query(QueryBuilders.matchAllQuery());
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 查询匹配
SearchHits hits = response.getHits();

System.out.println("took:" + response.getTook());
System.out.println("timeout:" + response.isTimedOut());
System.out.println("total:" + hits.getTotalHits());
System.out.println("MaxScore:" + hits.getMaxScore());
System.out.println("hits========>>");

for (SearchHit hit : hits) {
//输出每条查询的结果信息
System.out.println(hit.getSourceAsString());
}
System.out.println("<<========");

操作结果:

image-20231029202711865

代码示例图:

image-20231029202749747

代码解读:

  • 第一步,创建SearchRequest对象,指定索引库名
  • 第二步,利用request.source()构建DSL,DSL中可以包含查询、分页、排序、高亮等
  • query():代表查询条件,利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL
  • 第三步,利用client.search()发送请求,得到响应

这里关键的API有两个,一个是request.source(),它构建的就是DSL中的完整JSON参数。其中包含了querysortfromsizehighlight等所有功能:

image-20231029202838841

另一个是QueryBuilders,其中包含了我们学习过的各种叶子查询复合查询等:

image-20231029202905587

因此,我们解析SearchResponse的代码就是在解析这个JSON结果,对比如下:

image-20231029202956747

代码解读

elasticsearch返回的结果是一个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文档数据

其他查询

所有的查询条件都是由QueryBuilders来构建的,叶子查询也不例外。因此整套代码中变化的部分仅仅是query条件构造的方式,其它不动。

例如match查询:

@Test
void testMatch() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

private void handleResponse(SearchResponse response) {
    SearchHits searchHits = response.getHits();
    // 1.获取总条数
    long total = searchHits.getTotalHits().value;
    System.out.println("共搜索到" + total + "条数据");
    // 2.遍历结果数组
    SearchHit[] hits = searchHits.getHits();
    for (SearchHit hit : hits) {
        // 3.得到_source,也就是原始json文档
        String source = hit.getSourceAsString();
        // 4.反序列化并打印
        ItemDTO item = JSONUtil.toBean(source, ItemDTO.class);
        System.out.println(item);
    }
}

再比如multi_match查询:

@Test
void testMultiMatch() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

term查询

// 创建搜索请求对象
SearchRequest request = new SearchRequest();
request.indices("student");

// 构建查询的请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.termQuery("age", "30"));
request.source(sourceBuilder);

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

// 查询匹配
SearchHits hits = response.getHits();

System.out.println("took:" + response.getTook());
System.out.println("timeout:" + response.isTimedOut());
System.out.println("total:" + hits.getTotalHits());
System.out.println("MaxScore:" + hits.getMaxScore());
System.out.println("hits========>>");

for (SearchHit hit : hits) {
//输出每条查询的结果信息
System.out.println(hit.getSourceAsString());
}
System.out.println("<<========");

执行结果:

image-20231029203550970

分页查询

// 创建搜索请求对象
SearchRequest request = new SearchRequest();
request.indices("student");

// 构建查询的请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());

// 分页查询

// 当前页其实索引(第一条数据的顺序号),from
sourceBuilder.from(0);

// 每页显示多少条 size
sourceBuilder.size(2);
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 查询匹配
SearchHits hits = response.getHits();

System.out.println("took:" + response.getTook());
System.out.println("timeout:" + response.isTimedOut());
System.out.println("total:" + hits.getTotalHits());
System.out.println("MaxScore:" + hits.getMaxScore());
System.out.println("hits========>>");

for (SearchHit hit : hits) {
//输出每条查询的结果信息
System.out.println(hit.getSourceAsString());
}

System.out.println("<<========");

执行结果为:

image-20231029203712214

代码示例图:

image-20231029203738425

数据排序

// 构建查询的请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());

// 排序
sourceBuilder.sort("age", SortOrder.ASC);
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 查询匹配
SearchHits hits = response.getHits();

System.out.println("took:" + response.getTook());
System.out.println("timeout:" + response.isTimedOut());
System.out.println("total:" + hits.getTotalHits());
System.out.println("MaxScore:" + hits.getMaxScore());
System.out.println("hits========>>");

for (SearchHit hit : hits) {
//输出每条查询的结果信息
System.out.println(hit.getSourceAsString());
}
System.out.println("<<========");

执行结果为:

image-20231029203849371

过滤字段

// 创建搜索请求对象
SearchRequest request = new SearchRequest();
request.indices("student");

// 构建查询的请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchAllQuery());

//查询字段过滤
String[] excludes = {};
String[] includes = {"name", "age"};
sourceBuilder.fetchSource(includes, excludes);
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 查询匹配
SearchHits hits = response.getHits();

System.out.println("took:" + response.getTook());
System.out.println("timeout:" + response.isTimedOut());
System.out.println("total:" + hits.getTotalHits());
System.out.println("MaxScore:" + hits.getMaxScore());
System.out.println("hits========>>");

for (SearchHit hit : hits) {
//输出每条查询的结果信息
System.out.println(hit.getSourceAsString());
}
System.out.println("<<========");

执行结果为:

image-20231029203939640

Bool查询

// 创建搜索请求对象
SearchRequest request = new SearchRequest();
request.indices("student");

// 构建查询的请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

// 必须包含
boolQueryBuilder.must(QueryBuilders.matchQuery("age", "30"));

// 一定不含
boolQueryBuilder.mustNot(QueryBuilders.matchQuery("name", "zhangsan"));

// 可能包含
boolQueryBuilder.should(QueryBuilders.matchQuery("sex", "男"));

sourceBuilder.query(boolQueryBuilder);
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 查询匹配
SearchHits hits = response.getHits();

System.out.println("took:" + response.getTook());
System.out.println("timeout:" + response.isTimedOut());
System.out.println("total:" + hits.getTotalHits());
System.out.println("MaxScore:" + hits.getMaxScore());
System.out.println("hits========>>");

for (SearchHit hit : hits) {
//输出每条查询的结果信息
System.out.println(hit.getSourceAsString());
}
System.out.println("<<========");

执行结果为:

image-20231029204040601

代码示例图:

image-20231029204104711

范围查询

// 创建搜索请求对象
SearchRequest request = new SearchRequest();
request.indices("student");

// 构建查询的请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("age");

// 大于等于
rangeQuery.gte("30");

// 小于等于
rangeQuery.lte("40");

sourceBuilder.query(rangeQuery);
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 查询匹配

SearchHits hits = response.getHits();

System.out.println("took:" + response.getTook());
System.out.println("timeout:" + response.isTimedOut());
System.out.println("total:" + hits.getTotalHits());
System.out.println("MaxScore:" + hits.getMaxScore());
System.out.println("hits========>>");

for (SearchHit hit : hits) {
//输出每条查询的结果信息
System.out.println(hit.getSourceAsString());
}
System.out.println("<<========");

执行结果为:

image-20231029204212936

模糊查询

// 创建搜索请求对象
SearchRequest request = new SearchRequest();
request.indices("student");

// 构建查询的请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

sourceBuilder.query(QueryBuilders.fuzzyQuery("name","zhangsan").fuzziness(Fuzziness.ONE));
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// 查询匹配
SearchHits hits = response.getHits();

System.out.println("took:" + response.getTook());
System.out.println("timeout:" + response.isTimedOut());
System.out.println("total:" + hits.getTotalHits());
System.out.println("MaxScore:" + hits.getMaxScore());
System.out.println("hits========>>");

for (SearchHit hit : hits) {
//输出每条查询的结果信息
System.out.println(hit.getSourceAsString());

}

System.out.println("<<========");

执行结果为:

image-20231029204316080

高亮查询

// 高亮查询
SearchRequest request = new SearchRequest().indices("student");

//2.创建查询请求体构建器
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

//构建查询方式:高亮查询
TermsQueryBuilder termsQueryBuilder =QueryBuilders.termsQuery("name","zhangsan");

//设置查询方式
sourceBuilder.query(termsQueryBuilder);

//构建高亮字段
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("<font color='red'>");//设置标签前缀
highlightBuilder.postTags("</font>");//设置标签后缀
highlightBuilder.field("name");//设置高亮字段

//设置高亮构建对象
sourceBuilder.highlighter(highlightBuilder);

//设置请求体
request.source(sourceBuilder);

//3.客户端发送请求,获取响应对象
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

//4.打印响应结果
SearchHits hits = response.getHits();

System.out.println("took::"+response.getTook());
System.out.println("time_out::"+response.isTimedOut());
System.out.println("total::"+hits.getTotalHits());
System.out.println("max_score::"+hits.getMaxScore());
System.out.println("hits::::>>");

for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
System.out.println(sourceAsString);
//打印高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
System.out.println(highlightFields);

}
System.out.println("<<::::");

执行结果为:

image-20231029204457776

高亮查询与前面的查询有两点不同:

  • 条件同样是在request.source()中指定,只不过高亮条件要基于HighlightBuilder来构造
  • 高亮响应结果与搜索的文档结果不在一起,需要单独解析

首先来看高亮条件构造,其DSL和JavaAPI的对比如图:

image-20231029204528984
@Test
void testHighlight() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    // 2.1.query条件
    request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 2.2.高亮条件
    request.source().highlighter(
            SearchSourceBuilder.highlight()
                    .field("name")
                    .preTags("<em>")
                    .postTags("</em>")
    );
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

再来看结果解析,文档解析的部分不变,主要是高亮内容需要单独解析出来,其DSL和JavaAPI的对比如图:

image-20231029204707940

代码解读:

  • 3、4步:从结果中获取_sourcehit.getSourceAsString(),这部分是非高亮结果,json字符串。还需要反序列为ItemDTO对象
  • 5步:获取高亮结果。hit.getHighlightFields(),返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值
  • 5.1步:从Map中根据高亮字段名称,获取高亮字段值对象HighlightField
  • 5.2步:从HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了
  • 最后:用高亮的结果替换ItemDTO中的非高亮结果

聚合查询

  • 最大年龄

// 高亮查询
SearchRequest request = new SearchRequest().indices("student");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(AggregationBuilders.max("maxAge").field("age"));

//设置请求体
request.source(sourceBuilder);

//3.客户端发送请求,获取响应对象
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

//4.打印响应结果
SearchHits hits = response.getHits();

System.out.println(response);

执行结果为:

image-20231029204831069

可以看到在DSL中,aggs聚合条件与query条件是同一级别,都属于查询JSON参数。因此依然是利用request.source()方法来设置。

不过聚合条件的要利用AggregationBuilders这个工具类来构造。DSL与JavaAPI的语法对比如下:

image-20231029204858492

聚合结果与搜索文档同一级别,因此需要单独获取和解析。具体解析语法如下:

image-20231029204946252
  • 分组统计

// 高亮查询
SearchRequest request = new SearchRequest().indices("student");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(AggregationBuilders.terms("age_groupby").field("age"));

//设置请求体
request.source(sourceBuilder);

//3.客户端发送请求,获取响应对象
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

//4.打印响应结果
SearchHits hits = response.getHits();
System.out.println(response);

执行结果为:

image-20231029205301535

客户端对象

  • ElasticsearchOperations
  • RestHighLevelClient 推荐

ElasticsearchOperations

相关注解

@Document(indexName = "products", createIndex = true)
public class Product {
    @Id
    private Integer id;
    @Field(type = FieldType.Keyword)
    private String title;
    @Field(type = FieldType.Float)
    private Double price;
    @Field(type = FieldType.Text)
    private String description;
  	//get set ...
}
//1. @Document(indexName = "products", createIndex = true) 用在类上 作用:代表一个对象为一个文档
		-- indexName属性: 创建索引的名称
    -- createIndex属性: 是否创建索引
//2. @Id 用在属性上  作用:将对象id字段与ES中文档的_id对应
//3. @Field(type = FieldType.Keyword) 用在属性上 作用:用来描述属性在ES中存储类型以及分词情况
    -- type: 用来指定字段类型

索引文档

@Autowired
private ElasticsearchOperations elasticsearchOperations;

@Test
public void testCreate() throws IOException {
  Product product = new Product();
  product.setId(1); //存在id指定id  不存在id自动生成id
  product.setTitle("怡宝矿泉水");
  product.setPrice(129.11);
  product.setDescription("我们喜欢喝矿泉水....");
  elasticsearchOperations.save(product);
}

删除文档

@Test
public void testDelete() {
  Product product = new Product();
  product.setId(1);
  String delete = elasticsearchOperations.delete(product);
  System.out.println(delete);
}

查询文档

@Test
public void testGet() {
  Product product = elasticsearchOperations.get("1", Product.class);
  System.out.println(product);
}

更新文档

 @Test
public void testUpdate() {
  Product product = new Product();
  product.setId(1);
  product.setTitle("怡宝矿泉水");
  product.setPrice(129.11);
  product.setDescription("我们喜欢喝矿泉水,你们喜欢吗....");
  elasticsearchOperations.save(product);//不存在添加,存在更新
}

删除所有

@Test
public void testDeleteAll() {
  elasticsearchOperations.delete(Query.findAll(), Product.class);
}

查询所有

@Test
public void testFindAll() {
  SearchHits<Product> productSearchHits = elasticsearchOperations.search(Query.findAll(), Product.class);
  productSearchHits.forEach(productSearchHit -> {
    System.out.println("id: " + productSearchHit.getId());
    System.out.println("score: " + productSearchHit.getScore());
    Product product = productSearchHit.getContent();
    System.out.println("product: " + product);
  });
}

RestHighLevelClient

创建索引映射

 @Test
    public void testCreateIndex() throws IOException {
        CreateIndexRequest createIndexRequest = new CreateIndexRequest("fruit");
        createIndexRequest.mapping("{\n" +
                "    \"properties\": {\n" +
                "      \"title\":{\n" +
                "        \"type\": \"keyword\"\n" +
                "      },\n" +
                "      \"price\":{\n" +
                "        \"type\": \"double\"\n" +
                "      },\n" +
                "      \"created_at\":{\n" +
                "        \"type\": \"date\"\n" +
                "      },\n" +
                "      \"description\":{\n" +
                "        \"type\": \"text\"\n" +
                "      }\n" +
                "    }\n" +
                "  }\n" , XContentType.JSON);
        CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
        System.out.println(createIndexResponse.isAcknowledged());
        restHighLevelClient.close();
    }

索引文档

@Test
public void testIndex() throws IOException {
  IndexRequest indexRequest = new IndexRequest("fruit");
  indexRequest.source("{\n" +
                      "          \"id\" : 1,\n" +
                      "          \"title\" : \"蓝月亮\",\n" +
                      "          \"price\" : 123.23,\n" +
                      "          \"description\" : \"这个洗衣液非常不错哦!\"\n" +
                      "        }",XContentType.JSON);
  IndexResponse index = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
  System.out.println(index.status());
}

更新文档

@Test
public void testUpdate() throws IOException {
  UpdateRequest updateRequest = new UpdateRequest("fruit","qJ0R9XwBD3J1IW494-Om");
  updateRequest.doc("{\"title\":\"好月亮\"}",XContentType.JSON);
  UpdateResponse update = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
  System.out.println(update.status());
}

删除文档

@Test
public void testDelete() throws IOException {
  DeleteRequest deleteRequest = new DeleteRequest("fruit","1");
  DeleteResponse delete = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
  System.out.println(delete.status());
}

基于 id 查询文档

@Test
public void testGet() throws IOException {
  GetRequest getRequest = new GetRequest("fruit","1");
  GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
  System.out.println(getResponse.getSourceAsString());
}

查询所有

 @Test
public void testSearch() throws IOException {
  SearchRequest searchRequest = new SearchRequest("fruit");
  SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  sourceBuilder.query(QueryBuilders.matchAllQuery());
  searchRequest.source(sourceBuilder);
  SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
  //System.out.println(searchResponse.getHits().getTotalHits().value);
  SearchHit[] hits = searchResponse.getHits().getHits();
  for (SearchHit hit : hits) {
    System.out.println(hit.getSourceAsString());
  }
}

综合查询

 @Test
public void testSearch() throws IOException {
  SearchRequest searchRequest = new SearchRequest("fruit");
  SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  sourceBuilder
    .from(0)
    .size(2)
    .sort("price", SortOrder.DESC)
    .fetchSource(new String[]{"title"},new String[]{})
    .highlighter(new HighlightBuilder().field("description").requireFieldMatch(false).preTags("<span style='color:red;'>").postTags("</span>"))
    .query(QueryBuilders.termQuery("description","错"));
  searchRequest.source(sourceBuilder);
  SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
  System.out.println("总条数: "+searchResponse.getHits().getTotalHits().value);
  SearchHit[] hits = searchResponse.getHits().getHits();
  for (SearchHit hit : hits) {
    System.out.println(hit.getSourceAsString());
    Map<String, HighlightField> highlightFields = hit.getHighlightFields();
    highlightFields.forEach((k,v)-> System.out.println("key: "+k + " value: "+v.fragments()[0]));
  }
}

ElasticsearchRepository

Spring Data 的 Repository 接口提供了一种声明式的数据操作规范,无序编写任何代码,只需遵循 Spring Data 的方法定义规范即可完成数据的 CRUD 操作。

ElasticsearchRepository 继承自 Repository,其中已经预定义了基本的 CURD 方法,我们可以通过继承 ElasticsearchRepository,添加自定义的数据操作方法。

Repository 方法命名规范

自定义数据操作方法需要遵循 Repository 规范,示例如下:

关键词方法名es查询
AndfindByNameAndPrice{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }}
OrfindByNameOrPrice{ "query" : { "bool" : { "should" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }}
IsfindByName{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }}
NotfindByNameNot{ "query" : { "bool" : { "must_not" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }}
BetweefindByPriceBetween{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }}
LessThanfindByPriceLessThan{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : false } } } ] } }}
LessThanEquafindByPriceLessThanEqual{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }}
GreaterThanfindByPriceGreaterThan{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : false, "include_upper" : true } } } ] } }}
GreaterThanEqualfindByPriceGreaterThan{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }}
BeforefindByPriceBefore{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }}
AfterfindByPriceAfter{ "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }}
LikefindByNameLike{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
StartingWithfindByNameStartingWit{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
EndingWithfindByNameEndingWith{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
Contains/ContainingfindByNameContaining{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
In (when annotated as FieldType.Keyword)findByNameIn(Collection<String>names){ "query" : { "bool" : { "must" : [ {"bool" : {"must" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }}
InfindByNameIn(Collection<String>names){ "query": {"bool": {"must": [{"query_string":{"query": ""?" "?"", "fields": ["name"]}}]}}}
NotIn (when annotated as FieldType.Keyword)findByNameNotIn(Collection<String>names){ "query" : { "bool" : { "must" : [ {"bool" : {"must_not" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }}
NotInfindByNameNotIn(Collection<String>names){"query": {"bool": {"must": [{"query_string": {"query": "NOT("?" "?")", "fields": ["name"]}}]}}}
TruefindByAvailableTru`{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }}
FalsefindByAvailableFals{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "false", "fields" : [ "available" ] } } ] } }}
OrderByfindByAvailableTrueOrderByNameDesc{ "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }, "sort":[{"name":{"order":"desc"}}] }
ExistsfindByNameExists{"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}}
IsNullfindByNameIsNull{"query":{"bool":{"must_not":[{"exists":{"field":"name"}}]}}}
IsNotNullfindByNameIsNotNull{"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}}
IsEmptyfindByNameIsEmpt{"query":{"bool":{"must":[{"bool":{"must":[{"exists":{"field":"name"}}],"must_not":[{"wildcard":{"name":{"wildcard":"*"}}}]}}]}}}
IsNotEmptyfindByNameIsNotEmpty{"query":{"bool":{"must":[{"wildcard":{"name":{"wildcard":"*"}}}]}}

测试学生数据的 CRUD 操作

package cn.tedu.es.repo;

import cn.tedu.es.entity.Student;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.List;

/**
 * 只需要定义接口,继承 ElasticsearchRepository,基础增删改查方法,在父接口中已经提供
 *
 * spring data 的 Repository 数据访问规范,只要是 Repository 的子类型,都不需要自己写代码,也不需要添加任何注解
 */
public interface StudentRepository extends ElasticsearchRepository<Student, Long> {
    //在name字段中查找关键字
    List<Student> findByName(String name);
    //在name字段中搜索关键词,或者birthDate字段匹配日期
    List<Student> findByNameOrBirthDate(String name, String birthDate);
}

添加测试类,对学生数据进行 CRUD 测试

package cn.tedu.es;

import cn.tedu.es.entity.Student;
import cn.tedu.es.repo.StudentRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;
import java.util.Optional;

@SpringBootTest
public class Test1 {

    @Autowired
    private StudentRepository repository;

    @Test
    public void test1(){
        //在 es 服务器的 student索引中保存学生
        repository.save(new Student(9527L,"唐伯虎",'男',"2021-11-12"));
        repository.save(new Student(9528L,"华夫人",'女',"2021-11-12"));
        repository.save(new Student(9529L,"祝枝山",'男',"2021-11-12"));
        repository.save(new Student(9530L,"小强",'男',"2021-11-12"));
        repository.save(new Student(9531L,"旺财",'男',"2021-11-12"));
        repository.save(new Student(9532L,"如花",'女',"2021-11-12"));
    }

    @Test
    public void test2(){
        repository.save(new Student(9533L,"华太师",'男',"2020-11-20"));
    }

    @Test
    public void test3(){
        Optional<Student> stu = repository.findById(9527L);

        if(stu.isPresent()){ //Optional对象中是否存在Student对象
            System.out.println(stu);
        }
        System.out.println("------------------------------------");

        Iterable<Student> it = repository.findAll();
        for (Student s : it){
            System.out.println(s);
        }
    }

    @Test
    public void test4(){
        repository.deleteById(9531L);
    }

    @Test
    public void test5(){
        List<Student> list = repository.findByName("唐");
        for(Student s :list){
            System.out.println(s);
        }
    }

    @Test
    public void test6(){
        List<Student> list = repository.findByNameOrBirthDate("唐", "2021-11-12");
        for(Student s :list){
            System.out.println(s);
        }
    }
}

DAO 数据访问对象

@Repository
public interface ProductDao extends ElasticsearchRepository<Product,Long> {

}

实体类映射操作

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Document(indexName = "shopping", shards = 3, replicas = 1)
public class Product {

//必须有 id,这里的 id 是全局唯一的标识,等同于 es 中的"_id"
@Id
private Long id;//商品唯一标识

/**
* type : 字段数据类型
* analyzer : 分词器类型
* index : 是否索引(默认:true)
* Keyword : 短语,不进行分词
*/

@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;//商品名称

@Field(type = FieldType.Keyword)
private String category;//分类名称

@Field(type = FieldType.Double)
private Double price;//商品价格

@Field(type = FieldType.Keyword, index = false)
private String images;//图片地址

}

文档操作

public class SpringDataESProductDaoTest {

	@Autowired
	private ProductDao productDao;

/**
* 新增
*/

@Test
public void save(){

		Product product = new Product();
		product.setId(2L);
		product.setTitle("华为手机");
		product.setCategory("手机");
		product.setPrice(2999.0);
		product.setImages("http://www.atguigu/hw.jpg");
		productDao.save(product);
}

//修改
@Test
public void update(){
  
Product product = new Product();
product.setId(1L);
product.setTitle("小米 2 手机");
product.setCategory("手机");
product.setPrice(9999.0);
product.setImages("http://www.atguigu/xm.jpg");
productDao.save(product);

}

//根据 id 查询
@Test
public void findById(){

Product product = productDao.findById(1L).get();
System.out.println(product);
}
  
  //查询所有
  @Test

public void findAll(){

Iterable<Product> products = productDao.findAll();
	for (Product product : products) {
			System.out.println(product);
	}
}
  
  //删除
@Test
public void delete(){

Product product = new Product();
product.setId(1L);
productDao.delete(product);

}
  
  //批量新增

@Test

public void saveAll(){

List<Product> productList = new ArrayList<>();
		for (int i = 0; i < 10; i++) {

		Product product = new Product();
		product.setId(Long.valueOf(i));
		product.setTitle("["+i+"]小米手机");
		product.setCategory("手机");
		product.setPrice(1999.0+i);
		product.setImages("http://www.atguigu/xm.jpg");
		productList.add(product);
	}
productDao.saveAll(productList);
}
  
  //分页查询

@Test

public void findByPageable(){

//设置排序(排序方式,正序还是倒序,排序的 id)
Sort sort = Sort.by(Sort.Direction.DESC,"id");
int currentPage=0;//当前页,第一页从 0 开始,1 表示第二页
int pageSize = 5;//每页显示多少条

//设置查询分页
PageRequest pageRequest = PageRequest.of(currentPage, pageSize,sort);

//分页查询
Page<Product> productPage = productDao.findAll(pageRequest);
for (Product Product : productPage.getContent()) {

		System.out.println(Product);

	}

}

自动补全

当用户在搜索框输入字符时,我们应该提示出与该字符有关的搜索项,如图:

image-20231029220023222

这种根据用户输入的字母,提示完整词条的功能,就是自动补全了。

因为需要根据拼音字母来推断,因此要用到拼音分词功能。

拼音分词器

要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。

地址:github.com/medcl/elast…

安装方式与IK分词器一样,分三步:

  • ①解压

  • ②上传到虚拟机中,elasticsearch的plugin目录

  • ③重启elasticsearch

  • ④测试

详细安装步骤可以参考IK分词器的安装过程。

测试用法如下:

POST /_analyze
{
  "text": "如家酒店还不错",
  "analyzer": "pinyin"
}

结果:

image-20231029220118789

自定义分词器

默认的拼音分词器会将每个汉字单独分为拼音,而我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。

elasticsearch中分词器(analyzer)的组成包含三部分:

  • character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
  • tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
  • tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等

文档分词时会依次由这三部分来处理文档:

image-20231029220154823

声明自定义分词器的语法如下:

PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": { // 自定义分词器
        "my_analyzer": {  // 分词器名称
          "tokenizer": "ik_max_word",
          "filter": "py"
        }
      },
      "filter": { // 自定义tokenizer filter
        "py": { // 过滤器名称
          "type": "pinyin", // 过滤器类型,这里是pinyin
		  "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

测试:

image-20231029220235636

自动补全查询

elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:

  • 参与补全查询的字段必须是completion类型。

  • 字段的内容一般是用来补全的多个词条形成的数组。

比如,一个这样的索引库:

// 创建索引库
PUT test
{
  "mappings": {
    "properties": {
      "title":{
        "type": "completion"
      }
    }
  }
}

然后插入下面的数据:

// 示例数据
POST test/_doc
{
  "title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
  "title": ["SK-II", "PITERA"]
}
POST test/_doc
{
  "title": ["Nintendo", "switch"]
}

查询的DSL语句如下:

// 自动补全查询
GET /test/_search
{
  "suggest": {
    "title_suggest": {
      "text": "s", // 关键字
      "completion": {
        "field": "title", // 补全查询的字段
        "skip_duplicates": true, // 跳过重复的
        "size": 10 // 获取前10条结果
      }
    }
  }
}

自动补全API

@Override
public List<String> getSuggestions(String prefix) {
    try {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        request.source().suggest(new SuggestBuilder().addSuggestion(
            "suggestions",
            SuggestBuilders.completionSuggestion("suggestion")
            .prefix(prefix)
            .skipDuplicates(true)
            .size(10)
        ));
        // 3.发起请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        Suggest suggest = response.getSuggest();
        // 4.1.根据补全查询名称,获取补全结果
        CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
        // 4.2.获取options
        List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
        // 4.3.遍历
        List<String> list = new ArrayList<>(options.size());
        for (CompletionSuggestion.Entry.Option option : options) {
            String text = option.getText().toString();
            list.add(text);
        }
        return list;
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

image-20231029220528478

image-20231029220534850

数据同步

elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步

思路分析

常见的数据同步方案有三种:

  • 同步调用
  • 异步通知
  • 监听binlog

同步调用

方案一:同步调用

image-20231029220700844

基本步骤如下:

  • hotel-demo对外提供接口,用来修改elasticsearch中的数据
  • 酒店管理服务在完成数据库操作后,直接调用hotel-demo提供的接口

异步通知

方案二:异步通知

image-20231029220739749

流程如下:

  • hotel-admin对mysql数据库数据完成增、删、改后,发送MQ消息
  • hotel-demo监听MQ,接收到消息后完成elasticsearch数据修改

监听binlog

方案三:监听binlog

image-20231029220759203

流程如下:

  • 给mysql开启binlog功能
  • mysql完成增、删、改操作都会记录在binlog中
  • hotel-demo基于canal监听binlog变化,实时更新elasticsearch中的内容

选择

方式一:同步调用

  • 优点:实现简单,粗暴
  • 缺点:业务耦合度高

方式二:异步通知

  • 优点:低耦合,实现难度一般
  • 缺点:依赖mq的可靠性

方式三:监听binlog

  • 优点:完全解除服务间耦合
  • 缺点:开启binlog增加数据库负担、实现复杂度高

实现数据同步

步骤:

  • 导入课前资料提供的hotel-admin项目,启动并测试酒店数据的CRUD

  • 声明exchange、queue、RoutingKey

  • 在hotel-admin中的增、删、改业务中完成消息发送

  • 在hotel-demo中完成消息监听,并更新elasticsearch中数据

  • 启动并测试数据同步功能

demo

image-20231029221025840

其中包含了酒店的CRUD功能:

image-20231029221042849

声明交换机、队列

MQ结构如图:

image-20231029221059248

  • 1)引入依赖

在hotel-admin、hotel-demo中引入rabbitmq的依赖:

<!--amqp-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • 2)声明队列交换机名称

在hotel-admin和hotel-demo中的cn.itcast.hotel.constatnts包下新建一个类MqConstants

package cn.itcast.hotel.constatnts;

    public class MqConstants {
    /**
     * 交换机
     */
    public final static String HOTEL_EXCHANGE = "hotel.topic";
    /**
     * 监听新增和修改的队列
     */
    public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
    /**
     * 监听删除的队列
     */
    public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
    /**
     * 新增或修改的RoutingKey
     */
    public final static String HOTEL_INSERT_KEY = "hotel.insert";
    /**
     * 删除的RoutingKey
     */
    public final static String HOTEL_DELETE_KEY = "hotel.delete";
}
  • 3)声明队列交换机

在hotel-demo中,定义配置类,声明队列、交换机:

package cn.itcast.hotel.config;

import cn.itcast.hotel.constants.MqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MqConfig {
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
    }

    @Bean
    public Queue insertQueue(){
        return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
    }

    @Bean
    public Queue deleteQueue(){
        return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
    }

    @Bean
    public Binding insertQueueBinding(){
        return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
    }

    @Bean
    public Binding deleteQueueBinding(){
        return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
    }
}

发送MQ消息

在hotel-admin中的增、删、改业务中分别发送MQ消息:

image-20231029221202378

接收MQ消息

hotel-demo接收到MQ消息要做的事情包括:

  • 新增消息:根据传递的hotel的id查询hotel信息,然后新增一条数据到索引库
  • 删除消息:根据传递的hotel的id删除索引库中的一条数据

1)首先在hotel-demo的cn.itcast.hotel.service包下的IHotelService中新增新增、删除业务

void deleteById(Long id);

void insertById(Long id);

2)给hotel-demo中的cn.itcast.hotel.service.impl包下的HotelService中实现业务:

@Override
public void deleteById(Long id) {
    try {
        // 1.准备Request
        DeleteRequest request = new DeleteRequest("hotel", id.toString());
        // 2.发送请求
        client.delete(request, RequestOptions.DEFAULT);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

@Override
public void insertById(Long id) {
    try {
        // 0.根据id查询酒店数据
        Hotel hotel = getById(id);
        // 转换为文档类型
        HotelDoc hotelDoc = new HotelDoc(hotel);

        // 1.准备Request对象
        IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
        // 2.准备Json文档
        request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
        // 3.发送请求
        client.index(request, RequestOptions.DEFAULT);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

3)编写监听器

在hotel-demo中的cn.itcast.hotel.mq包新增一个类:

package cn.itcast.hotel.mq;

import cn.itcast.hotel.constants.MqConstants;
import cn.itcast.hotel.service.IHotelService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class HotelListener {

    @Autowired
    private IHotelService hotelService;

    /**
     * 监听酒店新增或修改的业务
     * @param id 酒店id
     */
    @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
    public void listenHotelInsertOrUpdate(Long id){
        hotelService.insertById(id);
    }

    /**
     * 监听酒店删除的业务
     * @param id 酒店id
     */
    @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
    public void listenHotelDelete(Long id){
        hotelService.deleteById(id);
    }
}

集群

单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题。

  • 海量数据存储问题:将索引库从逻辑上拆分为N个分片(shard),存储到多个节点
  • 单点故障问题:将分片数据在不同节点备份(replica )

ES集群相关概念:

  • 集群(cluster):一组拥有共同的 cluster name 的 节点。

  • 节点(node) :集群中的一个 Elasticearch 实例

  • 分片(shard):索引可以被拆分为不同的部分进行存储,称为分片。在集群环境下,一个索引的不同分片可以拆分到不同的节点中

    解决问题:数据量太大,单点存储量有限的问题。

image-20231029221358115

此处,我们把数据分成3片:shard0、shard1、shard2

  • 主分片(Primary shard):相对于副本分片的定义。

  • 副本分片(Replica shard)每个主分片可以有一个或者多个副本,数据和主分片一样。

数据备份可以保证高可用,但是每个分片备份一份,所需要的节点数量就会翻一倍,成本实在是太高了!

为了在高可用和成本间寻求平衡,我们可以这样做:

  • 首先对数据分片,存储到不同节点
  • 然后对每个分片进行备份,放到对方节点,完成互相备份

这样可以大大减少所需要的服务节点数量,如图,我们以3分片,每个分片备份一份为例:

image-20231029221417280

现在,每个分片都有1个备份,存储在3个节点:

  • node0:保存了分片0和1
  • node1:保存了分片0和2
  • node2:保存了分片1和2

搭建ES集群

部署es集群可以直接使用docker-compose来完成,不过要求你的Linux虚拟机至少有4G的内存空间

首先编写一个docker-compose文件,内容如下:

version: '2.2'
services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
    container_name: es01
    environment:
      - node.name=es01
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es02,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data01:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - elastic
  es02:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
    container_name: es02
    environment:
      - node.name=es02
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es01,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data02:/usr/share/elasticsearch/data
    networks:
      - elastic
  es03:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
    container_name: es03
    environment:
      - node.name=es03
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es01,es02
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data03:/usr/share/elasticsearch/data
    networks:
      - elastic

volumes:
  data01:
    driver: local
  data02:
    driver: local
  data03:
    driver: local

networks:
  elastic:
    driver: bridge

最后进行启动

docker-compose up

集群脑裂问题

集群职责划分

elasticsearch中集群节点有不同的职责划分:

image-20231029221848875

默认情况下,集群中的任何一个节点都同时具备上述四种角色。

但是真实的集群一定要将集群职责分离:

  • master节点:对CPU要求高,但是内存要求第
  • data节点:对CPU和内存要求都高
  • coordinating节点:对网络带宽、CPU要求高

职责分离可以让我们根据不同节点的需求分配不同的硬件去部署。而且避免业务之间的互相干扰。

一个典型的es集群职责划分如图:

image-20231029221902046

脑裂问题

脑裂是因为集群中的节点失联导致的。

例如一个集群中,主节点与其它节点失联:

image-20231029221915532

此时,node2和node3认为node1宕机,就会重新选主:

image-20231029221925748

当node3当选后,集群继续对外提供服务,node2和node3自成集群,node1自成集群,两个集群数据不同步,出现数据差异。

当网络恢复后,因为集群中有两个master节点,集群状态的不一致,出现脑裂的情况:

image-20231029221942470

解决脑裂的方案是,要求选票超过 ( eligible节点数量 + 1 )/ 2 才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes,在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题

例如:3个节点形成的集群,选票必须超过 (3 + 1) / 2 ,也就是2票。node3得到node2和node3的选票,当选为主。node1只有自己1票,没有当选。集群中依然只有1个主节点,没有出现脑裂。

Elasticsearch 8.X

安装

ElasticSearch是使用java开发的,且本版本的es需要的jdk版本要是1.8以上,所以安装ElasticSearch 之 前保证JDK1.8+安装完毕,并正确的配置好JDK环境变量,否则启动ElasticSearch失败。

修改config中elasticsearch.yml配置文件,更改安全权限配置。

解压压缩包进入bin文件里面,修改config中elasticsearch.yml配置文件,更改安全权限配置。

修改完成后启动 elasticsearch 直接启动即可 默认端口:9200

image-20231030081515621

启动成功后:访问http://127.0.0.1:9200

前期准备

依赖导入

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

连接客户端

@Configuration
public class MyClientConfig extends ElasticsearchConfiguration {

    @Override
    public ClientConfiguration clientConfiguration() {
        return ClientConfiguration.builder()
                .connectedTo("localhost:9200")
                .build();
    }
}

实体类

@Data
public class Student {

    private String id;
    private Double price;
    private String description;
}

API操作

创建索引

@Autowired
private ElasticsearchClient client;

		/**
     * 创建索引
     *
     * @throws IOException
     */
    @Test
    void createIndex() throws IOException {
        CreateIndexRequest request = new CreateIndexRequest.Builder().index("student").build();
        CreateIndexResponse response = client.indices().create(request);
      	System.out.println("创建索引成功:" + createIndexResponse.acknowledged());
    }

使用lambda表达式

/**
     * 创建索引
     *
     * @throws IOException
     */
    @Test
    void createIndex() throws IOException {
        CreateIndexResponse response = client.indices().create(
                builder -> builder.index("student"));
        System.out.println(response.acknowledged());
    }

查询索引

		/**
     * 查询索引
     *
     * @throws IOException
     */
    @Test
    void queryIndex() throws IOException {
        GetIndexResponse response = client.indices().get(
                builder -> builder.index("student"));
        System.out.println(response.result());
    }

删除索引

 		/**
     * 删除索引
     *
     * @throws IOException
     */
    @Test
    void deleteIndex() throws IOException {
        DeleteIndexResponse response = client.indices().delete(
                builder -> builder.index("products"));
        System.out.println(response);
    }

创建文档

		/**
     * 创建文档
     *
     * @throws IOException
     */
    @Test
    void createDocument() throws IOException {
        Student student = new Student();
        student.setId("1");
        student.setPrice(123.49);
        student.setDescription("我是第一个学生");
        IndexResponse response = client.index(builder -> builder.
                index("student").
                id(student.getId())
                .document(student));
        System.out.println(response.result());
    }

查询文档

/**
     * 查询文档
     *
     * @throws IOException
     */
    @Test
    void queryDocument() throws IOException {
        Student student = new Student();
        student.setId("1");
        student.setPrice(123.49);
        student.setDescription("我是第一个学生");
        SearchResponse<Student> response = client.search(s -> s
                        .index("student")
                        .query(q -> q
                                .match(t -> t
                                        .field("description")
                                        .query("学生")
                                )
                        ),
                Student.class
        );
        System.out.println(response);
    }