Elasticsearch

Elastic Stack简介

官方文档:https://www.elastic.co/cn/

Elastic Stack的组件

Elastic Stack核心产品包括 Elasticsearch、Kibana、Beats 和 Logstash(也称为 ELK Stack)等等。能够安全可靠地从任何来源获取任何格式的数据,然后对数据进行搜索、分析和可视化。
而Elasticsearch是Elastic Stack核心的核心。

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。作为 Elastic Stack 的核心,Elasticsearch 会集中存储您的数据,让您飞快完成搜索,微调相关性,进行强大的分析,并轻松缩放规模。

Kibana 可以将数据转变为结果、响应和解决方案,是 Elastic Stack 的可视化工具。使用 Kibana 针对大规模数据快速运行数据分析,以实现可观测性、安全和搜索。对来自任何来源的任何数据进行全面透彻的分析,从威胁情报到搜索分析,从日志到应用程序监测,不一而足。 我赞同Kibana文档中所说的一句话“一张图片胜过千万行日志”。

Logstash 是 Elastic Stack 的数据收集和转换工具,用于将不同来源的数据导入 Elasticsearch。

Beats 是轻量级的数据采集器,用于采集各种类型的数据并将其发送到 Elasticsearch 或 Logstash。

Elasticsearch快速入门

MySQL 与 Elasticsearch 的概念简单类比

MySQL

Elasticsearch

Table

Index

Row

Document

Column

Field

Schema

Mapping

SQL

DSL

倒排索引和正排索引

倒排索引是用于提高数据检索速度的一种数据结构,空间消耗比较大。倒排索引首先将检索文档进行分词得到多个词语/词条,然后将词语和文档 ID 建立关联,从而提高检索效率。

正排索引将文档 ID 和分词建立关联,根据词语查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词语,查询效率较低。

Java操作Elasticsearch

Spring Data Elasticsearch

官方文档:https://spring.io/projects/spring-data-elasticsearch/

集成与配置的基本示例

1.添加以下依赖

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

2.配置 Elasticsearch 连接

spring:
  data:
    elasticsearch:
      cluster-name: elasticsearch
      cluster-nodes: localhost:9200

3.创建实体类

import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;

@Document(indexName = "my_index", type = "my_type")
public class MyEntity {
    @Id
    private String id;
    private String name;
    private String description;
    
    // 省略构造函数和 getter/setter 方法
}

4.创建 Elasticsearch Repository

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface MyEntityRepository extends ElasticsearchRepository<MyEntity, String> {
    // 可以添加自定义查询方法
}

5.使用 Spring Data Elasticsearch

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MyEntityService {
    private final MyEntityRepository repository;

    @Autowired
    public MyEntityService(MyEntityRepository repository) {
        this.repository = repository;
    }

    public MyEntity save(MyEntity entity) {
        return repository.save(entity);
    }

    public MyEntity findById(String id) {
        return repository.findById(id).orElse(null);
    }

    public List<MyEntity> findByName(String name) {
        return repository.findByName(name);
    }

    // 其他操作...
}

Elasticsearch Java API Client

官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/introduction.html

集成与配置示例(学习自优秀的Elasticsearch的实践:https://docs.xxyopen.com/course/novel/4.html#%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E-elasticsearch-%E9%9B%86%E6%88%90%E4%B8%8E%E9%85%8D%E7%BD%AE

1.Kibana 中创建索引

PUT /book
{
  "mappings" : {
    "properties" : {
      "id" : {
        "type" : "long"
      },
      "authorId" : {
        "type" : "long"
      },
      "authorName" : {
        "type" : "text",
        "analyzer": "ik_smart"
      },
      "bookName" : {
        "type" : "text",
        "analyzer": "ik_smart"
      },
      "bookDesc" : {
        "type" : "text",
        "analyzer": "ik_smart"
      },
      "bookStatus" : {
        "type" : "short"
      },
      "categoryId" : {
        "type" : "integer"
      },
      "categoryName" : {
        "type" : "text",
        "analyzer": "ik_smart"
      },
      "lastChapterId" : {
        "type" : "long"
      },
      "lastChapterName" : {
        "type" : "text",
        "analyzer": "ik_smart"
      },
      "lastChapterUpdateTime" : {
        "type": "long"
      },
      "picUrl" : {
        "type" : "keyword",
        "index" : false,
        "doc_values" : false
      },
      "score" : {
        "type" : "integer"
      },
      "wordCount" : {
        "type" : "integer"
      },
      "workDirection" : {
        "type" : "short"
      },
      "visitCount" : {
        "type": "long"
      }
    }
  }
}

2.项目添加依赖

<dependencies>

    <dependency>
      <groupId>co.elastic.clients</groupId>
      <artifactId>elasticsearch-java</artifactId>
      <version>8.2.0</version>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.3</version>
    </dependency>

</dependencies>

3.application.yml 中配置连接信息

spring:
  elasticsearch:
    uris:
      - https://my-deployment-ce7ca3.es.us-central1.gcp.cloud.es.io:9243
    username: elastic
    password: qTjgYVKSuExX

4.配置 JacksonJsonpMapper

/**
 * elasticsearch 相关配置
 */
@Configuration
public class EsConfig {

    /**
     * 解决 ElasticsearchClientConfigurations 修改默认 ObjectMapper 配置的问题
     */
    @Bean
    JacksonJsonpMapper jacksonJsonpMapper() {
        return new JacksonJsonpMapper();
    }

}

5.新建搜索服务类

/**
 * 搜索 服务类
 */
public interface SearchService {

    /**
     * 小说搜索
     *
     * @param condition 搜索条件
     * @return 搜索结果
     */
    RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition);

}

