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

搜索流程如下所示:

这个需求涉及两个实体:

  • “评分(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. Linux telnet安装及端口测试联通性

    安装步骤: 可使用该文中的步骤进行安装,已经过本人验证,是可以安装成功的: https://blog.csdn.net/doubleqinyan/article/details/80492421 安装 ...

  2. Ubuntu安装jdk的步骤

    .markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...

  3. 基于AHB_BUS的eFlash控制器设计-软硬件系统设计

    eFlash软硬件系统设计 软硬件划分 划分好软硬件之后,IP暴露给软件的寄存器和时序如何? 文档体系:详细介绍eflash控制器的设计文档 RTL代码编写:详细介绍eflash控制器的RTL代码 1 ...

  4. Go-发送邮件

    1. 邮件 - mail From -- 发送者(这封邮件由谁进行发送的,一般都是该邮件的作者) To -- 邮件的接收者(发送邮件的人希望谁能收到邮件) Subject -- 邮件的主题(类似文章的 ...

  5. [转帖]Jmeter脚本录:抓取https请求

    Jmeter抓取http请求 https://blog.csdn.net/qq19970496/article/details/86595109 代理设置步骤请参照该篇文章.本文件只做补充HTTPS中 ...

  6. 【转帖】mysql一个索引块有多少指针_深刻理解MySQL系列之索引

    索引 查找一条数据的过程 先看下InnoDB的逻辑存储结构:node 表空间:能够看作是InnoDB存储引擎逻辑结构的最高层,全部的数据都存放在表空间中.默认有个共享表空间ibdata1.若是启用in ...

  7. [转帖]解析Linux gcore: 揭示程序内存捕获的秘密(linuxgcore)

    https://www.dbs724.com/133618.html Linux gcore 是一种在Linux系统中使用命令行工具捕获进程内存内容的方法.它允许程序员制定程序的一个内存快照,从而帮助 ...

  8. 解决input标签自动填充内容的问题

    autocomplete="new-password" 巧妙解决input标签自动填充问题 如果是同域名网站,并且曾经在该网站下登录过账号密码,并且选择了记住账号密码. chrom ...

  9. 在web中,用户输入的文字过多 和页面排版一行显示不下 怎么办

    在写管理系统中, 如果用户在input中,表单输入过多,应该如何去处理 常用的解决办法是: 1==>用户只能够输入一定范围内的字数 2==>超出几个字后(宽度)使用省略号显示. 3==&g ...

  10. TortoiseGit 常见问题汇总

    1.test分支修改后合并到master分支 1)切换本地分支到master分支 2)TortoiseGit --->  merge,选择远程分支test 提交到远程分支master 2.将远程 ...