Spring Boot整合Postgres实现轻量级全文搜索
有这样一个带有搜索功能的用户界面需求:
搜索流程如下所示:
这个需求涉及两个实体:
- “评分(Rating)、用户名(Username)”数据与
User
实体相关 - “创建日期(create date)、观看次数(number of views)、标题(title)、正文(body)”与
Story
实体相关
需要支持的功能对User
实体中的评分(Rating)的频繁修改以及下列搜索功能:
- 按User评分进行范围搜索
- 按Story创建日期进行范围搜索
- 按Story浏览量进行范围搜索
- 按Story标题进行全文搜索
- 按Story正文进行全文搜索
Postgres中创建表结构和索引
创建users
表和stories
表以及对应搜索需求相关的索引,包括:
- 使用 btree 索引来支持按User评分搜索
- 使用 btree 索引来支持按Story创建日期、查看次数的搜索
- 使用 gin 索引来支持全文搜索内容(同时创建全文搜索列
fulltext
,类型使用tsvector
以支持全文搜索)
具体创建脚本如下:
--Create Users table
CREATE TABLE IF NOT EXISTS users
(
id bigserial NOT NULL,
name character varying(100) NOT NULL,
rating integer,
PRIMARY KEY (id)
)
;
CREATE INDEX usr_rating_idx
ON users USING btree
(rating ASC NULLS LAST)
TABLESPACE pg_default
;
--Create Stories table
CREATE TABLE IF NOT EXISTS stories
(
id bigserial NOT NULL,
create_date timestamp without time zone NOT NULL,
num_views bigint NOT NULL,
title text NOT NULL,
body text NOT NULL,
fulltext tsvector,
user_id bigint,
PRIMARY KEY (id),
CONSTRAINT user_id_fk FOREIGN KEY (user_id)
REFERENCES users (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID
)
;
CREATE INDEX str_bt_idx
ON stories USING btree
(create_date ASC NULLS LAST,
num_views ASC NULLS LAST, user_id ASC NULLS LAST)
;
CREATE INDEX fulltext_search_idx
ON stories USING gin
(fulltext)
;
创建Spring Boot应用
- 项目依赖关系(这里使用Gradle构建):
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.3'
id 'io.spring.dependency-management' version '1.1.3'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yaml
中配置数据库连接信息
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: postgres
- 数据模型
定义需要用到的各种数据模型:
public record Period(String fieldName, LocalDateTime min, LocalDateTime max) {
}
public record Range(String fieldName, long min, long max) {
}
public record Search(List<Period> periods, List<Range> ranges, String fullText, long offset, long limit) {
}
public record UserStory(Long id, LocalDateTime createDate, Long numberOfViews,
String title, String body, Long userRating, String userName, Long userId) {
}
这里使用Java 16推出的新特性record实现,所以代码非常简洁。如果您还不了解的话,可以前往程序猿DD的Java新特性专栏补全一下知识点。
- 数据访问(Repository)
@Repository
public class UserStoryRepository {
private final JdbcTemplate jdbcTemplate;
@Autowired
public UserStoryRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<UserStory> findByFilters(Search search) {
return jdbcTemplate.query(
"""
SELECT s.id id, create_date, num_views,
title, body, user_id, name user_name,
rating user_rating
FROM stories s INNER JOIN users u
ON s.user_id = u.id
WHERE true
""" + buildDynamicFiltersText(search)
+ " order by create_date desc offset ? limit ?",
(rs, rowNum) -> new UserStory(
rs.getLong("id"),
rs.getTimestamp("create_date").toLocalDateTime(),
rs.getLong("num_views"),
rs.getString("title"),
rs.getString("body"),
rs.getLong("user_rating"),
rs.getString("user_name"),
rs.getLong("user_id")
),
buildDynamicFilters(search)
);
}
public void save(UserStory userStory) {
var keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection
.prepareStatement(
"""
INSERT INTO stories (create_date, num_views, title, body, user_id)
VALUES (?, ?, ?, ?, ?)
""",
Statement.RETURN_GENERATED_KEYS
);
ps.setTimestamp(1, Timestamp.valueOf(userStory.createDate()));
ps.setLong(2, userStory.numberOfViews());
ps.setString(3, userStory.title());
ps.setString(4, userStory.body());
ps.setLong(5, userStory.userId());
return ps;
}, keyHolder);
var generatedId = (Long) keyHolder.getKeys().get("id");
if (generatedId != null) {
updateFullTextField(generatedId);
}
}
private void updateFullTextField(Long generatedId) {
jdbcTemplate.update(
"""
UPDATE stories SET fulltext = to_tsvector(title || ' ' || body)
where id = ?
""",
generatedId
);
}
private Object[] buildDynamicFilters(Search search) {
var filtersStream = search.ranges().stream()
.flatMap(
range -> Stream.of((Object) range.min(), range.max())
);
var periodsStream = search.periods().stream()
.flatMap(
range -> Stream.of((Object) Timestamp.valueOf(range.min()), Timestamp.valueOf(range.max()))
);
filtersStream = Stream.concat(filtersStream, periodsStream);
if (!search.fullText().isBlank()) {
filtersStream = Stream.concat(filtersStream, Stream.of(search.fullText()));
}
filtersStream = Stream.concat(filtersStream, Stream.of(search.offset(), search.limit()));
return filtersStream.toArray();
}
private String buildDynamicFiltersText(Search search) {
var rangesFilterString =
Stream.concat(
search.ranges()
.stream()
.map(
range -> String.format(" and %s between ? and ? ", range.fieldName())
),
search.periods()
.stream()
.map(
range -> String.format(" and %s between ? and ? ", range.fieldName())
)
)
.collect(Collectors.joining(" "));
return rangesFilterString + buildFulltextFilterText(search.fullText());
}
private String buildFulltextFilterText(String fullText) {
return fullText.isBlank() ? "" : " and fulltext @@ plainto_tsquery(?) ";
}
}
- Controller实现
@RestController
@RequestMapping("/user-stories")
public class UserStoryController {
private final UserStoryRepository userStoryRepository;
@Autowired
public UserStoryController(UserStoryRepository userStoryRepository) {
this.userStoryRepository = userStoryRepository;
}
@PostMapping
public void save(@RequestBody UserStory userStory) {
userStoryRepository.save(userStory);
}
@PostMapping("/search")
public List<UserStory> search(@RequestBody Search search) {
return userStoryRepository.findByFilters(search);
}
}
小结
本文介绍了如何在Spring Boot中结合Postgres数据库实现全文搜索的功能,该方法比起使用Elasticsearch更为轻量级,非常适合一些小项目场景使用。希望本文内容对您有所帮助。如果您学习过程中如遇困难?可以加入我们超高质量的Spring技术交流群,参与交流与讨论,更好的学习与进步!更多Spring Boot教程可以点击直达!,欢迎收藏与转发支持!
参考资料
欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源
Spring Boot整合Postgres实现轻量级全文搜索的更多相关文章
- Spring Boot整合Elasticsearch
Spring Boot整合Elasticsearch Elasticsearch是一个全文搜索引擎,专门用于处理大型数据集.根据描述,自然而然使用它来存储和搜索应用程序日志.与Logstash和K ...
- Spring Boot 整合 Elasticsearch,实现 function score query 权重分查询
摘要: 原创出处 www.bysocket.com 「泥瓦匠BYSocket 」欢迎转载,保留摘要,谢谢! 『 预见未来最好的方式就是亲手创造未来 – <史蒂夫·乔布斯传> 』 运行环境: ...
- Spring Boot 整合 Shiro ,两种方式全总结!
在 Spring Boot 中做权限管理,一般来说,主流的方案是 Spring Security ,但是,仅仅从技术角度来说,也可以使用 Shiro. 今天松哥就来和大家聊聊 Spring Boot ...
- Spring Boot2 系列教程(三十二)Spring Boot 整合 Shiro
在 Spring Boot 中做权限管理,一般来说,主流的方案是 Spring Security ,但是,仅仅从技术角度来说,也可以使用 Shiro. 今天松哥就来和大家聊聊 Spring Boot ...
- Spring Boot整合EhCache
本文讲解Spring Boot与EhCache的整合. 1 EhCache简介 EhCache 是一个纯Java的进程内缓存框架,具有快速.精干等特点,是Hibernate中默认CacheProvid ...
- Spring Boot 整合 Kafka
Kafka 环境搭建 kafka 安装.配置.启动.测试说明: 1. 安装:直接官网下载安装包,解压到指定位置即可(kafka 依赖的 Zookeeper 在文件中已包含) 下载地址:https:// ...
- Spring Boot 整合 xxl-job
官方文档:https://www.xuxueli.com/xxl-job/ XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速.学习简单.轻量级.易扩展.现已开放源代码并接入多家公司线 ...
- spring boot整合jsp的那些坑(spring boot 学习笔记之三)
Spring Boot 整合 Jsp 步骤: 1.新建一个spring boot项目 2.修改pom文件 <dependency> <groupId>or ...
- spring boot 系列之四:spring boot 整合JPA
上一篇我们讲了spring boot 整合JdbcTemplate来进行数据的持久化, 这篇我们来说下怎么通过spring boot 整合JPA来实现数据的持久化. 一.代码实现 修改pom,引入依赖 ...
- Spring Kafka和Spring Boot整合实现消息发送与消费简单案例
本文主要分享下Spring Boot和Spring Kafka如何配置整合,实现发送和接收来自Spring Kafka的消息. 先前我已经分享了Kafka的基本介绍与集群环境搭建方法.关于Kafka的 ...
随机推荐
- C++编译器选择是否自动生成代码的背后逻辑
C++编译器选择是否自动生成代码的背后逻辑 编译器会为class和struct(实际上两者在C++中是一回事)自动生成构造函数.赋值操作符函数和析构函数.如果不是这样,那么开发者就必须自己写一些枯燥冗 ...
- 基于java+springboot的求职招聘网站-求职招聘管理系统
该系统是基于java+springboot开发的求职招聘网站.网上招聘管理系统.网上人才招聘系统.毕业生求职招聘系统.大学生求职招聘系统.校园招聘系统.企业招聘系统.是给师弟开发的毕业设计.大家学习过 ...
- AHB-SRAMC Design-02
AHB-SRAMC Design SRAMC(另外一种代码风格)解析 SRAM集成,顶层模块尽量不要写交互逻辑 module ahb_slave_if( input hclk, input hrest ...
- org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 2
1.报错 在运行SpringBoot项目时遇到报错: 17:44:47.558 [main] ERROR org.springframework.boot.SpringApplication -- A ...
- [转帖]redis缓存命中率介绍
缓存命中率的介绍 命中:可以直接通过缓存获取到需要的数据. 不命中:无法直接通过缓存获取到想要的数据,需要再次查询数据库或者执行其它的操作.原因可能是由于缓存中根本不存在,或者缓存已经过期. 通常来讲 ...
- 人大金仓学习之四-kmonitor
人大金仓学习之四-kmonitor 背景 kmonitor 其实时一个很好的工具和思路 开元的软件封装一下, 减轻技术复杂度,提高部署效率 并且能够更加快速的定位解决问题. 能够极大的提升客户体验. ...
- [转帖]PostgreSQL 的性能调优方法
https://juejin.cn/post/7119489847529570334 浅谈PostgreSQL的性能调校 PostgreSQL的性能调校是指调校数据库以提高性能和快速访问数据:我们可以 ...
- [转帖]Kafka需要知道的一些基础知识点
https://blog.csdn.net/daima_caigou/article/details/109101405 前言 kafka是常用MQ的一种,站在使用者的角度来看待,kafka以及所有的 ...
- Python学习之十_paramiko的简单学习
Python学习之十_paramiko的简单学习 简介 pywinrm 是python用于连接访问windows的工具 paramiko 是python用于连接访问linux的工具 ansible等工 ...
- [转帖]UseG1GC垃圾回收技术解析
https://www.cnblogs.com/yuanzipeng/p/13374690.html 介绍 G1 GC,全称Garbage-First Garbage Collector,通过-XX: ...