Commit cd8596fe authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Use full text search when searching issues

parent 2b195836
......@@ -117,6 +117,10 @@ module IssuableCollections
options[:attempt_group_search_optimizations] = true
end
if collection_type == 'Issue' && Feature.enabled?(:issues_full_text_search, @project || @group, default_enabled: :yaml)
options[:attempt_full_text_search] = true
end
params.permit(finder_type.valid_params).merge(options)
end
end
......
......@@ -37,6 +37,7 @@
# attempt_project_search_optimizations: boolean
# crm_contact_id: integer
# crm_organization_id: integer
# attempt_full_text_search: boolean
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
......@@ -46,6 +47,7 @@ class IssuableFinder
requires_cross_project_access unless: -> { params.project? }
FULL_TEXT_SEARCH_TERM_REGEX = /\A[\p{ASCII}|\p{Latin}]+\z/.freeze
NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze
attr_accessor :current_user, :params
......@@ -331,6 +333,8 @@ class IssuableFinder
return items if items.is_a?(ActiveRecord::NullRelation)
return items if Feature.enabled?(:disable_anonymous_search, type: :ops) && current_user.nil?
return items.pg_full_text_search(search) if use_full_text_search?
if use_cte_for_search?
cte = Gitlab::SQL::CTE.new(klass.table_name, items)
......@@ -341,6 +345,10 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
def use_full_text_search?
params[:attempt_full_text_search] && params[:search] =~ FULL_TEXT_SEARCH_TERM_REGEX
end
# rubocop: disable CodeReuse/ActiveRecord
def by_iids(items)
params[:iids].present? ? items.where(iid: params[:iids]) : items
......
......@@ -13,9 +13,9 @@
# This module sets up an after_commit hook that updates the search data
# when the searchable columns are changed.
#
# This also adds a `full_text_search` scope so you can do:
# This also adds a `pg_full_text_search` scope so you can do:
#
# Model.full_text_search("some search term")
# Model.pg_full_text_search("some search term")
module PgFullTextSearchable
extend ActiveSupport::Concern
......@@ -82,5 +82,20 @@ module PgFullTextSearchable
update_search_data!
end
end
def pg_full_text_search(search_term)
search_data_table = reflect_on_association(:search_data).klass.arel_table
joins(:search_data).where(
Arel::Nodes::InfixOperation.new(
'@@',
search_data_table[:search_vector],
Arel::Nodes::NamedFunction.new(
'websearch_to_tsquery',
[Arel::Nodes.build_quoted(TEXT_SEARCH_DICTIONARY), Arel::Nodes.build_quoted(search_term)]
)
)
)
end
end
end
---
name: issues_full_text_search
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71913
rollout_issue_url:
milestone: '14.5'
type: development
group: group::project management
default_enabled: false
......@@ -144,7 +144,7 @@ module Gitlab
def params_include_filters?
non_filtering_params = %i[
scope state sort group_id include_subgroups
attempt_group_search_optimizations non_archived issue_types
attempt_group_search_optimizations attempt_full_text_search non_archived issue_types
]
finder.params.except(*non_filtering_params).values.any?
......
......@@ -13,7 +13,22 @@ RSpec.describe DashboardController do
end
describe 'GET issues' do
it_behaves_like 'issuables list meta-data', :issue, :issues
context 'when issues_full_text_search is disabled' do
before do
stub_feature_flags(issues_full_text_search: false)
end
it_behaves_like 'issuables list meta-data', :issue, :issues
end
context 'when issues_full_text_search is enabled' do
before do
stub_feature_flags(issues_full_text_search: true)
end
it_behaves_like 'issuables list meta-data', :issue, :issues
end
it_behaves_like 'issuables requiring filter', :issues
end
......
......@@ -72,7 +72,21 @@ RSpec.describe Projects::IssuesController do
project.add_developer(user)
end
it_behaves_like "issuables list meta-data", :issue
context 'when issues_full_text_search is disabled' do
before do
stub_feature_flags(issues_full_text_search: false)
end
it_behaves_like 'issuables list meta-data', :issue
end
context 'when issues_full_text_search is enabled' do
before do
stub_feature_flags(issues_full_text_search: true)
end
it_behaves_like 'issuables list meta-data', :issue
end
it_behaves_like 'set sort order from user preference' do
let(:sorting_param) { 'updated_asc' }
......
......@@ -497,6 +497,8 @@ RSpec.describe 'Filter issues', :js do
end
it 'filters issues by searched text containing special characters' do
stub_feature_flags(issues_full_text_search: false)
issue = create(:issue, project: project, author: user, title: "issue with !@\#{$%^&*()-+")
search = '!@#{$%^&*()-+'
......
......@@ -632,6 +632,29 @@ RSpec.describe IssuesFinder do
end
end
context 'filtering by issue term using full-text search' do
let(:params) { { search: search_term, attempt_full_text_search: true } }
let_it_be(:english) { create(:issue, project: project1, title: 'title', description: 'something english') }
let_it_be(:japanese) { create(:issue, project: project1, title: '日本語 title', description: 'another english description') }
context 'with latin search term' do
let(:search_term) { 'title english' }
it 'returns matching issues' do
expect(issues).to contain_exactly(english, japanese)
end
end
context 'with non-latin search term' do
let(:search_term) { '日本語' }
it 'returns matching issues' do
expect(issues).to contain_exactly(japanese)
end
end
end
context 'filtering by issues iids' do
let(:params) { { iids: [issue3.iid] } }
......
......@@ -55,6 +55,34 @@ RSpec.describe PgFullTextSearchable do
end
end
describe '.pg_full_text_search' do
let(:english) { model_class.create!(title: 'title', description: 'something english') }
let(:with_accent) { model_class.create!(title: 'Jürgen', description: 'Ærøskøbing') }
let(:japanese) { model_class.create!(title: '日本語 title', description: 'another english description') }
before do
model_class.pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }]
[english, with_accent, japanese].each(&:update_search_data!)
end
it 'searches across all fields' do
expect(model_class.pg_full_text_search('title english')).to contain_exactly(english, japanese)
end
it 'searches for exact term with quotes' do
expect(model_class.pg_full_text_search('"something english"')).to contain_exactly(english)
end
it 'ignores accents' do
expect(model_class.pg_full_text_search('jurgen')).to contain_exactly(with_accent)
end
it 'does not support searching by non-Latin characters' do
expect(model_class.pg_full_text_search('日本')).to be_empty
end
end
describe '#update_search_data!' do
let(:model) { model_class.create!(title: 'title', description: 'description') }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment