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接口中的方法即可,这里不再赘述。

相关推荐
吴永琦(桂林电子科技大学)5 小时前
Git 与 Git常用命令
大数据·git·elasticsearch
麦芽糖021912 小时前
elasticsearch实战三 elasticsearch与mysql数据实时同步
大数据·mysql·elasticsearch
m0_7482500314 小时前
重学SpringBoot3-整合 Elasticsearch 8.x (二)使用Repository
大数据·elasticsearch·jenkins
GitCode官方20 小时前
GitCode 助力 Easy-Es,革新 Elasticsearch 开发体验
大数据·elasticsearch·开源·gitcode
C_V_Better1 天前
用Kibana实现Elasticsearch索引的增删改查:实战指南
elasticsearch·搜索引擎·es
日拱一卒无有尽, 功不唐捐终入海1 天前
elasticsearch
大数据·elasticsearch·搜索引擎
C_V_Better2 天前
Java集成Elasticsearch实战商品表增删改查全解析java操作ElasticSearch增删改查
java·elasticsearch
C_V_Better2 天前
Elasticsearch+Kibana安装启动与操作教程
elasticsearch·搜索引擎·es
一勺菠萝丶3 天前
Docker 容器 Elasticsearch 启动失败完整排查记录
elasticsearch·docker·jenkins