Files
fizzy/app/models/search.rb
T
Donal McBreen e8abc66eba MySQL search
Add a single search_index table for full-text search of cards and
comments.

For the search there is a full-text index on the title and content
columns. The board_ids is also included in the table and accessible
board ids are pre-loaded and included in the search query. This allows
us to filter out inaccessible records before joining with other tables.

Right now the search is just using boolean search. This would give us
a bunch of syntax options
(see https://dev.mysql.com/doc/refman/8.4/en/fulltext-boolean.html)
except the search query filters those out.

I've removed the searchable_by method for now - everything is built
on the assumption that there's a single search index table and all data
must fit into it.

Queries are written in SQL, we don't have a SearchIndex ActiveRecord
model. That's because we'll likely want to shard the table and it will
be simpler to just keep with the raw SQL for that.

There's no stemming, highlighting or snippet extraction yet - we are
dumping the full description in the search results.

Data can be reindexed with the search:reindex rake task.
2025-11-17 09:12:17 -05:00

52 lines
1.8 KiB
Ruby

class Search
attr_reader :user, :query
HIGHLIGHT_OPENING_MARK = "<mark class=\"circled-text\"><span></span>"
HIGHLIGHT_CLOSING_MARK = "</mark>"
def self.table_name_prefix
"search_"
end
def initialize(user, query)
@user = user
@query = Query.wrap(query)
end
def results
if query.valid? && board_ids.any?
perform_search
else
Search::Result.none
end
end
private
def board_ids
@board_ids ||= user.board_ids
end
def perform_search
query_string = query.to_s
sanitized_query = Search::Result.connection.quote(query_string)
Search::Result.from("search_index")
.joins("INNER JOIN cards ON search_index.card_id = cards.id")
.joins("INNER JOIN boards ON cards.board_id = boards.id")
.where("search_index.board_id IN (?)", board_ids)
.where("MATCH(search_index.content, search_index.title) AGAINST(? IN BOOLEAN MODE)", query_string)
.select([
"search_index.card_id as card_id",
"CASE WHEN search_index.searchable_type = 'Comment' THEN search_index.searchable_id ELSE NULL END as comment_id",
"COALESCE(search_index.title, cards.title) AS card_title_in_database",
"CASE WHEN search_index.searchable_type = 'Card' THEN search_index.content ELSE NULL END AS card_description_in_database",
"CASE WHEN search_index.searchable_type = 'Comment' THEN search_index.content ELSE NULL END AS comment_body_in_database",
"boards.name as board_name",
"cards.creator_id",
"search_index.created_at as created_at",
"MATCH(search_index.content, search_index.title) AGAINST(#{sanitized_query} IN BOOLEAN MODE) AS score"
].join(","))
.order("search_index.created_at DESC")
end
end