- <%= form_tag admin_tags_path, method: :get, class: 'search-topics' do %>
+ <%= form_tag admin_topics_path, method: :get, class: 'search-topics' do %>
<%= label_tag :q, "Search topics", class: "visuallyhidden" %>
diff --git a/db/migrate/20210422053757_optimize_free_text_search_indexes.rb b/db/migrate/20210422053757_optimize_free_text_search_indexes.rb
new file mode 100644
index 000000000..0e4911940
--- /dev/null
+++ b/db/migrate/20210422053757_optimize_free_text_search_indexes.rb
@@ -0,0 +1,99 @@
+class OptimizeFreeTextSearchIndexes < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ def up
+ execute "CREATE EXTENSION IF NOT EXISTS btree_gin"
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_petitions_for_search
+ ON petitions USING gin((
+ to_tsvector('english', id::text) ||
+ to_tsvector('english', action::text) ||
+ to_tsvector('english', background::text) ||
+ to_tsvector('english', COALESCE(additional_details, '')::text)),
+ state, debate_state
+ );
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_archived_petitions_for_search
+ ON archived_petitions USING gin((
+ to_tsvector('english', id::text) ||
+ to_tsvector('english', action::text) ||
+ to_tsvector('english', background::text) ||
+ to_tsvector('english', COALESCE(additional_details, '')::text)),
+ state, parliament_id, debate_state
+ );
+ SQL
+
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_petitions_on_action"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_petitions_on_background"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_petitions_on_additional_details"
+ execute "ANALYZE petitions"
+
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_archived_petitions_on_action"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_archived_petitions_on_background"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_archived_petitions_on_additional_details"
+ execute "ANALYZE archived_petitions"
+ end
+
+ def down
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_petitions_on_action
+ ON petitions USING gin(
+ to_tsvector('english', action)
+ );
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_petitions_on_background
+ ON petitions USING gin(
+ to_tsvector('english', background)
+ );
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_petitions_on_additional_details
+ ON petitions USING gin(
+ to_tsvector('english', additional_details)
+ );
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_archived_petitions_on_action
+ ON archived_petitions USING gin(
+ to_tsvector('english', action)
+ );
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_archived_petitions_on_background
+ ON archived_petitions USING gin(
+ to_tsvector('english', background)
+ );
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_archived_petitions_on_additional_details
+ ON archived_petitions USING gin(
+ to_tsvector('english', additional_details)
+ );
+ SQL
+
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_petitions_for_search"
+ execute "ANALYZE petitions"
+
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_archived_petitions_for_search"
+ execute "ANALYZE archived_petitions"
+
+ execute "DROP EXTENSION IF EXISTS btree_gin"
+ end
+end
diff --git a/db/migrate/20210425143147_optimize_search_indexes_for_departments.rb b/db/migrate/20210425143147_optimize_search_indexes_for_departments.rb
new file mode 100644
index 000000000..b03074fa1
--- /dev/null
+++ b/db/migrate/20210425143147_optimize_search_indexes_for_departments.rb
@@ -0,0 +1,21 @@
+class OptimizeSearchIndexesForDepartments < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ def up
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_departments_for_search
+ ON departments USING gin((
+ to_tsvector('english', name::text) ||
+ to_tsvector('english', COALESCE(acronym, '')::text)
+ ));
+ SQL
+
+ execute "ANALYZE departments"
+ end
+
+ def down
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_departments_for_search"
+ execute "ANALYZE departments"
+ end
+end
diff --git a/db/migrate/20210425143158_optimize_search_indexes_for_invalidations.rb b/db/migrate/20210425143158_optimize_search_indexes_for_invalidations.rb
new file mode 100644
index 000000000..a0ad961af
--- /dev/null
+++ b/db/migrate/20210425143158_optimize_search_indexes_for_invalidations.rb
@@ -0,0 +1,46 @@
+class OptimizeSearchIndexesForInvalidations < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ def up
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_invalidations_for_search
+ ON invalidations USING gin((
+ to_tsvector('english', summary::text) ||
+ to_tsvector('english', COALESCE(details)::text) ||
+ to_tsvector('english', COALESCE(petition_id)::text)
+ ));
+ SQL
+
+ execute "DROP INDEX CONCURRENTLY IF EXISTS ft_index_invalidations_on_details"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS ft_index_invalidations_on_id"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS ft_index_invalidations_on_petition_id"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS ft_index_invalidations_on_summary"
+ execute "ANALYZE invalidations"
+ end
+
+ def down
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS ft_index_invalidations_on_details
+ ON invalidations USING gin(to_tsvector('english', details::text));
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS ft_index_invalidations_on_id
+ ON invalidations USING gin(to_tsvector('english', id::text));
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS ft_index_invalidations_on_petition_id
+ ON invalidations USING gin(to_tsvector('english', petition_id::text));
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS ft_index_invalidations_on_summary
+ ON invalidations USING gin(to_tsvector('english', summary::text));
+ SQL
+
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_invalidations_for_search"
+ execute "ANALYZE invalidations"
+ end
+end
diff --git a/db/migrate/20210425143206_optimize_search_indexes_for_tags.rb b/db/migrate/20210425143206_optimize_search_indexes_for_tags.rb
new file mode 100644
index 000000000..28dad7c8f
--- /dev/null
+++ b/db/migrate/20210425143206_optimize_search_indexes_for_tags.rb
@@ -0,0 +1,33 @@
+class OptimizeSearchIndexesForTags < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ def up
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_tags_for_search
+ ON tags USING gin((
+ to_tsvector('english', name::text) ||
+ to_tsvector('english', COALESCE(description)::text)
+ ));
+ SQL
+
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_ft_tags_on_name"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_ft_tags_on_description"
+ execute "ANALYZE tags"
+ end
+
+ def down
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS index_ft_tags_on_name
+ ON tags USING gin(to_tsvector('english', name));
+ SQL
+
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS index_ft_tags_on_description
+ ON tags USING gin(to_tsvector('english', description));
+ SQL
+
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_tags_for_search"
+ execute "ANALYZE tags"
+ end
+end
diff --git a/db/migrate/20210425143213_optimize_search_indexes_for_topics.rb b/db/migrate/20210425143213_optimize_search_indexes_for_topics.rb
new file mode 100644
index 000000000..e97675b18
--- /dev/null
+++ b/db/migrate/20210425143213_optimize_search_indexes_for_topics.rb
@@ -0,0 +1,21 @@
+class OptimizeSearchIndexesForTopics < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ def up
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ index_topics_for_search
+ ON topics USING gin((
+ to_tsvector('english', code::text) ||
+ to_tsvector('english', name::text)
+ ));
+ SQL
+
+ execute "ANALYZE topics"
+ end
+
+ def down
+ execute "DROP INDEX CONCURRENTLY IF EXISTS index_topics_for_search"
+ execute "ANALYZE topics"
+ end
+end
diff --git a/db/migrate/20210425185707_add_name_index_to_departments.rb b/db/migrate/20210425185707_add_name_index_to_departments.rb
new file mode 100644
index 000000000..300b99826
--- /dev/null
+++ b/db/migrate/20210425185707_add_name_index_to_departments.rb
@@ -0,0 +1,11 @@
+class AddNameIndexToDepartments < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ def up
+ add_index(:departments, :name, algorithm: :concurrently)
+ end
+
+ def down
+ remove_index(:departments, :name, algorithm: :concurrently)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 11272852a..6d1c2d395 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,9 +10,10 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2021_04_21_214559) do
+ActiveRecord::Schema.define(version: 2021_04_25_185707) do
# These are extensions that must be enabled in order to support this database
+ enable_extension "btree_gin"
enable_extension "intarray"
enable_extension "plpgsql"
@@ -152,9 +153,7 @@
t.datetime "anonymized_at"
t.integer "moderated_by_id"
t.integer "topics", default: [], null: false, array: true
- t.index "to_tsvector('english'::regconfig, (action)::text)", name: "index_archived_petitions_on_action", using: :gin
- t.index "to_tsvector('english'::regconfig, (background)::text)", name: "index_archived_petitions_on_background", using: :gin
- t.index "to_tsvector('english'::regconfig, additional_details)", name: "index_archived_petitions_on_additional_details", using: :gin
+ t.index "((((to_tsvector('english'::regconfig, (id)::text) || to_tsvector('english'::regconfig, (action)::text)) || to_tsvector('english'::regconfig, (background)::text)) || to_tsvector('english'::regconfig, COALESCE(additional_details, ''::text)))), state, parliament_id, debate_state", name: "index_archived_petitions_for_search", using: :gin
t.index ["anonymized_at"], name: "index_archived_petitions_on_anonymized_at"
t.index ["debate_state", "parliament_id"], name: "index_archived_petitions_on_debate_state_and_parliament_id"
t.index ["departments"], name: "index_archived_petitions_on_departments", opclass: :gin__int_ops, using: :gin
@@ -312,6 +311,8 @@
t.date "end_date"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index "((to_tsvector('english'::regconfig, (name)::text) || to_tsvector('english'::regconfig, (COALESCE(acronym, ''::character varying))::text)))", name: "index_departments_for_search", using: :gin
+ t.index ["name"], name: "index_departments_on_name"
end
create_table "dissolution_notifications", id: :uuid, default: nil, force: :cascade do |t|
@@ -393,10 +394,7 @@
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "domain", limit: 255
- t.index "to_tsvector('english'::regconfig, (details)::text)", name: "ft_index_invalidations_on_details", using: :gin
- t.index "to_tsvector('english'::regconfig, (id)::text)", name: "ft_index_invalidations_on_id", using: :gin
- t.index "to_tsvector('english'::regconfig, (petition_id)::text)", name: "ft_index_invalidations_on_petition_id", using: :gin
- t.index "to_tsvector('english'::regconfig, (summary)::text)", name: "ft_index_invalidations_on_summary", using: :gin
+ t.index "(((to_tsvector('english'::regconfig, (summary)::text) || to_tsvector('english'::regconfig, (COALESCE(details))::text)) || to_tsvector('english'::regconfig, (COALESCE(petition_id))::text)))", name: "index_invalidations_for_search", using: :gin
t.index ["cancelled_at"], name: "index_invalidations_on_cancelled_at"
t.index ["completed_at"], name: "index_invalidations_on_completed_at"
t.index ["petition_id"], name: "index_invalidations_on_petition_id"
@@ -508,10 +506,8 @@
t.integer "moderated_by_id"
t.integer "deadline_extension", default: 0, null: false
t.integer "topics", default: [], null: false, array: true
+ t.index "((((to_tsvector('english'::regconfig, (id)::text) || to_tsvector('english'::regconfig, (action)::text)) || to_tsvector('english'::regconfig, (background)::text)) || to_tsvector('english'::regconfig, COALESCE(additional_details, ''::text)))), state, debate_state", name: "index_petitions_for_search", using: :gin
t.index "((last_signed_at > signature_count_validated_at))", name: "index_petitions_on_validated_at_and_signed_at"
- t.index "to_tsvector('english'::regconfig, (action)::text)", name: "index_petitions_on_action", using: :gin
- t.index "to_tsvector('english'::regconfig, (background)::text)", name: "index_petitions_on_background", using: :gin
- t.index "to_tsvector('english'::regconfig, additional_details)", name: "index_petitions_on_additional_details", using: :gin
t.index ["anonymized_at"], name: "index_petitions_on_anonymized_at"
t.index ["archived_at"], name: "index_petitions_on_archived_at"
t.index ["created_at", "state"], name: "index_petitions_on_created_at_and_state"
@@ -680,8 +676,7 @@
t.string "description", limit: 200
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.index "to_tsvector('english'::regconfig, (description)::text)", name: "index_ft_tags_on_description", using: :gin
- t.index "to_tsvector('english'::regconfig, (name)::text)", name: "index_ft_tags_on_name", using: :gin
+ t.index "((to_tsvector('english'::regconfig, (name)::text) || to_tsvector('english'::regconfig, (COALESCE(description))::text)))", name: "index_tags_for_search", using: :gin
t.index ["name"], name: "index_tags_on_name", unique: true
end
@@ -697,6 +692,7 @@
t.string "name", limit: 100, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index "((to_tsvector('english'::regconfig, (code)::text) || to_tsvector('english'::regconfig, (name)::text)))", name: "index_topics_for_search", using: :gin
t.index ["code"], name: "index_topics_on_code", unique: true
t.index ["name"], name: "index_topics_on_name", unique: true
end
diff --git a/spec/models/concerns/browseable_spec.rb b/spec/models/concerns/browseable_spec.rb
index 5653fb58c..ae0c78c5a 100644
--- a/spec/models/concerns/browseable_spec.rb
+++ b/spec/models/concerns/browseable_spec.rb
@@ -48,10 +48,13 @@ def self.all
describe Browseable::Search do
let(:scopes) { { all: -> { self }, open: -> { self } } }
let(:filters) { {} }
- let(:klass) { double(:klass, facet_definitions: scopes, filter_definitions: filters, default_page_size: 50, max_page_size: 50) }
+ let(:columns) { [Browseable::Query::Column.new('action', 'english', false)] }
+ let(:klass) { double(:klass, facet_definitions: scopes, filter_definitions: filters, query_columns: columns, default_page_size: 50, max_page_size: 50) }
let(:params) { { q: 'search', page: '3'} }
let(:search) { described_class.new(klass, params) }
+ let(:connection) { double(:connection) }
+
it "is enumerable" do
expect(search).to respond_to(:each)
end
@@ -166,16 +169,14 @@ def self.all
let(:petitions) { [petition] }
let(:results) { double(:results, to_a: petitions) }
+ let(:sql) { %[((to_tsvector('english', "petitions"."action"::text)) @@ plainto_tsquery('english', :query))] }
+
before do
- allow(klass).to receive(:basic_search).with('search').and_return(klass)
- allow(arel_table).to receive(:[]).with("*").and_return("*")
- allow(klass).to receive(:basic_search).with('search').and_return(klass)
- allow(klass).to receive(:except).with(:select).and_return(klass)
- allow(klass).to receive(:arel_table).and_return(arel_table)
- allow(klass).to receive(:select).with("*").and_return(klass)
- allow(klass).to receive(:except).with(:order).and_return(klass)
+ allow(klass).to receive(:quoted_table_name).and_return('"petitions"')
+ allow(klass).to receive(:connection).and_return(connection)
+ allow(connection).to receive(:quote_column_name).with('action').and_return('"action"')
+ allow(klass).to receive(:where).with([sql, query: "search"]).and_return(klass)
allow(klass).to receive(:paginate).with(page: 3, per_page: 50).and_return(results)
- allow(results).to receive(:to_a).and_return(petitions)
allow(results).to receive(:previous_page).and_return(2)
end
@@ -214,16 +215,14 @@ def self.all
let(:petitions) { [petition] }
let(:results) { double(:results, to_a: petitions) }
+ let(:sql) { %[((to_tsvector('english', "petitions"."action"::text)) @@ plainto_tsquery('english', :query))] }
+
before do
- allow(klass).to receive(:basic_search).with('search').and_return(klass)
- allow(arel_table).to receive(:[]).with("*").and_return("*")
- allow(klass).to receive(:basic_search).with('search').and_return(klass)
- allow(klass).to receive(:except).with(:select).and_return(klass)
- allow(klass).to receive(:arel_table).and_return(arel_table)
- allow(klass).to receive(:select).with("*").and_return(klass)
- allow(klass).to receive(:except).with(:order).and_return(klass)
+ allow(klass).to receive(:quoted_table_name).and_return('"petitions"')
+ allow(klass).to receive(:connection).and_return(connection)
+ allow(connection).to receive(:quote_column_name).with('action').and_return('"action"')
+ allow(klass).to receive(:where).with([sql, query: "search"]).and_return(klass)
allow(klass).to receive(:paginate).with(page: 3, per_page: 50).and_return(results)
- allow(results).to receive(:to_a).and_return(petitions)
allow(results).to receive(:next_page).and_return(4)
end
@@ -376,20 +375,15 @@ def self.all
let(:petitions) { [petition] }
let(:results) { double(:results, to_a: petitions) }
+ let(:sql) { %[((to_tsvector('english', "petitions"."action"::text)) @@ plainto_tsquery('english', :query))] }
+
context "when there is a search term" do
before do
- # This list of stubs is effectively testing the implementation of the
- # execute_search private method, however this is important because of
- # the need to exclude the ranking column added by the textacular gem
- # which can add a significant performance penalty.
-
- expect(arel_table).to receive(:[]).with("*").and_return("*")
- expect(klass).to receive(:basic_search).with('search').and_return(klass)
- expect(klass).to receive(:except).with(:select).and_return(klass)
- expect(klass).to receive(:arel_table).and_return(arel_table)
- expect(klass).to receive(:select).with("*").and_return(klass)
- expect(klass).to receive(:except).with(:order).and_return(klass)
- expect(klass).to receive(:paginate).with(page: 3, per_page: 50).and_return(results)
+ allow(klass).to receive(:quoted_table_name).and_return('"petitions"')
+ allow(klass).to receive(:connection).and_return(connection)
+ allow(connection).to receive(:quote_column_name).with('action').and_return('"action"')
+ allow(klass).to receive(:where).with([sql, query: "search"]).and_return(klass)
+ allow(klass).to receive(:paginate).with(page: 3, per_page: 50).and_return(results)
expect(results).to receive(:to_a).and_return(petitions)
end