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

相关推荐
老纪的技术唠嗑局4 小时前
告别OpenClaw配置丢失——Mindkeeper内测版邀测
大数据·elasticsearch·搜索引擎
Elasticsearch4 小时前
使用 Elasticsearch + Jina embeddings 进行无监督文档聚类
elasticsearch
勇哥的编程江湖6 小时前
flinkcdc streaming 同步数据到es记录过程
大数据·elasticsearch·flink·flinkcdc
曾阿伦6 小时前
Elasticsearch 7.x 常用命令备忘录
大数据·elasticsearch·搜索引擎
斯特凡今天也很帅7 小时前
Elasticsearch数据库专栏(二)DSL语句总结(更新中)
大数据·elasticsearch·搜索引擎
要记得喝水7 小时前
适用于 Git Bash 的脚本,批量提交和推送多个仓库的修改
git·elasticsearch·bash
二十七剑8 小时前
Elasticsearch的索引问题
大数据·elasticsearch·搜索引擎
A__tao16 小时前
Elasticsearch Mapping 一键生成 Java 实体类(支持嵌套 + 自动过滤注释)
java·python·elasticsearch
A__tao18 小时前
Elasticsearch Mapping 一键生成 Proto 文件(支持嵌套 + 注释过滤)
大数据·elasticsearch·jenkins
Devin~Y18 小时前
高并发电商与AI智能客服场景下的Java面试实战:从Spring Boot到RAG与向量数据库落地
java·spring boot·redis·elasticsearch·spring cloud·kafka·rag