Shard the search index into 16 tables
Create search_index_0 to search_index_15 tables and shard each index by account id. MySQL has no ability to pre-filter fulltext indexes by another field so this is the best bet for improving performance. Each fulltest index internally creates 11 sub tables (see https://dev.mysql.com/doc/refman/8.4/en/innodb-fulltext-index.html) so actually we have 192 tables in total here. The search_index table name is generated dynamically based on the account_id.
This commit is contained in:
committed by
Mike Dalessio
parent
996140e053
commit
3aa4a1a562
@@ -20,7 +20,7 @@ class Prompts::CardsController < ApplicationController
|
||||
|
||||
def search_cards
|
||||
published_cards
|
||||
.mentioning(params[:filter], board_ids: Current.user.board_ids)
|
||||
.mentioning(params[:filter], user: Current.user)
|
||||
.reverse_chronologically
|
||||
.limit(MAX_RESULTS)
|
||||
end
|
||||
|
||||
@@ -4,13 +4,14 @@ module Card::Searchable
|
||||
included do
|
||||
include ::Searchable
|
||||
|
||||
scope :mentioning, ->(query, board_ids:) do
|
||||
scope :mentioning, ->(query, user:) do
|
||||
query = Search::Query.wrap(query)
|
||||
|
||||
if query.valid?
|
||||
joins("INNER JOIN search_index ON search_index.card_id = cards.id AND search_index.board_id = cards.board_id")
|
||||
.where("search_index.board_id IN (?)", board_ids)
|
||||
.where("MATCH(search_index.content, search_index.title) AGAINST(? IN BOOLEAN MODE)", query.to_s)
|
||||
table_name = Searchable.search_index_table_name(user.account_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 (?)", user.board_ids)
|
||||
.where("MATCH(#{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", query.to_s)
|
||||
.distinct
|
||||
else
|
||||
none
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
module Searchable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def self.search_index_table_name(account_id)
|
||||
"search_index_#{account_id % 16}"
|
||||
end
|
||||
|
||||
included do
|
||||
after_create_commit :create_in_search_index
|
||||
after_update_commit :update_in_search_index
|
||||
@@ -13,8 +17,10 @@ module Searchable
|
||||
|
||||
private
|
||||
def create_in_search_index
|
||||
table_name = Searchable.search_index_table_name(account_id)
|
||||
|
||||
self.class.connection.execute self.class.sanitize_sql([
|
||||
"INSERT INTO search_index (searchable_type, searchable_id, card_id, board_id, title, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO #{table_name} (searchable_type, searchable_id, card_id, board_id, title, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
self.class.name,
|
||||
id,
|
||||
search_card_id,
|
||||
@@ -26,8 +32,10 @@ module Searchable
|
||||
end
|
||||
|
||||
def update_in_search_index
|
||||
table_name = Searchable.search_index_table_name(account_id)
|
||||
|
||||
result = self.class.connection.execute(self.class.sanitize_sql([
|
||||
"UPDATE search_index SET card_id = ?, board_id = ?, title = ?, content = ?, created_at = ? WHERE searchable_type = ? AND searchable_id = ?",
|
||||
"UPDATE #{table_name} SET card_id = ?, board_id = ?, title = ?, content = ?, created_at = ? WHERE searchable_type = ? AND searchable_id = ?",
|
||||
search_card_id,
|
||||
search_board_id,
|
||||
search_title,
|
||||
@@ -41,8 +49,10 @@ module Searchable
|
||||
end
|
||||
|
||||
def remove_from_search_index
|
||||
table_name = Searchable.search_index_table_name(account_id)
|
||||
|
||||
self.class.connection.execute self.class.sanitize_sql([
|
||||
"DELETE FROM search_index WHERE searchable_type = ? AND searchable_id = ?",
|
||||
"DELETE FROM #{table_name} WHERE searchable_type = ? AND searchable_id = ?",
|
||||
self.class.name,
|
||||
id
|
||||
])
|
||||
|
||||
@@ -33,7 +33,7 @@ class Filter < ApplicationRecord
|
||||
result = result.closed_at_window(closure_window) if closure_window
|
||||
result = result.closed_by(closers) if closers.present?
|
||||
result = terms.reduce(result) do |result, term|
|
||||
result.mentioning(term, board_ids: creator.board_ids)
|
||||
result.mentioning(term, user: creator)
|
||||
end
|
||||
|
||||
result
|
||||
|
||||
+13
-12
@@ -27,24 +27,25 @@ class Search
|
||||
query_string = query.to_s
|
||||
sanitized_query = Search::Result.connection.quote(query_string)
|
||||
sanitized_raw_query = Search::Result.connection.quote(query.terms)
|
||||
table_name = Searchable.search_index_table_name(user.account_id)
|
||||
|
||||
Search::Result.from("search_index")
|
||||
.joins("INNER JOIN cards ON search_index.card_id = cards.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("search_index.board_id IN (?)", board_ids)
|
||||
.where("MATCH(search_index.content, search_index.title) AGAINST(? IN BOOLEAN MODE)", query_string)
|
||||
.where("#{table_name}.board_id IN (?)", board_ids)
|
||||
.where("MATCH(#{table_name}.content, #{table_name}.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",
|
||||
"#{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",
|
||||
"COALESCE(#{table_name}.title, cards.title) AS card_title_in_database",
|
||||
"CASE WHEN #{table_name}.searchable_type = 'Card' THEN #{table_name}.content ELSE NULL END AS card_description_in_database",
|
||||
"CASE WHEN #{table_name}.searchable_type = 'Comment' THEN #{table_name}.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",
|
||||
"#{table_name}.created_at as created_at",
|
||||
"MATCH(#{table_name}.content, #{table_name}.title) AGAINST(#{sanitized_query} IN BOOLEAN MODE) AS score",
|
||||
"#{sanitized_raw_query} AS query"
|
||||
].join(","))
|
||||
.order("search_index.created_at DESC")
|
||||
.order("#{table_name}.created_at DESC")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,6 +46,52 @@
|
||||
],
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
"fingerprint": "08f15e8e259f1da31762507f718191732f86a4f4f6d2fe2719e70fd22c9bc738",
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/search.rb",
|
||||
"line": 33,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "Search::Result.from(Searchable.search_index_table_name(user.account_id)).joins(\"INNER JOIN cards ON #{Searchable.search_index_table_name(user.account_id)}.card_id = cards.id\")",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
"class": "Search",
|
||||
"method": "perform_search"
|
||||
},
|
||||
"user_input": "Searchable.search_index_table_name(user.account_id)",
|
||||
"confidence": "Medium",
|
||||
"cwe_id": [
|
||||
89
|
||||
],
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
"fingerprint": "e2069dcaa15f04898b9ad356ee3277e4a0b718e88c991c86d896dbded1b47f97",
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/card/searchable.rb",
|
||||
"line": 12,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "joins(\"INNER JOIN #{Searchable.search_index_table_name(account_id)} ON #{Searchable.search_index_table_name(account_id)}.card_id = cards.id AND #{Searchable.search_index_table_name(account_id)}.board_id = cards.board_id\")",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
"class": "Card::Searchable",
|
||||
"method": null
|
||||
},
|
||||
"user_input": "Searchable.search_index_table_name(account_id)",
|
||||
"confidence": "Medium",
|
||||
"cwe_id": [
|
||||
89
|
||||
],
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Mass Assignment",
|
||||
"warning_code": 70,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
class CreateSearchIndex < ActiveRecord::Migration[8.2]
|
||||
def up
|
||||
create_table :search_index do |t|
|
||||
t.string :searchable_type, null: false
|
||||
t.bigint :searchable_id, null: false
|
||||
t.bigint :card_id, null: false
|
||||
t.bigint :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 [:content, :title], type: :fulltext, name: "index_search_index_on_content_and_title"
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :search_index
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
class CreateSearchIndex < ActiveRecord::Migration[8.2]
|
||||
def up
|
||||
16.times do |i|
|
||||
create_table "search_index_#{i}".to_sym do |t|
|
||||
t.string :searchable_type, null: false
|
||||
t.bigint :searchable_id, null: false
|
||||
t.bigint :card_id, null: false
|
||||
t.bigint :board_id, null: false
|
||||
t.string :title
|
||||
t.text :content
|
||||
t.datetime :created_at, null: false
|
||||
|
||||
t.index [:searchable_type, :searchable_id], unique: true, name: "idx_si#{i}_type_id"
|
||||
t.index [:content, :title], type: :fulltext, name: "idx_si#{i}_fulltext"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
16.times do |i|
|
||||
drop_table "search_index_#{i}".to_sym
|
||||
end
|
||||
end
|
||||
end
|
||||
Generated
+183
-3
@@ -375,7 +375,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_12_093037) do
|
||||
t.index ["reacter_id"], name: "index_reactions_on_reacter_id"
|
||||
end
|
||||
|
||||
create_table "search_index", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
create_table "search_index_0", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
@@ -383,8 +383,188 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_12_093037) do
|
||||
t.bigint "searchable_id", null: false
|
||||
t.string "searchable_type", null: false
|
||||
t.string "title"
|
||||
t.index ["content", "title"], name: "index_search_index_on_content_and_title", type: :fulltext
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_search_index_on_searchable_type_and_searchable_id", unique: true
|
||||
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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "board_id", null: false
|
||||
t.bigint "card_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "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", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
namespace :search do
|
||||
desc "Reindex all cards and comments in the search index"
|
||||
task reindex: :environment do
|
||||
puts "Clearing search index..."
|
||||
ActiveRecord::Base.connection.execute("DELETE FROM search_index")
|
||||
puts "Clearing search index shards..."
|
||||
16.times do |i|
|
||||
ActiveRecord::Base.connection.execute("DELETE FROM search_index_#{i}")
|
||||
end
|
||||
|
||||
puts "Reindexing cards..."
|
||||
Card.find_each do |card|
|
||||
|
||||
@@ -4,44 +4,45 @@ class Card::SearchableTest < ActiveSupport::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
setup do
|
||||
ActiveRecord::Base.connection.execute "DELETE FROM search_index"
|
||||
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
|
||||
ActiveRecord::Base.connection.execute "DELETE FROM search_index"
|
||||
16.times { |i| ActiveRecord::Base.connection.execute "DELETE FROM search_index_#{i}" }
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
end
|
||||
|
||||
test "card search" do
|
||||
# Searching by title
|
||||
card = @board.cards.create!(title: "layout is broken", creator: @user)
|
||||
results = Card.mentioning("layout", board_ids: [@board.id])
|
||||
results = Card.mentioning("layout", user: @user)
|
||||
assert_includes results, card
|
||||
|
||||
# Searching by comment
|
||||
card_with_comment = @board.cards.create!(title: "Some card", creator: @user)
|
||||
card_with_comment.comments.create!(body: "overflowing text", creator: @user)
|
||||
results = Card.mentioning("overflowing", board_ids: [@board.id])
|
||||
results = Card.mentioning("overflowing", user: @user )
|
||||
assert_includes results, card_with_comment
|
||||
|
||||
# Sanitizing search query
|
||||
card_broken = @board.cards.create!(title: "broken layout", creator: @user)
|
||||
results = Card.mentioning("broken \"", board_ids: [@board.id])
|
||||
results = Card.mentioning("broken \"", user: @user )
|
||||
assert_includes results, card_broken
|
||||
|
||||
# Empty query returns no results
|
||||
assert_empty Card.mentioning("\"", board_ids: [@board.id])
|
||||
assert_empty Card.mentioning("\"", user: @user )
|
||||
|
||||
# Filtering by board_ids
|
||||
other_board = Board.create!(name: "Other Board", account: @account, creator: @user)
|
||||
card_in_board = @board.cards.create!(title: "searchable content", creator: @user)
|
||||
card_in_other_board = other_board.cards.create!(title: "searchable content", creator: @user)
|
||||
results = Card.mentioning("searchable", board_ids: [@board.id])
|
||||
results = Card.mentioning("searchable", user: @user)
|
||||
assert_includes results, card_in_board
|
||||
assert_not_includes results, card_in_other_board
|
||||
end
|
||||
|
||||
@@ -4,32 +4,35 @@ class Comment::SearchableTest < ActiveSupport::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
setup do
|
||||
ActiveRecord::Base.connection.execute "DELETE FROM search_index"
|
||||
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
|
||||
ActiveRecord::Base.connection.execute "DELETE FROM search_index"
|
||||
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)
|
||||
|
||||
# Comment is indexed on create
|
||||
comment = @card.comments.create!(body: "searchable comment text", creator: @user)
|
||||
result = ActiveRecord::Base.connection.execute(
|
||||
"SELECT COUNT(*) FROM search_index WHERE searchable_type = 'Comment' AND searchable_id = #{comment.id}"
|
||||
"SELECT COUNT(*) FROM #{table_name} WHERE searchable_type = 'Comment' AND searchable_id = #{comment.id}"
|
||||
).first[0]
|
||||
assert_equal 1, result
|
||||
|
||||
# Comment is updated in index
|
||||
comment.update!(body: "updated text")
|
||||
content = ActiveRecord::Base.connection.execute(
|
||||
"SELECT content FROM search_index WHERE searchable_type = 'Comment' AND searchable_id = #{comment.id}"
|
||||
"SELECT content FROM #{table_name} WHERE searchable_type = 'Comment' AND searchable_id = #{comment.id}"
|
||||
).first[0]
|
||||
assert_match /updat/, content
|
||||
|
||||
@@ -37,7 +40,7 @@ class Comment::SearchableTest < ActiveSupport::TestCase
|
||||
comment_id = comment.id
|
||||
comment.destroy
|
||||
result = ActiveRecord::Base.connection.execute(
|
||||
"SELECT COUNT(*) FROM search_index WHERE searchable_type = 'Comment' AND searchable_id = #{comment_id}"
|
||||
"SELECT COUNT(*) FROM #{table_name} WHERE searchable_type = 'Comment' AND searchable_id = #{comment_id}"
|
||||
).first[0]
|
||||
assert_equal 0, result
|
||||
|
||||
@@ -45,14 +48,14 @@ class Comment::SearchableTest < ActiveSupport::TestCase
|
||||
card_with_comment = @board.cards.create!(title: "Card One", creator: @user)
|
||||
card_with_comment.comments.create!(body: "unique searchable phrase", creator: @user)
|
||||
card_without_comment = @board.cards.create!(title: "Card Two", creator: @user)
|
||||
results = Card.mentioning("searchable", board_ids: [@board.id])
|
||||
results = Card.mentioning("searchable", user: @user)
|
||||
assert_includes results, card_with_comment
|
||||
assert_not_includes results, card_without_comment
|
||||
|
||||
# Comment stores parent card_id and board_id
|
||||
new_comment = @card.comments.create!(body: "test comment", creator: @user)
|
||||
row = ActiveRecord::Base.connection.execute(
|
||||
"SELECT card_id, board_id FROM search_index WHERE searchable_type = 'Comment' AND searchable_id = #{new_comment.id}"
|
||||
"SELECT card_id, board_id FROM #{table_name} WHERE searchable_type = 'Comment' AND searchable_id = #{new_comment.id}"
|
||||
).first
|
||||
assert_equal @card.id, row[0]
|
||||
assert_equal @board.id, row[1]
|
||||
|
||||
@@ -4,16 +4,17 @@ class SearchTest < ActiveSupport::TestCase
|
||||
self.use_transactional_tests = false
|
||||
|
||||
setup do
|
||||
ActiveRecord::Base.connection.execute "DELETE FROM search_index"
|
||||
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
|
||||
ActiveRecord::Base.connection.execute "DELETE FROM search_index"
|
||||
16.times { |i| ActiveRecord::Base.connection.execute "DELETE FROM search_index_#{i}" }
|
||||
Account.find_by(name: "Search Test")&.destroy
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user