rails: 使用 fts5/fts 对 sqlite 进行全文搜索 - rails 6.0

在 rails6 下完成 sqlie3 的 fts 配置
更新于: 2024-11-24 18:16:56

原来的

确认 sqlite3 版本

# sqlie3 版本
sqlite3 --version
# rails console 确认
irb(main):001:0> ActiveRecord::Base.connection.select_value("SELECT sqlite_version()")

创建 FTS 虚拟表

由于 SQLite 的 FTS 是虚拟表,不能通过 ActiveRecord 的常规迁移直接生成,我们需要使用 execute 手动创建。

创建一个新的迁移文件:rails db:migrate 

rails generate migration CreatePostsFTS
class CreatePostsFts < ActiveRecord::Migration[6.0]
  def up
    # 创建 FTS 虚拟表
    execute <<-SQL
      CREATE VIRTUAL TABLE posts_fts USING fts5(title);
    SQL

    # 将现有的 posts 数据同步到 FTS 表
    execute <<-SQL
      INSERT INTO posts_fts(rowid, title)
      SELECT id, title FROM posts;
    SQL
  end

  def down
    # 删除 FTS 表
    execute <<-SQL
      DROP TABLE IF EXISTS posts_fts;
    SQL
  end
end

同步数据的方法

class Post < ApplicationRecord
  after_save :update_fts
  after_destroy :remove_from_fts
  
  # 设置关联
  has_one :post_fts, foreign_key: "rowid"

  private

  def update_fts
    ActiveRecord::Base.connection.execute(<<-SQL)
      INSERT INTO posts_fts(rowid, title)
      VALUES(#{id}, #{ActiveRecord::Base.connection.quote(title)})
      ON CONFLICT(rowid) DO UPDATE SET title=excluded.title;
    SQL
  end

  def remove_from_fts
    ActiveRecord::Base.connection.execute(<<-SQL)
      DELETE FROM posts_fts WHERE rowid=#{id};
    SQL
  end
end

migrate

rails db:migrate

修改查询方法

在 Rails 控制器或模型中使用 FTS 查询,将 MATCH 语法嵌入 SQL 查询:

class PostsController < ApplicationController
  def index
    keyword = params[:q] || ""
    @posts = Post
               .joins("JOIN posts_fts ON posts.id = posts_fts.rowid")
               .where("posts_fts MATCH ?", keyword)
               .order(douban_rate: :desc)
               .limit(params[:limit] || 10)
               .offset(params[:offset] || 0)
    render json: @posts
  end
end

测试你的实现

# 查询
Post.joins("JOIN posts_fts ON posts.id = posts_fts.rowid")
    .where("posts_fts MATCH ?", "设计")
    .order(douban_rate: :desc)
    .limit(10)

# 测试数据同步:
Post.create(title: "新的设计标题")
Post.last.update(title: "更新后的设计标题")
Post.last.destroy

改进 search 方法

class Post 
  def self.search(keywords)
    if keywords.present?
      joins("JOIN posts_fts ON posts.id = posts_fts.rowid")
        .where("posts_fts MATCH ?", keywords)
    else
      all
    end
  end
end

增强功能(可选)

支持多关键词查询:如果希望支持多个关键词搜索,可以对传入的 keywords 参数进行预处理,比如将空格替换为 AND

def self.search(keywords)
  if keywords.present?
    processed_keywords = keywords.split.map { |word| "\"#{word}\"" }.join(" ")
    joins("JOIN posts_fts ON posts.id = posts_fts.rowid")
      .where("posts_fts MATCH ?", processed_keywords)
  else
    all
  end
end

现在的搜索

  • 多个词空格分隔
  • 搜索速度提升明显

存在的问题

部分中文搜索不到结果,如 “唐人”没有结果 但 “唐人街” 可以

搜索 “效果” 也是无结果,但实际用 like 也是可以搜索到结果的

使用 fts 搜索 “唐人” 无结果
搜索 “唐人街” 有结果
用 like 可以搜索到结果