Support SQLite searching

This commit is contained in:
Donal McBreen
2025-11-20 13:40:14 +00:00
parent a2333d9a37
commit bb36b4846f
13 changed files with 925 additions and 37 deletions
+70 -12
View File
@@ -13,21 +13,79 @@ module Searchable
private
def create_in_search_index
Search::Record.for_account(account_id).create!(search_record_attributes)
search_class = Search::Record.for_account(account_id)
if Search::Record.sqlite?
# SQLite: create with unstemmed content, FTS5 handles stemming
record = search_class.create!(search_record_attributes)
upsert_to_fts5(record.id)
else
# MySQL: create with stemmed content for FULLTEXT search
attrs = search_record_attributes.merge(
title: Search::Stemmer.stem(search_record_attributes[:title]),
content: Search::Stemmer.stem(search_record_attributes[:content])
)
search_class.create!(attrs)
end
end
def update_in_search_index
Search::Record.for_account(account_id).upsert_all(
[ search_record_attributes.merge(id: ActiveRecord::Type::Uuid.generate) ],
update_only: [ :card_id, :board_id, :title, :content, :created_at ]
)
search_class = Search::Record.for_account(account_id)
if Search::Record.sqlite?
# SQLite: find or create record, then upsert to FTS5
record = search_class.find_or_initialize_by(
searchable_type: self.class.name,
searchable_id: id
)
record.assign_attributes(search_record_attributes)
record.save!
upsert_to_fts5(record.id)
else
# MySQL: use upsert_all with stemmed content
attrs = search_record_attributes.merge(
id: ActiveRecord::Type::Uuid.generate,
title: Search::Stemmer.stem(search_record_attributes[:title]),
content: Search::Stemmer.stem(search_record_attributes[:content])
)
search_class.upsert_all(
[ attrs ],
update_only: [ :card_id, :board_id, :title, :content, :created_at ]
)
end
end
def remove_from_search_index
Search::Record.for_account(account_id).where(
searchable_type: self.class.name,
searchable_id: id
).delete_all
search_class = Search::Record.for_account(account_id)
record = search_class.find_by(searchable_type: self.class.name, searchable_id: id)
if record
# For SQLite, delete from FTS5 first
if Search::Record.sqlite?
delete_from_fts5(record.id)
end
record.delete
end
end
def upsert_to_fts5(record_id)
# Use raw unstemmed text - FTS5 Porter tokenizer handles stemming automatically
title = search_title
content = search_content
# Note: FTS5 virtual tables don't work properly with bound parameters in SQLite,
# so we need to use string interpolation with proper quoting
conn = ActiveRecord::Base.connection
sql = "INSERT OR REPLACE INTO search_records_fts(rowid, title, content) VALUES (#{record_id}, #{conn.quote(title)}, #{conn.quote(content)})"
conn.execute(sql)
end
def delete_from_fts5(record_id)
# Note: Use string interpolation for consistency (rowid is always an integer, so safe)
ActiveRecord::Base.connection.execute(
"DELETE FROM search_records_fts WHERE rowid = #{record_id}"
)
end
def search_record_attributes
@@ -37,9 +95,9 @@ module Searchable
searchable_id: id,
card_id: search_card_id,
board_id: search_board_id,
title: Search::Stemmer.stem(search_title),
content: Search::Stemmer.stem(search_content),
created_at: created_at
title: search_title,
content: search_content,
created_at: created_at || Time.current
}
end