公司内部决定开发一个类似飞书的资源管理项目,其中包括了资源的权限控制管理等等、素材的存储、素材的预览,中间还有一个功能点是文件库搜索,作为一个"资深"后端人,直觉来看这块得上个全文搜索以绝后患,今天我主要介绍下自己对这个单体项目全文搜索框架的hibernate-search实践。
全文搜索
全文搜索:从文本或数据库中,不限定资料字段,自由地萃取出消息的技术[zh.wikipedia.org/zh-cn/%E5%8...
在全文搜索的功能需求之上,我们需要了解实现的这个方案的核心的技术,就是倒排索引。
倒排索引
这个知识点有点烂大街了,简而言之就是我们先从内容提取出索引片段,然后mapping 索引到具体内容的位置,因为是从索引查找内容,称为倒排索引。有倒排就有正排索引,正排索引是通过id去搜索对应的完整数据的位置,和搜索内容本身无关,正排索引被应用在关系和非关系的数据库中。
倒排索引可以类比就是我们以前买新华字典的每一页最边侧有一个小小的字母
以前小的时候,查阅自己不会的字的发音解释,都会先把字典大致一翻,然后根据心里字母表的顺序从前或者从后找到对应的字母页,然后根据第二元音的顺序继续使用这个算法,现在想想是不是觉得一切生活处处是算法,整个过程基于倒排索引将数据存储在"字典中",然后我们通过二分的思想不断收缩查找范围实现内容检索。
Lucene
Lucene项目是后续很多比较知名的全文搜索项目的基石:en.wikipedia.org/wiki/Apache...
css
Apache Nutch -- 提供网络爬行和 HTML 解析
Apache Solr -- 企业搜索服务器
CrateDB -- 基于 Lucene 构建的开源分布式 SQL 数据库
DocFetcher -- 多平台桌面搜索应用程序
Elasticsearch -- 2010 年发布的企业搜索服务器
hibernate-search 提供了Lucene(默认) 和 Elasticsearch两种方案的底层实现,Lucene 是基于文件系统存储索引。Lucene的索引是分层的,依次为 Index(索引) --> Segment(段) --> Document(文档) --> Field(域) --> Term(词/词汇单元)。
名称 | 扩展名 | 简要描述 | 相关源码 |
---|---|---|---|
Segment File | segments_N | commit点信息,其中N是一个36进制表示的值 | SegmentInfos |
Lock File | write.lock | 文件锁,避免多个writer同时写;默认和索引文件一个目录。 | |
Segment Info | .si | segment的元数据信息,指明这个segment都包含哪些文件 | Lucene70SegmentInfoFormat |
Compound File | .cfs, .cfe | 如果启用compound功能,会压缩索引到2个文件内 | Lucene50CompoundFormat |
Fields | .fnm | 存储有哪些Field,以及相关信息 | Lucene60FieldInfosFormat |
Field Index | .fdx | Field数据文件的索引 | Lucene50StoredFieldsFormat |
Field Data | .fdt | Field数据文件 | Lucene50StoredFieldsFormat |
Term Dictionary | .tim | Term词典 | BlockTreeTermsWriter |
Term Index | .tip | 指向Term词典的索引 | BlockTreeTermsWriter |
Frequencies | .doc | 保留包含每个Term的文档列表 | Lucene50PostingsWriter |
Positions | .pos | Term在文章中出现的位置信息 | Lucene50PostingsWriter |
Payloads | .pay | offset偏移/payload附加信息 | Lucene50PostingsWriter |
Norms | .nvd, .nvm | .nvm保存加权因子元数据;.nvd存储加权数据 | Lucene70NormsFormat |
Per-Document Values | .dvd, .dvm | .dvm存文档正排元数据;.dvd存文档正排数据 | Lucene70DocValuesFormat |
Term Vector Index | .tvx | 指向tvd的offset | Lucene50TermVectorsFormat |
Term Vector Data | .tvd | 存储term vector信息 | Lucene50TermVectorsFormat |
Live Documents | .liv | 活着的文档列表。位图形式 | Lucene50LiveDocsFormat |
Point Values | .dii, .dim | 多维数据,地理位置等信息,用于处理数值型的查询 | Lucene60PointsFormat |
hibernate-search
6.2官方文档[docs.jboss.org/hibernate/s...] 前面都是hibernate-search的基础,hibernate-search 相当于是luence 和 mysql关系型数据库之间的一个同步器,在基于orm框架下,同一个entity被更新到mysql的同时,luence 也会自动将对应的数据更新到luence中,官方的架构图:
- 添加对应的依赖
xml
<dependency>
<groupId>org.hibernate.search</groupId>
<artifactId>hibernate-search-mapper-orm</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.search</groupId>
<artifactId>hibernate-search-backend-lucene</artifactId>
</dependency>
- 自定义索引的存储位置(6.2版本)
yaml
spring:
properties:
hibernate:
search:
backend:
directory:
root: /nfs/index
- 使用注解的方式配置需要全文搜索的javabean
less
@Data
@Entity
@Table(name="t_document_file_research")
@Indexed(index = "index_file")
public class FileResearchEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY )
private Long id;
@GenericField(projectable = Projectable.YES)
private Long sourceId;
@FullTextField(name = "search_name")
@GenericField(sortable = Sortable.YES,projectable = Projectable.YES)
private String name;
@GenericField(projectable = Projectable.YES)
private Long userId;
@GenericField(sortable = Sortable.YES,projectable = Projectable.YES)
private LocalDateTime updateTime;
@GenericField(projectable = Projectable.YES)
private Boolean deleteState;
@FullTextField(name = "search_label")
@GenericField(sortable = Sortable.YES,projectable = Projectable.YES) //
private String labelList;
}
使用 FullTextField 注解,配置哪些字段是需要被搜索的,FullTextField不支持排序高亮持久化的配置,可以添加GenericField 重新命名索引字段名。具体的注解使用自行参考官方文档哈。
- 代码完成查询的代码,比如我们需要有一个功能需求是 对name和labelList的全文匹配,如果两者命中一个就分页返回10条返回结果。
scss
SearchSession session = null;
try {
Session hibernateSession = sessionFactory.openSession();
SearchSession session = Search.session(hibernateSession);
SearchScope<FileResearchEntity> scope = session.scope( FileResearchEntity.class );
return session.search(FileResearchEntity.class)
.where( f -> f.bool()
.minimumShouldMatchNumber(1) // 必须匹配成功一条条件
.should(f.match().field("search_name" ).matching(pageRulerRequest.getKeyword().getValue()))
.should( f.match().field("search_label").matching(pageRulerRequest.getKeyword().getValue())
)).fetchHits(0,10);
}finally {
close(session);
}
如果我们使用spring jpa 修改FileResearchEntity 数据,那么hibernate-search会自动级联完成数据更新同步。
后言
在持久层操作的框架上,基本上的公司都是使用mybatis或者spring data jpa ,简单说下自己对这两个框架的理解和感受。
mybatis 这个框架在国内刚流行的时候,大部分都是基于ssm框架,早期的框架配置比较多,不像如今进入了spring boot的注解时代,所以以前的mybatis 都是通过配置各种xml,在xml编写mybatis语法的sql。后续基于mybatis的增强框架mybatis-plus问世,基于注解开发让操作数据变的更加的方便。
mybatis 不是强orm框架,缺少像hibernate框架,对代码层和数据库层一些表明字段名的等信息硬绑定,但是mybatis 更加灵活,但是mybatis自带了很多的脚本语法,可以很方便实现出复杂的动态查询语句,目前看热度mybatis更胜spring data jpa。
spring data jpa 是基于hibernate 的orm持久层框架,属于spring 全家桶的子项目,全注解的开发模式,但是因为基于hibernate ,稍微复杂点的还能依赖JpaSpecificationExecutor,但是如果一些很复杂的联表动态参数,可能短板就比较明显。一般我们都是配合querydsl一起使用 ,或者评论可以告知下是不是有更好的解决方案。