Commit 135659a7 authored by Yorick Peterse's avatar Yorick Peterse Committed by Robert Speicher

Use ILIKE/LIKE + UNION in Project.search

This chance is broken up in two steps:

1. Use ILIKE on PostgreSQL and LIKE on MySQL, instead of using
   "WHERE lower(x) LIKE lower(y)" as ILIKE is significantly faster than
   using lower(). In many cases the use of lower() will force a slow
   sequence scan.

2. Instead of using 1 query that searches both projects and namespaces
   using a JOIN we're using 2 separate queries that are UNION'd
   together. Using a JOIN would force a slow sequence scan, using a
   UNION avoids this.

This method now uses Arel as Arel automatically uses ILIKE on PostgreSQL
and LIKE on MySQL, removing the need to handle this manually.
parent d24ee2a2
...@@ -266,13 +266,31 @@ class Project < ActiveRecord::Base ...@@ -266,13 +266,31 @@ class Project < ActiveRecord::Base
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end end
# Searches for a list of projects based on the query given in `query`.
#
# On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
# search. On MySQL a regular "LIKE" is used as it's already
# case-insensitive.
#
# query - The search query as a String.
def search(query) def search(query)
ptable = Project.arel_table
ntable = Namespace.arel_table
pattern = "%#{query}%"
projects = select(:id).where(
ptable[:path].matches(pattern).
or(ptable[:name].matches(pattern)).
or(ptable[:description].matches(pattern))
)
namespaces = select(:id).
joins(:namespace). joins(:namespace).
where('LOWER(projects.name) LIKE :query OR where(ntable[:name].matches(pattern))
LOWER(projects.path) LIKE :query OR
LOWER(namespaces.name) LIKE :query OR union = Gitlab::SQL::Union.new([projects, namespaces])
LOWER(projects.description) LIKE :query',
query: "%#{query.try(:downcase)}%") where("projects.id IN (#{union.to_sql})")
end end
def search_by_visibility(level) def search_by_visibility(level)
......
...@@ -582,7 +582,58 @@ describe Project, models: true do ...@@ -582,7 +582,58 @@ describe Project, models: true do
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy } it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey } it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
end end
end
describe '.search' do
let(:project) { create(:project, description: 'kitten mittens') }
it 'returns projects with a matching name' do
expect(described_class.search(project.name)).to eq([project])
end
it 'returns projects with a partially matching name' do
expect(described_class.search(project.name[0..2])).to eq([project])
end
it 'returns projects with a matching name regardless of the casing' do
expect(described_class.search(project.name.upcase)).to eq([project])
end
it 'returns projects with a matching description' do
expect(described_class.search(project.description)).to eq([project])
end
it 'returns projects with a partially matching description' do
expect(described_class.search('kitten')).to eq([project])
end
it 'returns projects with a matching description regardless of the casing' do
expect(described_class.search('KITTEN')).to eq([project])
end
it 'returns projects with a matching path' do
expect(described_class.search(project.path)).to eq([project])
end
it 'returns projects with a partially matching path' do
expect(described_class.search(project.path[0..2])).to eq([project])
end
it 'returns projects with a matching path regardless of the casing' do
expect(described_class.search(project.path.upcase)).to eq([project])
end
it 'returns projects with a matching namespace name' do
expect(described_class.search(project.namespace.name)).to eq([project])
end
it 'returns projects with a partially matching namespace name' do
expect(described_class.search(project.namespace.name[0..2])).to eq([project])
end
it 'returns projects with a matching namespace name regardless of the casing' do
expect(described_class.search(project.namespace.name.upcase)).to eq([project])
end
end end
describe '#rename_repo' do describe '#rename_repo' do
......
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