diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 1a69ec85d18..8984cef42e9 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -18,6 +18,7 @@ # assignee_id: integer or 'None' or 'Any' # assignee_username: string # search: string +# in: 'title', 'description' or a string joined them with comma # label_name: string # sort: string # non_archived: boolean @@ -56,6 +57,7 @@ class IssuableFinder milestone_title my_reaction_emoji search + in ] end @@ -408,7 +410,7 @@ class IssuableFinder items = klass.with(cte.to_arel).from(klass.table_name) end - items.full_search(search) + items.full_search(search, matched_columns: params[:in]) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 45e494725d7..bf39effa265 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -14,6 +14,7 @@ # milestone_title: string # assignee_id: integer # search: string +# in: 'title', 'description' or a string joined them with comma # label_name: string # sort: string # my_reaction_emoji: string diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index e190d5d90c9..3cfe9533bb6 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -15,6 +15,7 @@ # author_id: integer # assignee_id: integer # search: string +# in: 'title', 'description' or a string joined them with comma # label_name: string # sort: string # non_archived: boolean diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0d363ec68b7..d6893a59b43 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -136,10 +136,18 @@ module Issuable # This method uses ILIKE on PostgreSQL and LIKE on MySQL. # # query - The search query as a String + # matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma. # # Returns an ActiveRecord::Relation. - def full_search(query) - fuzzy_search(query, [:title, :description]) + def full_search(query, matched_columns: 'title,description') + allowed_columns = [:title, :description] + matched_columns = matched_columns.to_s.split(',').map(&:to_sym) + matched_columns &= allowed_columns + + # Matching title or description if the matched_columns did not contain any allowed columns. + matched_columns = [:title, :description] if matched_columns.empty? + + fuzzy_search(query, matched_columns) end def sort_by_attribute(method, excluded_labels: []) diff --git a/changelogs/unreleased/search-title.yml b/changelogs/unreleased/search-title.yml new file mode 100644 index 00000000000..ff0933ed0b2 --- /dev/null +++ b/changelogs/unreleased/search-title.yml @@ -0,0 +1,5 @@ +--- +title: Add 'in' filter that modifies scope of 'search' filter to issues and merge requests API +merge_request: 24350 +author: Hiroyuki Sato +type: added diff --git a/doc/api/issues.md b/doc/api/issues.md index fb06119063f..b86340bb5e7 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -31,6 +31,7 @@ GET /issues?iids[]=42&iids[]=43 GET /issues?author_id=5 GET /issues?assignee_id=5 GET /issues?my_reaction_emoji=star +GET /issues?search=foo&in=title ``` | Attribute | Type | Required | Description | @@ -46,6 +47,7 @@ GET /issues?my_reaction_emoji=star | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search issues against their `title` and `description` | +| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joined them with comma. Default is `title,description` | | `created_after` | datetime | no | Return issues created on or after the given time | | `created_before` | datetime | no | Return issues created on or before the given time | | `updated_after` | datetime | no | Return issues updated on or after the given time | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index c9b271eada3..c351be5bd55 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -24,6 +24,7 @@ GET /merge_requests?labels=bug,reproduced GET /merge_requests?author_id=5 GET /merge_requests?my_reaction_emoji=star GET /merge_requests?scope=assigned_to_me +GET /merge_requests?search=foo&in=title ``` Parameters: @@ -47,6 +48,7 @@ Parameters: | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | | `search` | string | no | Search merge requests against their `title` and `description` | +| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joined them with comma. Default is `title,description` | | `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests | ```json diff --git a/lib/api/issues.rb b/lib/api/issues.rb index dac700482b4..a797f01e89f 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -43,7 +43,8 @@ module API desc: 'Return issues sorted in `asc` or `desc` order.' optional :milestone, type: String, desc: 'Return issues for a specific milestone' optional :iids, type: Array[Integer], desc: 'The IID array of issues' - optional :search, type: String, desc: 'Search issues for text present in the title or description' + optional :search, type: String, desc: 'Search issues for text present in the title, description or any combination of these' + optional :in, type: String, desc: '`title`, `description` or a string joined them with comma' optional :created_after, type: DateTime, desc: 'Return issues created after the specified time' optional :created_before, type: DateTime, desc: 'Return issues created before the specified time' optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time' diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 8c1951cc535..991b7310cfd 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -109,7 +109,8 @@ module API optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' optional :source_branch, type: String, desc: 'Return merge requests with the given source branch' optional :target_branch, type: String, desc: 'Return merge requests with the given target branch' - optional :search, type: String, desc: 'Search merge requests for text present in the title or description' + optional :search, type: String, desc: 'Search merge requests for text present in the title, description or any combination of these' + optional :in, type: String, desc: '`title`, `description` or a string joined them with comma' optional :wip, type: String, values: %w[yes no], desc: 'Search merge requests for WIP in the title' use :pagination end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 682fae06434..34cb09942be 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -314,6 +314,14 @@ describe IssuesFinder do end end + context 'filtering by issue term in title' do + let(:params) { { search: 'git', in: 'title' } } + + it 'returns issues with title match for search term' do + expect(issues).to contain_exactly(issue1) + end + end + context 'filtering by issues iids' do let(:params) { { iids: issue3.iid } } diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index a4bf3e2350a..d7003cdb760 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -139,6 +139,78 @@ describe Issuable do it 'returns issues with a matching description for a query shorter than 3 chars' do expect(issuable_class.full_search(searchable_issue2.description.downcase)).to eq([searchable_issue2]) end + + context 'when mathing columns is "title"' do + it 'returns issues with a matching title' do + expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'title')) + .to eq([searchable_issue]) + end + + it 'returns no issues with a matching description' do + expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'title')) + .to be_empty + end + end + + context 'when mathing columns is "description"' do + it 'returns no issues with a matching title' do + expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'description')) + .to be_empty + end + + it 'returns issues with a matching description' do + expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'description')) + .to eq([searchable_issue]) + end + end + + context 'when mathing columns is "title,description"' do + it 'returns issues with a matching title' do + expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'title,description')) + .to eq([searchable_issue]) + end + + it 'returns issues with a matching description' do + expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'title,description')) + .to eq([searchable_issue]) + end + end + + context 'when mathing columns is nil"' do + it 'returns issues with a matching title' do + expect(issuable_class.full_search(searchable_issue.title, matched_columns: nil)) + .to eq([searchable_issue]) + end + + it 'returns issues with a matching description' do + expect(issuable_class.full_search(searchable_issue.description, matched_columns: nil)) + .to eq([searchable_issue]) + end + end + + context 'when mathing columns is "invalid"' do + it 'returns issues with a matching title' do + expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'invalid')) + .to eq([searchable_issue]) + end + + it 'returns issues with a matching description' do + expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'invalid')) + .to eq([searchable_issue]) + end + end + + context 'when mathing columns is "title,invalid"' do + it 'returns issues with a matching title' do + expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'title,invalid')) + .to eq([searchable_issue]) + end + + it 'returns no issues with a matching description' do + expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'title,invalid')) + .to be_empty + end + end end describe '.to_ability_name' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index ba7930f6c9d..707b91e3b2a 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -208,6 +208,18 @@ describe API::Issues do expect_paginated_array_response(issue.id) end + it 'returns issues matching given search string for title and scoped in title' do + get api("/issues", user), params: { search: issue.title, in: 'title' } + + expect_paginated_array_response(issue.id) + end + + it 'returns an empty array if no issue matches given search string for title and scoped in description' do + get api("/issues", user), params: { search: issue.title, in: 'description' } + + expect_paginated_array_response([]) + end + it 'returns issues matching given search string for description' do get api("/issues", user), params: { search: issue.description } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 79c0a1953dc..ed8a56a2221 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -260,6 +260,18 @@ describe API::MergeRequests do expect_response_ordered_exactly(merge_request) end + it 'returns merge requests matching given search string for title and scoped in title' do + get api("/merge_requests", user), params: { search: merge_request.title, in: 'title' } + + expect_response_ordered_exactly(merge_request) + end + + it 'returns an empty array if no merge reques matches given search string for description and scoped in title' do + get api("/merge_requests", user), params: { search: merge_request.description, in: 'title' } + + expect_response_contain_exactly + end + it 'returns merge requests for project matching given search string for description' do get api("/merge_requests", user), params: { project_id: project.id, search: merge_request.description }