94 lines
3.5 KiB
Ruby
94 lines
3.5 KiB
Ruby
module Search::Record::SQLite
|
|
extend ActiveSupport::Concern
|
|
|
|
included do
|
|
self.table_name = "search_records"
|
|
|
|
# Override the UUID id attribute from ApplicationRecord
|
|
# SQLite uses integer auto-increment primary key (no default)
|
|
attribute :id, :integer, default: nil
|
|
|
|
after_save :upsert_to_fts5_table
|
|
after_destroy :delete_from_fts5_table
|
|
end
|
|
|
|
class_methods do
|
|
def for_account(account_id)
|
|
# SQLite uses a single table, no sharding
|
|
self
|
|
end
|
|
|
|
def matching_scope(query)
|
|
# SQLite FTS5: join on rowid for fast lookup with native highlighting
|
|
# Porter tokenizer handles stemming automatically
|
|
joins("INNER JOIN search_records_fts ON search_records_fts.rowid = #{table_name}.id")
|
|
.where("search_records_fts MATCH ?", query)
|
|
end
|
|
|
|
def search_scope(relation, query)
|
|
# SQLite: matching scope already selected all columns + FTS5 highlight columns
|
|
# Re-select to add query terms (ActiveRecord replaces the select list)
|
|
opening_mark = Search::Highlighter::OPENING_MARK
|
|
closing_mark = Search::Highlighter::CLOSING_MARK
|
|
ellipsis = Search::Highlighter::ELIPSIS
|
|
|
|
relation.select(
|
|
"#{table_name}.id",
|
|
"#{table_name}.account_id",
|
|
"#{table_name}.searchable_type",
|
|
"#{table_name}.searchable_id",
|
|
"#{table_name}.card_id",
|
|
"#{table_name}.board_id",
|
|
"#{table_name}.title",
|
|
"#{table_name}.content",
|
|
"#{table_name}.created_at",
|
|
"highlight(search_records_fts, 0, #{connection.quote(opening_mark)}, #{connection.quote(closing_mark)}) AS highlighted_title",
|
|
"highlight(search_records_fts, 1, #{connection.quote(opening_mark)}, #{connection.quote(closing_mark)}) AS highlighted_content",
|
|
"snippet(search_records_fts, 1, #{connection.quote(opening_mark)}, #{connection.quote(closing_mark)}, #{connection.quote(ellipsis)}, 20) AS content_snippet",
|
|
"#{connection.quote(query.terms)} AS query"
|
|
)
|
|
end
|
|
end
|
|
|
|
def card_title
|
|
if card_id
|
|
# Use FTS5 native highlighting (already HTML-safe from FTS5)
|
|
# Fall back to card.title if highlight is nil (e.g., for comment matches where title is NULL)
|
|
if has_attribute?(:highlighted_title) && highlighted_title.present?
|
|
highlighted_title.html_safe
|
|
else
|
|
card.title
|
|
end
|
|
end
|
|
end
|
|
|
|
def card_description
|
|
if card_id && has_attribute?(:content_snippet) && content_snippet.present?
|
|
# Use FTS5 native snippet for content (already HTML-safe from FTS5)
|
|
content_snippet.html_safe
|
|
end
|
|
end
|
|
|
|
def comment_body
|
|
if comment && has_attribute?(:content_snippet) && content_snippet.present?
|
|
# Use FTS5 native snippet for content (already HTML-safe from FTS5)
|
|
content_snippet.html_safe
|
|
end
|
|
end
|
|
|
|
private
|
|
def upsert_to_fts5_table
|
|
# Use raw unstemmed text - FTS5 Porter tokenizer handles stemming automatically
|
|
# Note: FTS5 virtual tables don't work properly with bound parameters in SQLite,
|
|
# so we need to use string interpolation with proper quoting
|
|
self.class.connection.execute(
|
|
"INSERT OR REPLACE INTO search_records_fts(rowid, title, content) VALUES (#{id}, #{self.class.connection.quote(title)}, #{self.class.connection.quote(content)})"
|
|
)
|
|
end
|
|
|
|
def delete_from_fts5_table
|
|
# Note: Use string interpolation for consistency (rowid is always an integer, so safe)
|
|
self.class.connection.execute("DELETE FROM search_records_fts WHERE rowid = #{id}")
|
|
end
|
|
end
|