Replace Search::Index with Search::Records
Lean on ActiveRecord models for searching and strip out the raw SQL. Replaces the search_index_* tables with sharded search_records_* tables as that allows us to use a Search::Record model name. A Class is dynamically created for each record table shard so that we and we can access it via the Search::Record.for_account(account_id) method.
This commit is contained in:
committed by
Mike Dalessio
parent
fe6df7085f
commit
28efe28f24
@@ -5,37 +5,26 @@ module Card::Searchable
|
||||
include ::Searchable
|
||||
|
||||
scope :mentioning, ->(query, user:) do
|
||||
query = Search::Query.wrap(query)
|
||||
search_records = Search::Record.for_account(user.account_id)
|
||||
|
||||
if query.valid?
|
||||
table_name = Searchable.search_index_table_name(user.account_id)
|
||||
uuid_type = ActiveRecord::Type.lookup(:uuid, adapter: :trilogy)
|
||||
serialized_board_ids = user.board_ids.map { |id| uuid_type.serialize(id) }
|
||||
|
||||
joins("INNER JOIN #{table_name} ON #{table_name}.card_id = cards.id AND #{table_name}.board_id = cards.board_id")
|
||||
.where("#{table_name}.board_id IN (?)", serialized_board_ids)
|
||||
.where("MATCH(#{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", query.to_s)
|
||||
.distinct
|
||||
else
|
||||
none
|
||||
end
|
||||
joins(search_records.card_join)
|
||||
.merge(search_records.for_query(query: Search::Query.wrap(query), user: user))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def search_title
|
||||
Search::Stemmer.stem title
|
||||
end
|
||||
def search_title
|
||||
title
|
||||
end
|
||||
|
||||
def search_content
|
||||
Search::Stemmer.stem description.to_plain_text
|
||||
end
|
||||
def search_content
|
||||
description.to_plain_text
|
||||
end
|
||||
|
||||
def search_card_id
|
||||
id
|
||||
end
|
||||
def search_card_id
|
||||
id
|
||||
end
|
||||
|
||||
def search_board_id
|
||||
board_id
|
||||
end
|
||||
def search_board_id
|
||||
board_id
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,20 +5,19 @@ module Comment::Searchable
|
||||
include ::Searchable
|
||||
end
|
||||
|
||||
private
|
||||
def search_title
|
||||
nil
|
||||
end
|
||||
def search_title
|
||||
nil
|
||||
end
|
||||
|
||||
def search_content
|
||||
Search::Stemmer.stem body.to_plain_text
|
||||
end
|
||||
def search_content
|
||||
body.to_plain_text
|
||||
end
|
||||
|
||||
def search_card_id
|
||||
card_id
|
||||
end
|
||||
def search_card_id
|
||||
card_id
|
||||
end
|
||||
|
||||
def search_board_id
|
||||
card.board_id
|
||||
end
|
||||
def search_board_id
|
||||
card.board_id
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
module Searchable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def self.search_index_table_name(account_id)
|
||||
"search_index_#{Zlib.crc32(account_id) % 16}"
|
||||
end
|
||||
|
||||
included do
|
||||
after_create_commit :create_in_search_index
|
||||
after_update_commit :update_in_search_index
|
||||
@@ -17,54 +13,40 @@ module Searchable
|
||||
|
||||
private
|
||||
def create_in_search_index
|
||||
table_name = Searchable.search_index_table_name(account_id)
|
||||
uuid_type = ActiveRecord::Type.lookup(:uuid, adapter: :trilogy)
|
||||
|
||||
self.class.connection.execute self.class.sanitize_sql([
|
||||
"INSERT INTO #{table_name} (id, searchable_type, searchable_id, card_id, board_id, title, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
uuid_type.serialize(ActiveRecord::Type::Uuid.generate),
|
||||
self.class.name,
|
||||
uuid_type.serialize(id),
|
||||
uuid_type.serialize(search_card_id),
|
||||
uuid_type.serialize(search_board_id),
|
||||
search_title,
|
||||
search_content,
|
||||
created_at
|
||||
])
|
||||
Search::Record.for_account(account_id).create!(search_record_attributes)
|
||||
end
|
||||
|
||||
def update_in_search_index
|
||||
table_name = Searchable.search_index_table_name(account_id)
|
||||
uuid_type = ActiveRecord::Type.lookup(:uuid, adapter: :trilogy)
|
||||
|
||||
result = self.class.connection.execute(self.class.sanitize_sql([
|
||||
"UPDATE #{table_name} SET card_id = ?, board_id = ?, title = ?, content = ?, created_at = ? WHERE searchable_type = ? AND searchable_id = ?",
|
||||
uuid_type.serialize(search_card_id),
|
||||
uuid_type.serialize(search_board_id),
|
||||
search_title,
|
||||
search_content,
|
||||
created_at,
|
||||
self.class.name,
|
||||
uuid_type.serialize(id)
|
||||
]))
|
||||
|
||||
create_in_search_index if result.affected_rows == 0
|
||||
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]
|
||||
)
|
||||
end
|
||||
|
||||
def remove_from_search_index
|
||||
table_name = Searchable.search_index_table_name(account_id)
|
||||
uuid_type = ActiveRecord::Type.lookup(:uuid, adapter: :trilogy)
|
||||
|
||||
self.class.connection.execute self.class.sanitize_sql([
|
||||
"DELETE FROM #{table_name} WHERE searchable_type = ? AND searchable_id = ?",
|
||||
self.class.name,
|
||||
uuid_type.serialize(id)
|
||||
])
|
||||
Search::Record.for_account(account_id).where(
|
||||
searchable_type: self.class.name,
|
||||
searchable_id: id
|
||||
).delete_all
|
||||
end
|
||||
|
||||
# Models must implement these methods:
|
||||
# - search_title: returns title string or nil
|
||||
# - search_content: returns content string
|
||||
# - search_card_id: returns the card id (self.id for cards, card_id for comments)
|
||||
# - search_board_id: returns the board id
|
||||
def search_record_attributes
|
||||
{
|
||||
account_id: account_id,
|
||||
searchable_type: self.class.name,
|
||||
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
|
||||
}
|
||||
end
|
||||
|
||||
# Models must implement these methods:
|
||||
# - account_id: returns the account id
|
||||
# - search_title: returns title string or nil
|
||||
# - search_content: returns content string
|
||||
# - search_card_id: returns the card id (self.id for cards, card_id for comments)
|
||||
# - search_board_id: returns the board id
|
||||
end
|
||||
|
||||
+3
-43
@@ -1,49 +1,9 @@
|
||||
class Search
|
||||
attr_reader :user, :query
|
||||
|
||||
module Search
|
||||
def self.table_name_prefix
|
||||
"search_"
|
||||
end
|
||||
|
||||
def initialize(user, query)
|
||||
@user = user
|
||||
@query = Query.wrap(query)
|
||||
def self.results(query:, user:)
|
||||
Record.for_account(user.account_id).search(query: Query.wrap(query), user: user)
|
||||
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_raw_query = Search::Result.connection.quote(query.terms)
|
||||
table_name = Searchable.search_index_table_name(user.account_id)
|
||||
uuid_type = ActiveRecord::Type.lookup(:uuid, adapter: :trilogy)
|
||||
serialized_board_ids = board_ids.map { |id| uuid_type.serialize(id) }
|
||||
|
||||
Search::Result.from(table_name)
|
||||
.joins("INNER JOIN cards ON #{table_name}.card_id = cards.id")
|
||||
.joins("INNER JOIN boards ON cards.board_id = boards.id")
|
||||
.where("#{table_name}.board_id IN (?)", serialized_board_ids)
|
||||
.where("MATCH(#{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", query_string)
|
||||
.select([
|
||||
"#{table_name}.card_id as card_id",
|
||||
"CASE WHEN #{table_name}.searchable_type = 'Comment' THEN #{table_name}.searchable_id ELSE NULL END as comment_id",
|
||||
"boards.name as board_name",
|
||||
"cards.creator_id",
|
||||
"cards.number as number",
|
||||
"#{table_name}.created_at as created_at",
|
||||
"#{sanitized_raw_query} AS query"
|
||||
].join(","))
|
||||
.order("#{table_name}.created_at DESC")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
class Search::Record < ApplicationRecord
|
||||
self.abstract_class = true
|
||||
|
||||
SHARD_COUNT = 16
|
||||
|
||||
SHARD_CLASSES = SHARD_COUNT.times.map do |shard_id|
|
||||
Class.new(self) do
|
||||
self.table_name = "search_records_#{shard_id}"
|
||||
|
||||
def self.name
|
||||
"Search::Record"
|
||||
end
|
||||
end
|
||||
end.freeze
|
||||
|
||||
belongs_to :searchable, polymorphic: true
|
||||
belongs_to :card
|
||||
|
||||
# Virtual attributes from search query
|
||||
attribute :query, :string
|
||||
|
||||
validates :account_id, :searchable_type, :searchable_id, :card_id, :board_id, :created_at, presence: true
|
||||
|
||||
class << self
|
||||
def for_account(account_id)
|
||||
SHARD_CLASSES[shard_id_for_account(account_id)]
|
||||
end
|
||||
|
||||
def shard_id_for_account(account_id)
|
||||
Zlib.crc32(account_id.to_s) % SHARD_COUNT
|
||||
end
|
||||
|
||||
def card_join
|
||||
"INNER JOIN #{table_name} ON #{table_name}.card_id = cards.id"
|
||||
end
|
||||
end
|
||||
|
||||
scope :for_query, ->(query:, user:) do
|
||||
if query.valid? && user.board_ids.any?
|
||||
matching(query.to_s).for_user(user)
|
||||
else
|
||||
none
|
||||
end
|
||||
end
|
||||
|
||||
scope :matching, ->(query) do
|
||||
where("MATCH(#{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", query)
|
||||
end
|
||||
|
||||
scope :for_user, ->(user) do
|
||||
where(account_id: user.account_id, board_id: user.board_ids)
|
||||
end
|
||||
|
||||
scope :search, ->(query:, user:) do
|
||||
for_query(query: query, user: user)
|
||||
.includes(:searchable, card: [:board, :creator])
|
||||
.select(:id, :searchable_type, :searchable_id, :card_id, :board_id, :account_id, :created_at, "#{connection.quote(query.terms)} AS query")
|
||||
.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def source
|
||||
searchable_type == "Comment" ? searchable : card
|
||||
end
|
||||
|
||||
def comment
|
||||
searchable if searchable_type == "Comment"
|
||||
end
|
||||
|
||||
def card_title
|
||||
highlight(card.title, show: :full) if card_id
|
||||
end
|
||||
|
||||
def card_description
|
||||
highlight(card.description.to_plain_text, show: :snippet) if card_id
|
||||
end
|
||||
|
||||
def comment_body
|
||||
highlight(comment.body.to_plain_text, show: :snippet) if comment
|
||||
end
|
||||
|
||||
private
|
||||
def highlight(text, show:)
|
||||
if text.present? && attribute?(:query)
|
||||
highlighter = Search::Highlighter.new(query)
|
||||
show == :snippet ? highlighter.snippet(text) : highlighter.highlight(text)
|
||||
else
|
||||
text
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,7 @@ module User::Searcher
|
||||
end
|
||||
|
||||
def search(terms)
|
||||
Search.new(self, terms).results
|
||||
Search.results(query: terms, user: self)
|
||||
end
|
||||
|
||||
def remember_search(terms)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<%= link_to result.source, class: "search__result", data: { turbo_frame: "_top", action: "bar#reset" } do %>
|
||||
<div>
|
||||
<h3 class="search__title txt--medium margin-none">
|
||||
# <%= result.number %> <%= result.card_title %>
|
||||
# <%= result.card.number %> <%= result.card_title %>
|
||||
</h3>
|
||||
|
||||
<% if result.card_description.present? %>
|
||||
@@ -12,7 +12,7 @@
|
||||
<% else
|
||||
if result.comment_body.present? %>
|
||||
<div class="search__excerpt search__excerpt--comment">
|
||||
<%= avatar_preview_tag result.creator %>
|
||||
<%= avatar_preview_tag result.card.creator %>
|
||||
<div>
|
||||
<%= result.comment_body %>
|
||||
</div>
|
||||
@@ -20,6 +20,6 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="overflow-ellipsis translucent txt-ink"><%= result.board_name %></div>
|
||||
<div class="overflow-ellipsis translucent txt-ink"><%= result.card.board.name %></div>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
class CreateSearchRecordShards < ActiveRecord::Migration[8.2]
|
||||
SHARD_COUNT = 16
|
||||
|
||||
def up
|
||||
# Create 16 sharded search_records tables
|
||||
SHARD_COUNT.times do |shard_id|
|
||||
create_table "search_records_#{shard_id}", id: :uuid do |t|
|
||||
t.uuid :account_id, null: false
|
||||
t.string :searchable_type, null: false
|
||||
t.uuid :searchable_id, null: false
|
||||
t.uuid :card_id, null: false
|
||||
t.uuid :board_id, null: false
|
||||
t.string :title
|
||||
t.text :content
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index [:searchable_type, :searchable_id], unique: true
|
||||
t.index :account_id
|
||||
t.index [:content, :title], type: :fulltext
|
||||
end
|
||||
end
|
||||
|
||||
# Drop old search_index tables
|
||||
SHARD_COUNT.times do |shard_id|
|
||||
drop_table "search_index_#{shard_id}", if_exists: true
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
SHARD_COUNT.times do |shard_id|
|
||||
drop_table "search_records_#{shard_id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
Generated
+225
-193
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.2].define(version: 2025_11_13_163145) do
|
||||
ActiveRecord::Schema[8.2].define(version: 2025_11_13_190256) do
|
||||
create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.datetime "accessed_at"
|
||||
t.uuid "account_id", null: false
|
||||
@@ -396,198 +396,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_13_163145) do
|
||||
t.index ["reacter_id"], name: "index_reactions_on_reacter_id"
|
||||
end
|
||||
|
||||
create_table "search_index_0", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si0_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si0_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_1", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si1_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si1_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_10", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si10_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si10_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_11", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si11_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si11_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_12", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si12_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si12_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_13", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si13_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si13_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_14", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si14_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si14_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_15", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si15_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si15_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_2", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si2_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si2_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_3", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si3_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si3_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_4", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si4_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si4_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_5", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si5_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si5_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_6", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si6_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si6_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_7", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si7_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si7_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_8", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si8_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si8_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_index_9", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "idx_si9_fulltext", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "idx_si9_type_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_queries", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -600,6 +408,230 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_13_163145) do
|
||||
t.index ["user_id"], name: "index_search_queries_on_user_id"
|
||||
end
|
||||
|
||||
create_table "search_records_0", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_0_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_0_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_0_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_1", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_1_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_1_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_1_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_10", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_10_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_10_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_10_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_11", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_11_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_11_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_11_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_12", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_12_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_12_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_12_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_13", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_13_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_13_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_13_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_14", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_14_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_14_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_14_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_15", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_15_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_15_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_15_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_2", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_2_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_2_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_2_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_3", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_3_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_3_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_3_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_4", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_4_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_4_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_4_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_5", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_5_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_5_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_5_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_6", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_6_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_6_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_6_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_7", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_7_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_7_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_7_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_8", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_8_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_8_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_8_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_records_9", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.uuid "board_id", null: false
|
||||
t.uuid "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.uuid "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["account_id"], name: "index_search_records_9_on_account_id"
|
||||
t.index ["content", "title"], name: "index_search_records_9_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_records_9_on_searchable_type_and_searchable_id", unique: true
|
||||
end
|
||||
|
||||
create_table "search_results", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
namespace :search do
|
||||
desc "Reindex all cards and comments in the search index"
|
||||
task reindex: :environment do
|
||||
puts "Clearing search index shards..."
|
||||
16.times do |i|
|
||||
ActiveRecord::Base.connection.execute("DELETE FROM search_index_#{i}")
|
||||
puts "Clearing search records..."
|
||||
Search::Record::SHARD_COUNT.times do |shard_id|
|
||||
ActiveRecord::Base.connection.execute("DELETE FROM search_records_#{shard_id}")
|
||||
end
|
||||
|
||||
puts "Reindexing cards..."
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
require "test_helper"
|
||||
|
||||
class SearchesControllerTest < ActionDispatch::IntegrationTest
|
||||
include SearchTestHelper
|
||||
|
||||
setup do
|
||||
Card.all.each(&:reindex)
|
||||
Comment.all.each(&:reindex)
|
||||
@board.update!(all_access: true)
|
||||
@card = @board.cards.create!(title: "Layout is broken", creator: @user)
|
||||
@comment_card = @board.cards.create!(title: "Some card", creator: @user)
|
||||
@comment_card.comments.create!(body: "overflowing text issue", creator: @user)
|
||||
|
||||
sign_in_as :kevin
|
||||
untenanted { sign_in_as @user }
|
||||
end
|
||||
|
||||
test "show" do
|
||||
skip("TODO:PLANB: search")
|
||||
get search_path(q: "broken")
|
||||
|
||||
test "search" do
|
||||
# Searching by card title
|
||||
get search_path(q: "broken", script_name: "/#{@account.external_account_id}")
|
||||
assert_select "li", text: /Layout is broken/
|
||||
end
|
||||
|
||||
test "show with card id" do
|
||||
skip("TODO:PLANB: search")
|
||||
get search_path(q: cards(:logo).id)
|
||||
# Searching by comment
|
||||
get search_path(q: "overflowing", script_name: "/#{@account.external_account_id}")
|
||||
assert_select "li", text: /Some card/
|
||||
|
||||
# Searching by card id
|
||||
get search_path(q: @card.id, script_name: "/#{@account.external_account_id}")
|
||||
assert_select "form[data-controller='auto-submit']"
|
||||
end
|
||||
|
||||
test "show with non-existent card id" do
|
||||
skip("TODO:PLANB: search")
|
||||
get search_path(q: "999999")
|
||||
|
||||
# Searching with non-existent card id
|
||||
get search_path(q: "999999", script_name: "/#{@account.external_account_id}")
|
||||
assert_select "form[data-controller='auto-submit']", count: 0
|
||||
assert_select ".search__empty", text: "No matches"
|
||||
end
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class Card::SearchableTest < ActiveSupport::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
setup do
|
||||
16.times { |i| ActiveRecord::Base.connection.execute "DELETE FROM search_index_#{i}" }
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
|
||||
@account = Account.create!(name: "Search Test")
|
||||
@user = User.create!(name: "Test User", account: @account)
|
||||
@board = Board.create!(name: "Test Board", account: @account, creator: @user)
|
||||
Current.account = @account
|
||||
end
|
||||
|
||||
teardown do
|
||||
16.times { |i| ActiveRecord::Base.connection.execute "DELETE FROM search_index_#{i}" }
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
end
|
||||
include SearchTestHelper
|
||||
|
||||
test "card search" do
|
||||
# Searching by title
|
||||
|
||||
@@ -1,58 +1,28 @@
|
||||
require "test_helper"
|
||||
|
||||
class Comment::SearchableTest < ActiveSupport::TestCase
|
||||
self.use_transactional_tests = false
|
||||
include SearchTestHelper
|
||||
|
||||
setup do
|
||||
16.times { |i| ActiveRecord::Base.connection.execute "DELETE FROM search_index_#{i}" }
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
|
||||
@account = Account.create!(name: "Search Test")
|
||||
@user = User.create!(name: "Test User", account: @account)
|
||||
@board = Board.create!(name: "Test Board", account: @account, creator: @user)
|
||||
@card = @board.cards.create!(title: "Test Card", creator: @user)
|
||||
Current.account = @account
|
||||
end
|
||||
|
||||
teardown do
|
||||
16.times { |i| ActiveRecord::Base.connection.execute "DELETE FROM search_index_#{i}" }
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
end
|
||||
|
||||
test "comment search" do
|
||||
table_name = Searchable.search_index_table_name(@account.id)
|
||||
uuid_type = ActiveRecord::Type.lookup(:uuid, adapter: :trilogy)
|
||||
|
||||
# Comment is indexed on create
|
||||
comment = @card.comments.create!(body: "searchable comment text", creator: @user)
|
||||
result = ActiveRecord::Base.connection.execute(
|
||||
ActiveRecord::Base.sanitize_sql([
|
||||
"SELECT COUNT(*) FROM #{table_name} WHERE searchable_type = 'Comment' AND searchable_id = ?",
|
||||
uuid_type.serialize(comment.id)
|
||||
])
|
||||
).first[0]
|
||||
assert_equal 1, result
|
||||
record = Search::Record.for_account(@account.id).find_by(searchable_type: "Comment", searchable_id: comment.id)
|
||||
assert_not_nil record
|
||||
|
||||
# Comment is updated in index
|
||||
comment.update!(body: "updated text")
|
||||
content = ActiveRecord::Base.connection.execute(
|
||||
ActiveRecord::Base.sanitize_sql([
|
||||
"SELECT content FROM #{table_name} WHERE searchable_type = 'Comment' AND searchable_id = ?",
|
||||
uuid_type.serialize(comment.id)
|
||||
])
|
||||
).first[0]
|
||||
assert_match /updat/, content
|
||||
record = Search::Record.for_account(@account.id).find_by(searchable_type: "Comment", searchable_id: comment.id)
|
||||
assert_match /updat/, record.content
|
||||
|
||||
# Comment is removed from index on destroy
|
||||
comment_id = comment.id
|
||||
comment.destroy
|
||||
result = ActiveRecord::Base.connection.execute(
|
||||
ActiveRecord::Base.sanitize_sql([
|
||||
"SELECT COUNT(*) FROM #{table_name} WHERE searchable_type = 'Comment' AND searchable_id = ?",
|
||||
uuid_type.serialize(comment_id)
|
||||
])
|
||||
).first[0]
|
||||
assert_equal 0, result
|
||||
record = Search::Record.for_account(@account.id).find_by(searchable_type: "Comment", searchable_id: comment_id)
|
||||
assert_nil record
|
||||
|
||||
# Finding cards via comment search
|
||||
card_with_comment = @board.cards.create!(title: "Card One", creator: @user)
|
||||
@@ -64,14 +34,8 @@ class Comment::SearchableTest < ActiveSupport::TestCase
|
||||
|
||||
# Comment stores parent card_id and board_id
|
||||
new_comment = @card.comments.create!(body: "test comment", creator: @user)
|
||||
row = ActiveRecord::Base.connection.execute(
|
||||
ActiveRecord::Base.sanitize_sql([
|
||||
"SELECT card_id, board_id FROM #{table_name} WHERE searchable_type = 'Comment' AND searchable_id = ?",
|
||||
uuid_type.serialize(new_comment.id)
|
||||
])
|
||||
).first
|
||||
# Deserialize binary UUIDs from result
|
||||
assert_equal @card.id, uuid_type.deserialize(row[0])
|
||||
assert_equal @board.id, uuid_type.deserialize(row[1])
|
||||
record = Search::Record.for_account(@account.id).find_by(searchable_type: "Comment", searchable_id: new_comment.id)
|
||||
assert_equal @card.id, record.card_id
|
||||
assert_equal @board.id, record.board_id
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class SearchTest < ActiveSupport::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
setup do
|
||||
16.times { |i| ActiveRecord::Base.connection.execute "DELETE FROM search_index_#{i}" }
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
|
||||
@account = Account.create!(name: "Search Test")
|
||||
@user = User.create!(name: "Test User", account: @account)
|
||||
@board = Board.create!(name: "Test Board", account: @account, creator: @user)
|
||||
Current.account = @account
|
||||
end
|
||||
|
||||
teardown do
|
||||
16.times { |i| ActiveRecord::Base.connection.execute "DELETE FROM search_index_#{i}" }
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
end
|
||||
include SearchTestHelper
|
||||
|
||||
test "search" do
|
||||
# Search cards and comments
|
||||
@@ -24,11 +9,11 @@ class SearchTest < ActiveSupport::TestCase
|
||||
comment_card = @board.cards.create!(title: "Some card", creator: @user)
|
||||
comment_card.comments.create!(body: "overflowing text", creator: @user)
|
||||
|
||||
results = Search.new(@user, "layout").results
|
||||
results = Search.results(query: "layout", user: @user)
|
||||
assert results.find { |it| it.card_id == card.id }
|
||||
|
||||
results = Search.new(@user, "overflowing").results
|
||||
assert results.find { |it| it.card_id == comment_card.id && it.comment_id.present? }
|
||||
results = Search.results(query: "overflowing", user: @user)
|
||||
assert results.find { |it| it.card_id == comment_card.id && it.searchable_type == "Comment" }
|
||||
|
||||
# Don't include inaccessible boards
|
||||
other_user = User.create!(name: "Other User", account: @account)
|
||||
@@ -36,13 +21,13 @@ class SearchTest < ActiveSupport::TestCase
|
||||
accessible_card = @board.cards.create!(title: "searchable content", creator: @user)
|
||||
inaccessible_card = inaccessible_board.cards.create!(title: "searchable content", creator: other_user)
|
||||
|
||||
results = Search.new(@user, "searchable").results
|
||||
results = Search.results(query: "searchable", user: @user)
|
||||
assert results.find { |it| it.card_id == accessible_card.id }
|
||||
assert_not results.find { |it| it.card_id == inaccessible_card.id }
|
||||
|
||||
# Empty board_ids returns no results
|
||||
user_without_access = User.create!(name: "No Access User", account: @account)
|
||||
results = Search.new(user_without_access, "anything").results
|
||||
results = Search.results(query: "anything", user: user_without_access)
|
||||
assert_empty results
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
module SearchTestHelper
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
self.use_transactional_tests = false
|
||||
|
||||
setup :setup_search_test
|
||||
teardown :teardown_search_test
|
||||
end
|
||||
|
||||
def setup_search_test
|
||||
clear_search_records
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
Identity.find_by(email_address: "test@example.com")&.destroy
|
||||
|
||||
@account = Account.create!(name: "Search Test", external_account_id: ActiveRecord::FixtureSet.identify("search_test"))
|
||||
@identity = Identity.create!(email_address: "test@example.com")
|
||||
@membership = Membership.create!(identity: @identity, tenant: @account.external_account_id)
|
||||
@user = User.create!(name: "Test User", account: @account, membership: @membership)
|
||||
@board = Board.create!(name: "Test Board", account: @account, creator: @user)
|
||||
Current.account = @account
|
||||
end
|
||||
|
||||
def teardown_search_test
|
||||
clear_search_records
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
Identity.find_by(email_address: "test@example.com")&.destroy
|
||||
end
|
||||
|
||||
private
|
||||
def clear_search_records
|
||||
Search::Record::SHARD_COUNT.times do |shard_id|
|
||||
ActiveRecord::Base.connection.execute("DELETE FROM search_records_#{shard_id}")
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user