前言:本文基于若依前后端分离版本(Spring Boot 3.3.0 + Vue 3 + Activiti 8.1.0)进行改造,相关教程可以在网上找到。在撰写此博客期间,笔者刚刚开始接触 Java Web,本系列下的文章内容包含大量“个人初期”视角,注意鉴别。

全文检索

回想一下我们查找知网的时候,我们可以搜索标题得知一些信息,同样,我们有的时候需要检索他的摘要才能得到一些算法和方法的信息。

所以,这个时候单纯去模糊查找MySQL数据库就不太合适了,我们需要一个新的数据库,他能保存文件的一些关键信息,包括{标题、作者、摘要(甚至是全文)}等,相比MySQL的一条条轻量化的记录数据(比如文档实际的保存位置、标题、ISBN等),我们期望新的数据库能将文本的全文进行保存,能让我们:翻开书看,查找书中内容。而不是只看封面获得一些“表面信息”。

这个时候就可以使用Elasticserarch:分布式搜索和分析引擎

对比一下MySQL?

MySQL的过程:建数据库,sql建表,Java中定义某个domain/calss,使用mapper映射实现增删查改。

  • 比如:课程表、课程类、课程mapper、课程服务。

Elasticsearch过程:Java中定义某个domain/class,调用库函数保存和查找。

  • 比如:文档类、文档保存与检索。

Elasticsearch基本结构以及处理对象基本结构(现阶段理解不一定对):

所以MySQL查,查一条条数据,去检索的是每一列、每一行,找到匹配的;Elasticserarch查,可以也是查每一个记录的属性,只不过这个属性中有一个巨无霸:content。他甚至可以是全文。

如果有一个人的名字,同时出现在多个文档中(内容层面),MySQL可查不到这些数据,Elasticserarch就可以查这些文档的内容,返回所有包含这个名字的文档。

MySQL: 数据库 -> 数据表(XX表)-> 主键+属性(保存位置、所属部门、创建者)
Elasticsearch: 数据库 -> 仓库(indexName)-> ID+其他文档内容(标题+作者+内容+介绍)

Elasticsearch中主要保存能用于检索的信息,MySQL记录更加详细的标签类的信息,二者可以通过某个属性连接。

如何使用?

定义实体类与仓库

我们在原有的MySQL的类上,新建一个类,它用来保存/承接Elasticsearch的搜索结果,需要注意的应该就是@Document注解。其他部分就像MySQL一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@JsonIgnoreProperties(ignoreUnknown = true)
@Document(indexName = "archives")
public class ElasticsearchArchive {

@Id
private String id;

@Field(type = FieldType.Keyword)
private String mysqlDanghao; // 对应MySQL中的档号

@Field(type = FieldType.Text)
private String title; // 档案标题

// other code
}
  • 1、indexName:我们没有显式指明一个数据库,与MySQL似乎不太像,不过可以把这个indexName作为一个假想的数据库,我们在这个数据库下,只能查找ElasticsearchArchive这个类型。
  • 2、 Elasticsearch也是要一个主键ID的,string类型,不过不同自己去设置,他会帮你自动生成的。别忘了@Id注解。
  • 3、其他属性使用@Field,并标Type。

插入数据库

再插入数据库时,应该同时插入MySQL与Elasticsearch。

原本插入Archive数据库的方法,仅修改MySQL。

1
2
3
4
5
6
@Override
public int insertArchive(Archive archive)
{
archive.setCreateTime(DateUtils.getNowDate());
return archiveMapper.insertArchive(archive);
}

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public int insertArchive(Archive archive)
{
archive.setCreateTime(DateUtils.getNowDate());
// 获取文件内容,构建保存到Elasticsearch的对象
String filePath = archive.getDianziLocation();
ElasticsearchArchive document = new ElasticsearchArchive();
document.setMysqlDanghao(archive.getDanghao());
document.setTitle(archive.getName());
document.setContent(getFileContent(filePath));
document.setFileType(archive.getCarrierType());
document.setDescription(archive.getDescription());
document.setSecretLevel(archive.getSecretLevel());
document.setCreateTime(archive.getCreateTime());
document.setFilePath(filePath);
// 保存到Elasticsearch,注意此处的方法:(实例)仓库.save(实例)
archiveRepository.save(document);
// 保存到MySQL数据库
return archiveMapper.insertArchive(archive);
}

MySQL由于可以使用mapper服务接口,可以避免直接对数据库的操作,为什么Elasticsearch并没有这些服务,凭什么除了new对象以外能直接用一个服务调用save进行保存呢?这个服务似乎还是以Repository结尾的。

之前我似乎把indexName称为仓库,基于同样的@Document(indexName=“XXX”)索引,会形成一个仓库,而我们需要基于@Document(indexName=“XXX”)下面的实体类,创建一个仓库的接口。

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;


import java.util.List;

@Repository
public interface ArchiveRepository extends ElasticsearchRepository<ElasticsearchArchive, Long> {
List<ElasticsearchArchive> findByContent(String content);
// other code
}

这下就能解释上一段的仓库服务接口是哪里来的了。

查看插入的数据

如果安装了kibana,可以打开localhost:5601,输入:

1
GET /archives/_search

可以查到保存的文件:

前端发起检索

不用写实现!

在仓库部分定义了List<ElasticsearchArchive> findByContent(String content);但是并没有给出具体的实现,这是Spring Data Elasticsearch 提供的一个基础接口,它支持根据方法名自动生成查询实现。

因此可以根据合理的命名方式,让他自动实现功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Repository
public interface ArchiveRepository extends ElasticsearchRepository<ElasticsearchArchive, Long> {

// 根据标题或内容搜索
List<ElasticsearchArchive> findByTitleContainingOrContentContaining(String title, String content);

// 根据内容查询
List<ElasticsearchArchive> findByContent(String content);

// 根据文件类型和内容搜索
List<ElasticsearchArchive> findByFileTypeAndContentContaining(String fileType, String content);

// 根据保密级别和内容搜索
List<ElasticsearchArchive> findBySecretLevelAndContentContaining(String secretLevel, String content);
}

上一节的演示中使用的就是findByContent(String content);方法。