WebFlux集成ElasticSearch实现多关键词响应式全文检索

几日前接到一个需求,要对系统现有知识库中的一个模块添加全文检索功能,类似于语雀,要引入ElasticSearch:

由于整个系统是基于响应式webFlux的,所以在这里记录一下。

1. 目前的数据库表

文档实体类如下:

java 复制代码
@Getter
@Setter
@Entity
@Table(name = "qa_files")
public class QAFile extends GenericEntity<String> {
    @Column
    private String questionId;
    @Column
    private String questionTitle;
    @Column
    private String questionDetail;
    @Column
    private String uploaderId;
    @Column
    private Date uploadTime;
    @Column
    private String content;
    @Column
    @JsonIgnore
    private boolean delete;
    @Column
    private String editorId;
    @Column
    private Date updateTime;
    @Column
    private String typeId;	// 文档类型
    @Column
    private Integer version;	// 文档版本
}

现在要对questionTitle (文档标题,纯文本格式)、questionDetail (文档描述,纯文本格式)、content(文档内容,富文本格式)这三个字段支持全文检索,并且可以使用多关键词(由空格分隔)检索。

2. 本地安装ElasticSearch、es-head插件、Kibana、ik分词器

具体链接可以看这里,说的蛮详细

SpringBoot整合Elasticsearch(最新最全,高效安装到使用)

3. 引入依赖并修改配置文件

配置文件中添加:

ini 复制代码
spring.elasticsearch.rest.uris=127.0.0.1:9200
spring.elasticsearch.rest.connection-timeout=1s
spring.elasticsearch.rest.read-timeout=30s

pom.xml:

xml 复制代码
<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.4.1</version>
	<relativePath />
</parent>

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-webflux</artifactId>
	</dependency>
	<dependency>
		<groupId>io.projectreactor</groupId>
		<artifactId>reactor-core</artifactId>
		<version>3.4.17</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-r2dbc</artifactId>
		<version>2.3.0.RELEASE</version>
	</dependency>
	<!-- ElasticSearch -->
	<dependency>
		<groupId>org.elasticsearch</groupId>
		<artifactId>elasticsearch</artifactId>
		<version>7.9.3</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
	</dependency>
</dependencies>

这里我使用的springboot版本为2.4.x,对应ES版本为7.9.3,其他版本对应如下:

此外spring-boot-starter-data-elasticsearch对reactor-core版本也有要求,如果引入后有冲突可以自行去Maven central排查

4. 修改实体类

java 复制代码
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Getter
@Setter
@Entity
@Table(name = "qa_files")
@Document(indexName = "qa_file_index")// 稍后通过Kibana创建索引
public class QAFile extends GenericEntity<String> {
    @Column
    private String questionId;
    @Column
    @Field(type = FieldType.Text, analyzer = "ik_max_word") //指定字段类型和解析器,使用ik_max_word最细粒度拆分
    private String questionTitle;
    @Column
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String questionDetail;
    @Column
    private String uploaderId;
    @Column
    private Date uploadTime;
    @Column
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String content;
    @Column
    @JsonIgnore
    private boolean delete;
    @Column
    private String editorId;
    @Column
    private Date updateTime;
    @Column
    private String typeId;	// 文档类型
    @Column
    private Integer version;	// 文档版本
}

5. 创建qa_file_index索引

打开本地Kibana页面左侧选择开发工具Dev Tools - Elastic 在控制台创建索引:

