From 1c21d3739ba474ad0156cf72e16feb00eb158f8b Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Tue, 21 Jan 2025 01:27:35 +0100 Subject: [PATCH] Refactor Speaker indexing into a Speaker::Index record. Follow-up to https://github.com/adrienpoly/rubyvideo/pull/540 --- app/models/speaker/index.rb | 27 +++++++++++ app/models/speaker/searchable.rb | 48 +++++-------------- ...2175230_create_speaker_full_text_search.rb | 2 +- test/test_helper.rb | 2 +- 4 files changed, 41 insertions(+), 38 deletions(-) create mode 100644 app/models/speaker/index.rb diff --git a/app/models/speaker/index.rb b/app/models/speaker/index.rb new file mode 100644 index 00000000..46a35339 --- /dev/null +++ b/app/models/speaker/index.rb @@ -0,0 +1,27 @@ +class Speaker::Index < ApplicationRecord + self.table_name = :speakers_search_index + + include ActiveRecord::SQLite::Index # Depends on `table_name` being assigned. + class_attribute :index_columns, default: {name: 0, github: 1} + + belongs_to :speaker, foreign_key: :rowid + + def self.search(query) + query = query&.gsub(/[^[:word:]]/, " ") || "" # remove non-word characters + query = query.split.map { |word| "#{word}*" }.join(" ") # wildcard search + where("#{table_name} match ?", query) + end + + def self.snippets(**) + index_columns.each_key.reduce(all) { |relation, column| relation.snippet(column, **) } + end + + def self.snippet(column, tag: "mark", omission: "…", limit: 32) + offset = index_columns.fetch(column) + select("snippet(#{table_name}, #{offset}, '<#{tag}>', '', '#{omission}', #{limit}) AS #{column}_snippet") + end + + def reindex + update! id: speaker.id, name: speaker.name, github: speaker.github + end +end diff --git a/app/models/speaker/searchable.rb b/app/models/speaker/searchable.rb index 08149a8e..528a331d 100644 --- a/app/models/speaker/searchable.rb +++ b/app/models/speaker/searchable.rb @@ -2,38 +2,27 @@ module Speaker::Searchable extend ActiveSupport::Concern included do - scope :ft_search, ->(query) do - query = query&.gsub(/[^[:word:]]/, " ") || "" # remove non-word characters - query = query.split.map { |word| "#{word}*" }.join(" ") # wildcard search - joins("join speakers_search_index idx on speakers.id = idx.rowid") - .where("speakers_search_index match ?", query) - end + has_one :index, foreign_key: :rowid, dependent: :destroy + + scope :ft_search, ->(query) { select("speakers.*").joins(:index).merge(Speaker::Index.search(query)) } scope :with_snippets, ->(**options) do - select("speakers.*") - .select_snippet("name", 0, **options) - .select_snippet("github", 1, **options) + select("speakers.*").merge(Speaker::Index.snippets(**options)) end scope :ranked, -> do select("speakers.*, bm25(speakers_search_index, 2, 1) AS combined_score") - .order(Arel.sql("combined_score ASC")) + .order(combined_score: :asc) end - after_create_commit :create_in_index - after_update_commit :update_in_index - after_destroy_commit :remove_from_index + after_save_commit :reindex end class_methods do - def rebuild_search_index - connection.execute("DELETE FROM speakers_search_index") - Speaker.find_each(&:create_in_index) - end - - def select_snippet(column, offset, tag: "mark", omission: "…", limit: 32) - select("snippet(speakers_search_index, #{offset}, '<#{tag}>', '', '#{omission}', #{limit}) AS #{column}_snippet") + def reindex_all + Speaker::Index.delete_all + Speaker.find_each(&:reindex) end end @@ -41,21 +30,8 @@ def name_with_snippet try(:name_snippet) || name end - def create_in_index - execute_sql_with_binds "insert into speakers_search_index(rowid, name, github) values (?, ?, ?)", id, name, github - end - - def update_in_index - execute_sql_with_binds "update speakers_search_index set name = ?, github = ? where rowid = ?", name, github, id - end - - def remove_from_index - execute_sql_with_binds "delete from speakers_search_index where rowid = ?", id - end - - private - - def execute_sql_with_binds(*statement) - self.class.connection.execute self.class.sanitize_sql(statement) + def index + super || build_index end + delegate :reindex, to: :index end diff --git a/db/migrate/20250102175230_create_speaker_full_text_search.rb b/db/migrate/20250102175230_create_speaker_full_text_search.rb index 50ca6cc8..516abab9 100644 --- a/db/migrate/20250102175230_create_speaker_full_text_search.rb +++ b/db/migrate/20250102175230_create_speaker_full_text_search.rb @@ -4,7 +4,7 @@ def up "name", "github", "tokenize = porter" ] - Speaker.rebuild_search_index + Speaker.reindex_all end def down diff --git a/test/test_helper.rb b/test/test_helper.rb index b94fea19..d7b98590 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,7 +23,7 @@ class ActiveSupport::TestCase # end Talk.reindex_all - Speaker.rebuild_search_index + Speaker.reindex_all end # Run tests in parallel with specified workers parallelize(workers: :number_of_processors)