From 70dd754cf7f44f4cdd53f53ab381795b906974fa Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 21 Nov 2025 09:36:18 +0000 Subject: [PATCH 1/7] Account key for search records Add an account key field to improve search performance. This field allows us to filter the records by account directly in the fulltext index, so we only need to examine rows belonging to the relevant account. --- app/models/concerns/searchable.rb | 3 +- app/models/search/record.rb | 8 ++- ...92508_add_account_key_to_search_records.rb | 37 +++++++++++ db/schema.rb | 65 ++++++++++++++----- 4 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 db/migrate/20251121092508_add_account_key_to_search_records.rb diff --git a/app/models/concerns/searchable.rb b/app/models/concerns/searchable.rb index 17e965298..5b1d5a1bb 100644 --- a/app/models/concerns/searchable.rb +++ b/app/models/concerns/searchable.rb @@ -19,7 +19,7 @@ module Searchable 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 ] + update_only: [ :card_id, :board_id, :title, :content, :created_at, :account_key ] ) end @@ -33,6 +33,7 @@ module Searchable def search_record_attributes { account_id: account_id, + account_key: "account#{account_id}", searchable_type: self.class.name, searchable_id: id, card_id: search_card_id, diff --git a/app/models/search/record.rb b/app/models/search/record.rb index 72cf39865..1bf933a53 100644 --- a/app/models/search/record.rb +++ b/app/models/search/record.rb @@ -37,14 +37,16 @@ class Search::Record < ApplicationRecord scope :for_query, ->(query:, user:) do if query.valid? && user.board_ids.any? - matching(query.to_s).for_user(user) + matching(query.to_s, user.account_id).for_user(user) else none end end - scope :matching, ->(query) do - where("MATCH(#{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", query) + scope :matching, ->(query, account_id) do + account_key = "account#{account_id}" + full_query = "+#{account_key} +(#{query})" + where("MATCH(#{table_name}.account_key, #{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", full_query) end scope :for_user, ->(user) do diff --git a/db/migrate/20251121092508_add_account_key_to_search_records.rb b/db/migrate/20251121092508_add_account_key_to_search_records.rb new file mode 100644 index 000000000..f1cd26a99 --- /dev/null +++ b/db/migrate/20251121092508_add_account_key_to_search_records.rb @@ -0,0 +1,37 @@ +class AddAccountKeyToSearchRecords < ActiveRecord::Migration[8.2] + def up + 16.times do |shard_id| + table_name = "search_records_#{shard_id}" + + # Add account_key column + add_column table_name, :account_key, :string, null: false, default: "" + + # Backfill existing records with account_key + execute <<-SQL + UPDATE #{table_name} + SET account_key = CONCAT('account', account_id) + SQL + + # Create new fulltext index including account_key + add_index table_name, [:account_key, :content, :title], type: :fulltext + + # Drop existing fulltext index + remove_index table_name, column: [:content, :title], type: :fulltext + end + end + + def down + 16.times do |shard_id| + table_name = "search_records_#{shard_id}" + + # Drop new fulltext index + remove_index table_name, column: [:account_key, :content, :title], type: :fulltext + + # Restore original fulltext index + add_index table_name, [:content, :title], type: :fulltext + + # Remove account_key column + remove_column table_name, :account_key + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e6a403fee..bfe3bebcc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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_20_203100) do +ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) 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 @@ -408,8 +408,24 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do t.index ["user_id"], name: "index_search_queries_on_user_id" end + create_table "search_records", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.uuid "account_id", null: false + t.string "account_key", 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_on_account_id" + t.index ["account_key", "content", "title"], name: "index_search_records_on_account_key_and_content_and_title", type: :fulltext + t.index ["searchable_type", "searchable_id"], name: "index_search_records_on_searchable_type_and_searchable_id", unique: true + 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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -418,12 +434,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_0_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -432,12 +449,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_1_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -446,12 +464,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_10_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -460,12 +479,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_11_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -474,12 +494,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_12_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -488,12 +509,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_13_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -502,12 +524,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_14_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -516,12 +539,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_15_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -530,12 +554,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_2_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -544,12 +569,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_3_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -558,12 +584,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_4_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -572,12 +599,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_5_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -586,12 +614,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_6_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -600,12 +629,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_7_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -614,12 +644,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_8_on_account_key_and_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.string "account_key", default: "", null: false t.uuid "board_id", null: false t.uuid "card_id", null: false t.text "content" @@ -628,7 +659,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_20_203100) do 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 ["account_key", "content", "title"], name: "index_search_records_9_on_account_key_and_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 From 2b23d85653f3aae4ffb5ea9431b7cc5814819e2f Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Fri, 21 Nov 2025 11:13:44 +0100 Subject: [PATCH 2/7] Add dir=auto to title inputs See: https://github.com/basecamp/once-campfire/pull/97 --- app/views/cards/container/_title.html.erb | 2 +- app/views/cards/edit.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index 47a6a0924..a7176ecb2 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -23,7 +23,7 @@ <%= form.label :title, class: "flex flex-column align-center autoresize__wrapper", data: { autoresize_target: "wrapper", autoresize_clone_value: "" } do %> <%= form.text_area :title, placeholder: "Name it…", class: "card-field__title autoresize__textarea input input--textarea full-width borderless txt-align-start hide-focus-ring hide-scrollbar", - autofocus: card.title.blank?, rows: 1, + autofocus: card.title.blank?, rows: 1, dir: "auto", data: { autoresize_target: "textarea", action: "input->autoresize#resize auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent" } %> <% end %> diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index e243fb2b4..0f33ab9c3 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -4,7 +4,7 @@

<%= form.label :title, class: "flex flex-column align-center autoresize__wrapper", data: { autoresize_target: "wrapper", autoresize_clone_value: "" } do %> <%= form.text_area :title, class: "card-field__title autoresize__textarea input input--textarea full-width borderless txt-align-start hide-focus-ring hide-scrollbar", - required: true, autofocus: true, placeholder: "Name it…", rows: 1, + required: true, autofocus: true, placeholder: "Name it…", rows: 1, dir: "auto", data: { autoresize_target: "textarea", action: "input->autoresize#resize keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> <% end %>

From 74d1cf735ba27949307e51c35a31c399a1c61f02 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 21 Nov 2025 10:20:27 +0000 Subject: [PATCH 3/7] Only add and fill account_key --- app/models/search/record.rb | 4 +-- ...92508_add_account_key_to_search_records.rb | 18 ----------- db/schema.rb | 31 ++++++++++--------- 3 files changed, 17 insertions(+), 36 deletions(-) diff --git a/app/models/search/record.rb b/app/models/search/record.rb index 1bf933a53..912a56492 100644 --- a/app/models/search/record.rb +++ b/app/models/search/record.rb @@ -44,9 +44,7 @@ class Search::Record < ApplicationRecord end scope :matching, ->(query, account_id) do - account_key = "account#{account_id}" - full_query = "+#{account_key} +(#{query})" - where("MATCH(#{table_name}.account_key, #{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", full_query) + where("MATCH(#{table_name}.account_key, #{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", query) end scope :for_user, ->(user) do diff --git a/db/migrate/20251121092508_add_account_key_to_search_records.rb b/db/migrate/20251121092508_add_account_key_to_search_records.rb index f1cd26a99..8960f56bc 100644 --- a/db/migrate/20251121092508_add_account_key_to_search_records.rb +++ b/db/migrate/20251121092508_add_account_key_to_search_records.rb @@ -3,20 +3,8 @@ class AddAccountKeyToSearchRecords < ActiveRecord::Migration[8.2] 16.times do |shard_id| table_name = "search_records_#{shard_id}" - # Add account_key column add_column table_name, :account_key, :string, null: false, default: "" - - # Backfill existing records with account_key - execute <<-SQL - UPDATE #{table_name} - SET account_key = CONCAT('account', account_id) - SQL - - # Create new fulltext index including account_key add_index table_name, [:account_key, :content, :title], type: :fulltext - - # Drop existing fulltext index - remove_index table_name, column: [:content, :title], type: :fulltext end end @@ -24,13 +12,7 @@ class AddAccountKeyToSearchRecords < ActiveRecord::Migration[8.2] 16.times do |shard_id| table_name = "search_records_#{shard_id}" - # Drop new fulltext index remove_index table_name, column: [:account_key, :content, :title], type: :fulltext - - # Restore original fulltext index - add_index table_name, [:content, :title], type: :fulltext - - # Remove account_key column remove_column table_name, :account_key end end diff --git a/db/schema.rb b/db/schema.rb index bfe3bebcc..d94794ed3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -408,21 +408,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.index ["user_id"], name: "index_search_queries_on_user_id" end - create_table "search_records", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| - t.uuid "account_id", null: false - t.string "account_key", 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_on_account_id" - t.index ["account_key", "content", "title"], name: "index_search_records_on_account_key_and_content_and_title", type: :fulltext - t.index ["searchable_type", "searchable_id"], name: "index_search_records_on_searchable_type_and_searchable_id", unique: true - 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.string "account_key", default: "", null: false @@ -435,6 +420,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_0_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_0_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -450,6 +436,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_1_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_1_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -465,6 +452,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_10_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_10_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -480,6 +468,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_11_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_11_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -495,6 +484,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_12_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_12_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -510,6 +500,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_13_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_13_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -525,6 +516,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_14_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_14_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -540,6 +532,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_15_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_15_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -555,6 +548,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_2_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_2_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -570,6 +564,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_3_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_3_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -585,6 +580,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_4_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_4_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -600,6 +596,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_5_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_5_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -615,6 +612,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_6_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_6_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -630,6 +628,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_7_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_7_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -645,6 +644,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_8_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_8_on_account_key_and_content_and_title", type: :fulltext + 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 @@ -660,6 +660,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_9_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_9_on_account_key_and_content_and_title", type: :fulltext + 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 From 7c15be02f894898a289558bcf71972215666d2ca Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Fri, 21 Nov 2025 11:48:29 +0100 Subject: [PATCH 4/7] Skip navigation when opening a new tab --- app/javascript/controllers/turbo_navigation_controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/controllers/turbo_navigation_controller.js b/app/javascript/controllers/turbo_navigation_controller.js index 4c5823620..d755342e9 100644 --- a/app/javascript/controllers/turbo_navigation_controller.js +++ b/app/javascript/controllers/turbo_navigation_controller.js @@ -6,6 +6,8 @@ export default class extends Controller { } backIfSamePath(event) { + if (event.ctrlKey || event.metaKey || event.shiftKey) { return } + const link = event.target.closest("a") const targetUrl = new URL(link.href) From 3c4c2b5eaa236be42c7aae54feb837a695f79d3a Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Fri, 21 Nov 2025 10:48:53 +0000 Subject: [PATCH 5/7] Don't run migrations on deploy --- Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6341f3fc1..ea1e28f8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,9 +59,6 @@ RUN useradd rails --create-home --shell /bin/bash && \ chown -R rails:rails db log storage tmp USER rails:rails -# Entrypoint prepares the database. -ENTRYPOINT ["/rails/bin/docker-entrypoint"] - # Ruby GC tuning values pulled from Autotuner recommendations ENV RUBY_GC_HEAP_0_INIT_SLOTS=692636 \ RUBY_GC_HEAP_1_INIT_SLOTS=175943 \ From adff4591fec9662fe116ff692a1192d79463b355 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 21 Nov 2025 11:05:43 +0000 Subject: [PATCH 6/7] Search with the tenant key The key has been added and populated so use it for searching. --- app/models/search/record.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/search/record.rb b/app/models/search/record.rb index 912a56492..1bf933a53 100644 --- a/app/models/search/record.rb +++ b/app/models/search/record.rb @@ -44,7 +44,9 @@ class Search::Record < ApplicationRecord end scope :matching, ->(query, account_id) do - where("MATCH(#{table_name}.account_key, #{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", query) + account_key = "account#{account_id}" + full_query = "+#{account_key} +(#{query})" + where("MATCH(#{table_name}.account_key, #{table_name}.content, #{table_name}.title) AGAINST(? IN BOOLEAN MODE)", full_query) end scope :for_user, ->(user) do From cb71af823120231fc21f693cb722f408732c9de2 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 21 Nov 2025 11:25:16 +0000 Subject: [PATCH 7/7] Remove the old fulltext indexes We only search by account_key, content, and title now. --- ...old_fulltext_indexes_from_search_records.rb | 16 ++++++++++++++++ db/schema.rb | 18 +----------------- 2 files changed, 17 insertions(+), 17 deletions(-) create mode 100644 db/migrate/20251121112416_remove_old_fulltext_indexes_from_search_records.rb diff --git a/db/migrate/20251121112416_remove_old_fulltext_indexes_from_search_records.rb b/db/migrate/20251121112416_remove_old_fulltext_indexes_from_search_records.rb new file mode 100644 index 000000000..f9d38dadf --- /dev/null +++ b/db/migrate/20251121112416_remove_old_fulltext_indexes_from_search_records.rb @@ -0,0 +1,16 @@ +class RemoveOldFulltextIndexesFromSearchRecords < ActiveRecord::Migration[8.2] + def up + # Remove the old fulltext indexes (content, title) from all 16 search_records shards + # We're keeping the new indexes (account_key, content, title) for tenant-aware searching + (0..15).each do |shard| + remove_index "search_records_#{shard}", name: "index_search_records_#{shard}_on_content_and_title" + end + end + + def down + # Re-create the old fulltext indexes in case we need to rollback + (0..15).each do |shard| + add_index "search_records_#{shard}", [ :content, :title ], type: :fulltext, name: "index_search_records_#{shard}_on_content_and_title" + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d94794ed3..f951af36b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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_21_092508) do +ActiveRecord::Schema[8.2].define(version: 2025_11_21_112416) 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 @@ -420,7 +420,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_0_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_0_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -436,7 +435,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_1_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_1_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -452,7 +450,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_10_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_10_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -468,7 +465,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_11_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_11_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -484,7 +480,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_12_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_12_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -500,7 +495,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_13_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_13_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -516,7 +510,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_14_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_14_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -532,7 +525,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_15_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_15_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -548,7 +540,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_2_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_2_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -564,7 +555,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_3_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_3_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -580,7 +570,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_4_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_4_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -596,7 +585,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_5_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_5_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -612,7 +600,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_6_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_6_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -628,7 +615,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_7_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_7_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -644,7 +630,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_8_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_8_on_account_key_and_content_and_title", type: :fulltext - 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 @@ -660,7 +645,6 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_21_092508) do t.string "title" t.index ["account_id"], name: "index_search_records_9_on_account_id" t.index ["account_key", "content", "title"], name: "index_search_records_9_on_account_key_and_content_and_title", type: :fulltext - 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