Commit 55bec644 authored by Robert Speicher's avatar Robert Speicher

Merge branch '235676-prioritize-exact-matches' into 'master'

Prioritize exact matches on project search

See merge request gitlab-org/gitlab!43136
parents 18a28232 7085ed1c
......@@ -50,7 +50,12 @@ class ProjectsFinder < UnionFinder
use_cte = params.delete(:use_cte)
collection = Project.wrap_with_cte(collection) if use_cte
collection = filter_projects(collection)
sort(collection)
if params[:sort] == 'similarity' && params[:search] && Feature.enabled?(:project_finder_similarity_sort)
collection.sorted_by_similarity_desc(params[:search])
else
sort(collection)
end
end
private
......@@ -209,7 +214,11 @@ class ProjectsFinder < UnionFinder
end
def sort(items)
params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.projects_order_id_desc
if params[:sort].present?
items.sort_by_attribute(params[:sort])
else
items.projects_order_id_desc
end
end
def by_archived(projects)
......
......@@ -13,13 +13,17 @@ module Resolvers
description: 'Search query for project name, path, or description'
argument :ids, [GraphQL::ID_TYPE],
required: false,
description: 'Filter projects by IDs'
required: false,
description: 'Filter projects by IDs'
argument :search_namespaces, GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Include namespace in project search'
argument :sort, GraphQL::STRING_TYPE,
required: false,
description: 'Sort order of results'
def resolve(**args)
ProjectsFinder
.new(current_user: current_user, params: project_finder_params(args), project_ids_relation: parse_gids(args[:ids]))
......@@ -33,7 +37,8 @@ module Resolvers
without_deleted: true,
non_public: params[:membership],
search: params[:search],
search_namespaces: params[:search_namespaces]
search_namespaces: params[:search_namespaces],
sort: params[:sort]
}.compact
end
......
---
title: Add sort by similarity to getProjects GraphQL call
merge_request: 43136
author:
type: changed
---
name: project_finder_similarity_sort
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43136
rollout_issue_url:
type: development
group: group::threat_insights
default_enabled: false
......@@ -15010,6 +15010,11 @@ type Query {
Include namespace in project search
"""
searchNamespaces: Boolean
"""
Sort order of results
"""
sort: String
): ProjectConnection
"""
......
......@@ -43593,6 +43593,16 @@
},
"defaultValue": null
},
{
"name": "sort",
"description": "Sort order of results",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -6,6 +6,7 @@ query getProjects(
$after: String = ""
$first: Int!
$searchNamespaces: Boolean = false
$sort: String
) {
projects(
search: $search
......@@ -13,6 +14,7 @@ query getProjects(
first: $first
membership: true
searchNamespaces: $searchNamespaces
sort: $sort
) {
nodes {
...Project
......
......@@ -31,6 +31,10 @@ RSpec.describe ProjectsFinder, :do_not_mock_admin_mode do
let(:use_cte) { true }
let(:finder) { described_class.new(params: params.merge(use_cte: use_cte), current_user: current_user, project_ids_relation: project_ids_relation) }
before do
stub_feature_flags(project_finder_similarity_sort: false)
end
subject { finder.execute }
shared_examples 'ProjectFinder#execute examples' do
......@@ -304,9 +308,33 @@ RSpec.describe ProjectsFinder, :do_not_mock_admin_mode do
end
describe 'sorting' do
let(:params) { { sort: 'name_asc' } }
context 'when sorting by a field' do
let(:params) { { sort: 'name_asc' } }
it { is_expected.to eq([internal_project, public_project]) }
end
it { is_expected.to eq([internal_project, public_project]) }
context 'when sorting by similarity' do
let(:params) { { sort: 'similarity', search: 'pro' } }
let_it_be(:internal_project2) do
create(:project, :internal, group: group, name: 'projA', path: 'projA')
end
let_it_be(:internal_project3) do
create(:project, :internal, group: group, name: 'projABC', path: 'projABC')
end
let_it_be(:internal_project4) do
create(:project, :internal, group: group, name: 'projAB', path: 'projAB')
end
before do
stub_feature_flags(project_finder_similarity_sort: true)
end
it { is_expected.to eq([internal_project2, internal_project4, internal_project3]) }
end
end
describe 'with admin user' do
......
......@@ -15,7 +15,6 @@ RSpec.describe Resolvers::ProjectsResolver do
let_it_be(:group_project) { create(:project, :public, group: group) }
let_it_be(:private_project) { create(:project, :private) }
let_it_be(:other_private_project) { create(:project, :private) }
let_it_be(:other_private_project) { create(:project, :private) }
let_it_be(:private_group_project) { create(:project, :private, group: private_group) }
let_it_be(:user) { create(:user) }
......@@ -28,6 +27,10 @@ RSpec.describe Resolvers::ProjectsResolver do
private_group.add_developer(user)
end
before do
stub_feature_flags(project_finder_similarity_sort: false)
end
context 'when user is not logged in' do
let(:current_user) { nil }
......@@ -117,6 +120,24 @@ RSpec.describe Resolvers::ProjectsResolver do
is_expected.to contain_exactly(project)
end
end
context 'when sort is similarity' do
let_it_be(:named_project1) { create(:project, :public, name: 'projAB', path: 'projAB') }
let_it_be(:named_project2) { create(:project, :public, name: 'projABC', path: 'projABC') }
let_it_be(:named_project3) { create(:project, :public, name: 'projA', path: 'projA') }
let(:filters) { { search: 'projA', sort: 'similarity' } }
it 'returns projects in order of similarity to search' do
stub_feature_flags(project_finder_similarity_sort: true)
is_expected.to eq([named_project3, named_project1, named_project2])
end
it 'returns projects not in order of similarity to search if flag is off' do
is_expected.not_to eq([named_project3, named_project1, named_project2])
end
end
end
end
end
......
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