ElasticsearchJSON 复制代码
PUT /qa_file_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "ik_analyzer": {
          "type": "custom",
          "tokenizer": "ik_max_word"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "questionTitle": {
        "type": "text",
        "analyzer": "ik_analyzer",
        "search_analyzer": "ik_smart"
      },
      "questionDetail": {
        "type": "text",
        "analyzer": "ik_analyzer",
        "search_analyzer": "ik_smart"
      },
      "content": {
        "type": "text",
        "analyzer": "ik_analyzer",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

返回如下内容即创建成功:

然后此时可以通过es-head查看索引以及其中的数据:

6. 将数据从表中同步至ES

首先创建一个Repository接口继承ReactiveCrudRepository

Java 复制代码
import com.clinic.HeartLine.domain.knowledge.QAFile;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface QAFileESRepository extends ReactiveCrudRepository<QAFile, String> {
}

如果是非响应式可以继承ElasticsearchRepository或ElasticsearchTemplate,里面封装了很多基础操作,可以通过该接口操作ElasticSearch中的数据。

接着写一个数据同步接口,将符合条件的文档从表中同步至ES(这里由于content是富文本格式,因此用HtmlUtils.cleanHtmlTag将html标签去除):

Java 复制代码
@Override
@Transactional
public Mono<Boolean> syncDataToES() {
    return this.createQuery()
            .where("delete", false)
            .fetch()
            .collectList()
            .publishOn(Schedulers.boundedElastic())
            .flatMap(qaFiles -> {
                qaFiles.forEach(qaFile -> qaFile.setContent(HtmlUtils.cleanHtmlTag(qaFile.getContent())));
                return qaFileESRepository.saveAll(qaFiles)
                        .collectList()
                        .thenReturn(true);
            });
}

7. 全文检索接口

接下来就可以写接口了,这里最终通过HighlightBuilder将原本文档的文本替换为包含检索高亮关键词的文本摘要,在构建BoolQueryBuilder时为每个字段分配了匹配权重,ES会自动根据匹配度对结果进行排序:

Java 复制代码
@Override
@Transactional
public Mono<List<QAFile>> fullTextSearch(String query) {
    String[] queryTerms = query.split("\s+"); //按空格分隔多个查询条件
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

    for (String term : queryTerms) {
        // 对每个查询词,内部使用should来进行多个字段的OR查询
        BoolQueryBuilder termQuery = QueryBuilders.boolQuery()
                .should(QueryBuilders.matchQuery("questionTitle", term).boost(2.0 f))
                .should(QueryBuilders.matchQuery("questionDetail", term))
                .should(QueryBuilders.matchQuery("content", term).boost(1.5 f));
        // 将每个查询词的查询添加到总的查询中,使用must连接不同查询词(AND查询)
        boolQuery.must(termQuery);
    }
    HighlightBuilder highlightBuilder = new HighlightBuilder()
            .field("questionTitle") // 高亮questionTitle字段
            .field("questionDetail") // 高亮questionDetail字段
            .field("content") // 高亮content字段
            .preTags("<span style="color: red;">") // 设置高亮标签
            .postTags("</span>")
            .fragmentSize(50); // 设置每个片段的最大长度,单位字符
    // 构建查询
    NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
            .withQuery(boolQuery)
            .withPageable(PageRequest.of(0, 10)) //默认返回10条
            .withHighlightBuilder(highlightBuilder)
            .build();
    SearchHits<QAFile> searchHits = elasticsearchRestTemplate.search(searchQuery, QAFile.class);

    List<QAFile> results = searchHits.stream()
            .map(searchHit -> {
                QAFile qaFile = searchHit.getContent();
                Map<String, List<String>> highlightFields = searchHit.getHighlightFields();
                List<String> highlightedContent = highlightFields.get("content");
                if (highlightedContent != null && !highlightedContent.isEmpty()) {
                    // 取第一个高亮部分(如果有多个高亮部分)
                    qaFile.setContent(highlightedContent.get(0));
                }
                return qaFile;
            })
            .collect(Collectors.toList());

    return Flux.fromIterable(results)
            .flatMapSequential(this::fillUserInfo)
            .collectList();
}

最终实现结果如下:

除此接口外还要对文档增删改操作接口添加同步至ES的代码,只需要调用Repository接口中的方法即可,这里不再赘述。

相关推荐
小湘西3 分钟前
Elasticsearch 的一些默认配置上下限
java·大数据·elasticsearch
Dxy12393102163 小时前
Elasticsearch 8如何做好标题搜索
大数据·elasticsearch
斯普信云原生组4 小时前
Elasticsearch(ES) 内存 CPU 过高问题排查报告
大数据·elasticsearch·搜索引擎
弘毅 失败的 mian4 小时前
Git 分支管理
大数据·经验分享·笔记·git·elasticsearch
阿坤带你走近大数据5 小时前
Elasticsearch(ES)的基本概念、架构及基本使用介绍
大数据·elasticsearch
Elastic 中国社区官方博客5 小时前
使用 Elasticsearch 中的结构化输出创建可靠的 agents
大数据·人工智能·elk·elasticsearch·搜索引擎·ai·全文检索
G皮T6 小时前
【Elasticsearch】查询性能调优(六):track_total_hits 影响返回结果的相关性排序吗
大数据·数据库·elasticsearch·搜索引擎·全文检索·性能·opensearch
LCG米7 小时前
嵌入式Linux系统构建:为STM32MP157移植Buildroot并开发温湿度采集驱动
linux·stm32·elasticsearch
phil zhang8 小时前
Celer:为大型C/C++项目打造的极简包管理器
开发语言·c++·elasticsearch
sysinside20 小时前
Elasticsearch 9.2 发布 - 分布式搜索和分析引擎
大数据·分布式·elasticsearch