有这样一个带有搜索功能的用户界面需求:

搜索流程如下所示:

这个需求涉及两个实体:

  • “评分(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应用

  1. 项目依赖关系(这里使用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()
}
  1. application.yaml中配置数据库连接信息
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: postgres
  1. 数据模型

定义需要用到的各种数据模型:

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新特性专栏补全一下知识点。

  1. 数据访问(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(?) ";
}
}
  1. 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实现轻量级全文搜索的更多相关文章

  1. Spring Boot整合Elasticsearch

    Spring Boot整合Elasticsearch   Elasticsearch是一个全文搜索引擎,专门用于处理大型数据集.根据描述,自然而然使用它来存储和搜索应用程序日志.与Logstash和K ...

  2. Spring Boot 整合 Elasticsearch,实现 function score query 权重分查询

    摘要: 原创出处 www.bysocket.com 「泥瓦匠BYSocket 」欢迎转载,保留摘要,谢谢! 『 预见未来最好的方式就是亲手创造未来 – <史蒂夫·乔布斯传> 』 运行环境: ...

  3. Spring Boot 整合 Shiro ,两种方式全总结!

    在 Spring Boot 中做权限管理,一般来说,主流的方案是 Spring Security ,但是,仅仅从技术角度来说,也可以使用 Shiro. 今天松哥就来和大家聊聊 Spring Boot ...

  4. Spring Boot2 系列教程(三十二)Spring Boot 整合 Shiro

    在 Spring Boot 中做权限管理,一般来说,主流的方案是 Spring Security ,但是,仅仅从技术角度来说,也可以使用 Shiro. 今天松哥就来和大家聊聊 Spring Boot ...

  5. Spring Boot整合EhCache

    本文讲解Spring Boot与EhCache的整合. 1 EhCache简介 EhCache 是一个纯Java的进程内缓存框架,具有快速.精干等特点,是Hibernate中默认CacheProvid ...

  6. Spring Boot 整合 Kafka

    Kafka 环境搭建 kafka 安装.配置.启动.测试说明: 1. 安装:直接官网下载安装包,解压到指定位置即可(kafka 依赖的 Zookeeper 在文件中已包含) 下载地址:https:// ...

  7. Spring Boot 整合 xxl-job

    官方文档:https://www.xuxueli.com/xxl-job/ XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速.学习简单.轻量级.易扩展.现已开放源代码并接入多家公司线 ...

  8. spring boot整合jsp的那些坑(spring boot 学习笔记之三)

    Spring Boot 整合 Jsp 步骤: 1.新建一个spring boot项目 2.修改pom文件 <dependency>            <groupId>or ...

  9. spring boot 系列之四:spring boot 整合JPA

    上一篇我们讲了spring boot 整合JdbcTemplate来进行数据的持久化, 这篇我们来说下怎么通过spring boot 整合JPA来实现数据的持久化. 一.代码实现 修改pom,引入依赖 ...

  10. Spring Kafka和Spring Boot整合实现消息发送与消费简单案例

    本文主要分享下Spring Boot和Spring Kafka如何配置整合,实现发送和接收来自Spring Kafka的消息. 先前我已经分享了Kafka的基本介绍与集群环境搭建方法.关于Kafka的 ...

随机推荐

  1. FGC频繁导致CPU 飙升定位及JVM配置优化总结

    本文为博主原创,未经允许不得转载: 目录: 1. 定位消耗cpu 的服务进程和线程 2. 定位FGC 的原因 3. 定位jvm 参数是否导致FGC 4. 调试最优解的 jvm 配置 描述:项目中存在一 ...

  2. Blazor的技术优点

    Blazor是一种使用.NET和C#构建客户端Web应用程序的新兴技术.它允许开发者在浏览器中直接运行.NET代码,而无需依赖JavaScript.Blazor的技术优点主要表现在以下几个方面: 单一 ...

  3. 11-verilog-有限状态机

    有限状态机 写RTL的时候,实现一个功能的时候有很多种方法 将系统划分为多个状态,状态之间有状态的转移,第一步,第二步......形成有限状态机 流水线技术设计,从输入到输出有多个步骤,多个步骤可以并 ...

  4. 安卓系统如何使用谷歌框架下的app?

    1.问题 安卓系统从理论上无法使用谷歌框架下的应用(比如像GMail,YouTube,Google play等等),会导致一些麻烦(闪退,卡在登陆界面等等) 注意:使用前提是会魔法,否则请绕道 2.解 ...

  5. Python Code_01

    author : 写bug的盼盼 development time : 2021/8/27 19:41 输出数字 print(520) 输出字符串 print('HelloWorld') print( ...

  6. 【面试题精讲】MySQL中覆盖索引是什么

    有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准https://blog.zysicyj.top 首发博客地址 系列文章地址 在MySQL中,覆盖索引是一种特殊类型的索引,它 ...

  7. canal报错nosuchmethod ..bytebuffer

    解决方法:更新jdk版本与es一致

  8. [转帖]配置 Windows XP 正常上网(TLS HTTPS),连接到 NAS

    https://zhuanlan.zhihu.com/p/208685816# 学习一下. 知乎用户8kqKq9 等 45 人赞同了该文章 Windows XP 是经典的.高效的.可靠的.性能良好的操 ...

  9. [转帖]Oracle数据库下PreparedStatementCache内存问题解决方案

    https://github.com/alibaba/druid/wiki/Oracle%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%8BPreparedStatementCac ...

  10. [转帖]深入理解mysql-第十二章 mysql查询优化-Explain 详解(下)

    我们前面两章详解了Explain的各个属性,我们看到的都是mysql已经生成的执行计划,那这个执行计划的是如何生成的?我们能看到一些过程指标数据吗?实际mysql贴心为我们提供了执行计划的各项成本评估 ...