6.新建数据库搜索服务实现类

/**
 * Elasticsearch 搜索 服务实现类
 */
@ConditionalOnProperty(prefix = "spring.elasticsearch", name = "enable", havingValue = "true")
@Service
@RequiredArgsConstructor
@Slf4j
public class EsSearchServiceImpl implements SearchService {

    private final ElasticsearchClient esClient;

    @SneakyThrows
    @Override
    public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {

        SearchResponse<EsBookDto> response = esClient.search(s -> {

                    SearchRequest.Builder searchBuilder = s.index(EsConsts.IndexEnum.BOOK.getName());
                    buildSearchCondition(condition, searchBuilder);
                    // 排序
                    if (!StringUtils.isBlank(condition.getSort())) {
                        searchBuilder.sort(o ->
                                o.field(f -> f.field(StringUtils
                                                .underlineToCamel(condition.getSort().split(" ")[0]))
                                        .order(SortOrder.Desc))
                        );
                    }
                    // 分页
                    searchBuilder.from((condition.getPageNum() - 1) * condition.getPageSize())
                            .size(condition.getPageSize());

                    return searchBuilder;
                },
                EsBookDto.class
        );

        TotalHits total = response.hits().total();

        List<BookInfoRespDto> list = new ArrayList<>();
        List<Hit<EsBookDto>> hits = response.hits().hits();
        for (Hit<EsBookDto> hit : hits) {
            EsBookDto book = hit.source();
            assert book != null;
            list.add(BookInfoRespDto.builder()
                    .id(book.getId())
                    .bookName(book.getBookName())
                    .categoryId(book.getCategoryId())
                    .categoryName(book.getCategoryName())
                    .authorId(book.getAuthorId())
                    .authorName(book.getAuthorName())
                    .wordCount(book.getWordCount())
                    .lastChapterName(book.getLastChapterName())
                    .build());
        }
        assert total != null;
        return RestResp.ok(PageRespDto.of(condition.getPageNum(), condition.getPageSize(), total.value(), list));
    
    }

    /**
    * 构建查询条件
    */
    private void buildSearchCondition(BookSearchReqDto condition, SearchRequest.Builder searchBuilder) {

        BoolQuery boolQuery = BoolQuery.of(b -> {

            if (!StringUtils.isBlank(condition.getKeyword())) {
                // 关键词匹配
                b.must((q -> q.multiMatch(t -> t
                        .fields("bookName^2","authorName^1.8","bookDesc^0.1")
                        .query(condition.getKeyword())
                )
                ));
            }

            // 精确查询
            if (Objects.nonNull(condition.getWorkDirection())) {
                b.must(TermQuery.of(m -> m
                        .field("workDirection")
                        .value(condition.getWorkDirection())
                )._toQuery());
            }

            if (Objects.nonNull(condition.getCategoryId())) {
                b.must(TermQuery.of(m -> m
                        .field("categoryId")
                        .value(condition.getCategoryId())
                )._toQuery());
            }

            // 范围查询
            if (Objects.nonNull(condition.getWordCountMin())) {
                b.must(RangeQuery.of(m -> m
                        .field("wordCount")
                        .gte(JsonData.of(condition.getWordCountMin()))
                )._toQuery());
            }

            if (Objects.nonNull(condition.getWordCountMax())) {
                b.must(RangeQuery.of(m -> m
                        .field("wordCount")
                        .lt(JsonData.of(condition.getWordCountMax()))
                )._toQuery());
            }

            if (Objects.nonNull(condition.getUpdateTimeMin())) {
                b.must(RangeQuery.of(m -> m
                        .field("lastChapterUpdateTime")
                        .gte(JsonData.of(condition.getUpdateTimeMin().getTime()))
                )._toQuery());
            }

            return b;

        });

        searchBuilder.query(q -> q.bool(boolQuery));

    }
}

7.BookController 中注入 SearchService bean,调用searchBooks方法实现按配置动态切换搜索引擎的功能

public class BookController {

    private final SearchService searchService;

    /**
     * 小说搜索接口
     */
    @GetMapping("search_list")
    public RestResp<PageRespDto<BookInfoRespDto>> searchBooks(BookSearchReqDto condition) {
        return searchService.searchBooks(condition);
    }

}

两个API对比

Spring Data Elasticsearch提供了更高级别的抽象,隐藏了许多底层细节,可以更快速地开始使用。通过定义存储库接口来轻松地执行常见的CRUD操作和查询。它提供了一些预定义的方法,如保存、查找和删除,以及通过方法命名约定自动生成查询。

Elasticsearch Java API Client提供了更低级别的控制,可以更精确地控制请求和响应的细节。它适用于需要更多自定义和特定操作的场景,但也需要更多的代码编写和管理。

Comment