Spring Boot整合Postgres实现轻量级全文搜索

这篇具有很好参考价值的文章主要介绍了Spring Boot整合Postgres实现轻量级全文搜索。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

Spring Boot整合Postgres实现轻量级全文搜索

搜索流程如下所示:

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应用

  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教程可以点击直达!,欢迎收藏与转发支持!

参考资料

  • Postgres full-text search Spring boot integration
  • Java 16新特性:record

欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源文章来源地址https://www.toymoban.com/news/detail-826970.html

到了这里,关于Spring Boot整合Postgres实现轻量级全文搜索的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《Java Web轻量级整合开发入门》学习笔记

    轻量级Java Web整合开发 第一章 轻量级Java Web开发概述 1.2  java web 开发概述 1.JSP是一种编译执行的前台页面技术。对于每个JSP页面,Web服务器都会生成一个相应的Java文件,然后再编译该Java文件,生成相应的Class类型文件。在客户端访问到的JSP页面,就是相应Class文件执行的结果

    2024年02月08日
    浏览(58)
  • 简要介绍Spring原生框架与Spring是轻量级框架的原因

    😉😉  学习交流群: ✅✅ 1: 这是孙哥suns给大家的福利! ✨✨ 2:我们 免费分享 Netty、Dubbo、k8s、Mybatis、Spring... 应用和源码级别 的 视频资料 🥭🥭 3:QQ群: 583783824     📚📚  工作微信: BigTreeJava 拉你进微信群,免费领取! 🍎🍎 4:本文章内容出自上述:Spring应用课

    2024年02月05日
    浏览(166)
  • 【Spring Cloud系统】- 轻量级高可用工具Keepalive详解

    Keepalive是Linux下一个轻量级高可用解决方案。高可用(High Avaliability简称HA)就是主机的冗余和接管。 基本功能 :心跳检测、资源接管、检测集群中的服务,在集群结点共享IP地址的所有者。 Keepalive主要是通过路由冗余来实现高可用功能,配置简单,只需要一个配置文件即可

    2024年02月09日
    浏览(42)
  • 一种轻量级定时任务实现

    现在市面上有各式各样的分布式定时任务,每个都有其独特的特点,我们这边的项目因为一开始使用的是分布式开源调度框架TBSchedule,但是这个框架依赖ZK, 由于ZK的不稳定性和项目老旧无人维护 ,导致我们的定时任务会偶发出现异常,比如:任务停止、任务项丢失、任务不

    2024年02月14日
    浏览(48)
  • 一种轻量级websocket实现方案

    定义ws服务器工具类WsktUtil 开机启动ws服务器 测试结果 自定义一个WebSocketClient子类 测试连接ws服务器 测试效果

    2024年02月15日
    浏览(41)
  • 使用es实现轻量级分布式锁

    一般来说,实现分布式锁的方式有哪几种? 一:Redisson实现 二:ZK实现   这两种实现网上的实现是千篇一律,在本文就不做过多的讲解了   其它方式好像没有了,真的是这样么?   答案是否定的,今天我就给大家分享一个新的思路,使用es实现一个分布式锁,分布式

    2024年02月06日
    浏览(68)
  • 轻量级软件FastGithub实现稳定访问github

    当我们想访问全球最大的“同性交友网站”https://github.com/ 时,总会出现无法访问的界面,令人非常苦恼: 幸运的是,有一种轻量级的软件可以帮助我们稳定地访问GitHub,那就是FastGithub。 FastGithub是一个简洁且专一的软件,它可以帮助你稳定地访问GitHub。FastGithub通过修改本地

    2024年02月06日
    浏览(49)
  • [Netty源码] Netty轻量级对象池实现分析 (十三)

    1.对象池技术介绍 对象池其实就是缓存一些对象从而避免大量创建同一个类型的对象, 类似线程池。对象池缓存了一些已经创建好的对象, 避免需要的时候创建。同时限制了实例的个数。 池化技术最终要的就是重复的使用池内已经创建的对象。 创建对象的开销大 会创建大量的

    2023年04月18日
    浏览(47)
  • OpenHarmony实战开发-如何实现一个轻量级输入法应用。

    ​ 本示例使用inputMethodEngine实现一个轻量级输入法应用kikaInput,支持在运行OpenHarmony OS的智能终端上。 使用说明 1.使用hdc shell aa start ability -a InputMethod -b cn.openharmony.inputmethodchoosedialog命令拉起切换输入法弹窗,点击kikainput切换输入法到当前应用。 2.点击应用中的编辑框,拉起

    2024年04月24日
    浏览(60)
  • 教你使用PHP实现一个轻量级HTML模板引擎

    🏆作者简介,黑夜开发者,全栈领域新星创作者✌,2023年6月csdn上海赛道top4。多年电商行业从业经验,对系统架构,数据分析处理等大规模应用场景有丰富经验。 🏆本文已收录于PHP专栏:PHP进阶实战教程。 🏆另有专栏PHP入门基础教程,希望各位大佬多多支持❤️。 在 W

    2024年02月15日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包