Skip to content

Commit 061829d

Browse files
authored
Merge pull request #2307 from broadinstitute/jb-data-types-search
Adding search options for data types (SCP-6051)
2 parents 1301918 + 3b206a1 commit 061829d

File tree

12 files changed

+156
-18
lines changed

12 files changed

+156
-18
lines changed

app/controllers/api/v1/search_controller.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,14 @@ def index
282282
sort_type = params[:order].to_sym
283283
end
284284

285+
result_types = params[:data_types]&.split(',')&.map(&:to_sym) || []
286+
@studies = StudySearchService.filter_results_by_data_type(@studies, result_types)
287+
285288
# convert to array to allow appending external search results (Azul, TDR, etc.)
286289
@studies = @studies.to_a
287290

291+
# filter results by file type, if requested
292+
288293
# perform Azul search if there are facets/terms provided by user, and they requested HCA results
289294
# run this before inferred search so that they are weighted and sorted correctly
290295
# skip if user is searching inside a collection or they are performing global gene search
@@ -391,6 +396,7 @@ def index
391396
# only show results where we found a hit in gene search
392397
@inferred_studies = Study.where(:id.in => new_genes[:study_ids])
393398
end
399+
@inferred_studies = StudySearchService.filter_results_by_data_type(@inferred_studies, result_types)
394400
@inferred_accessions = @inferred_studies.pluck(:accession)
395401
logger.info "Found #{@inferred_accessions.count} inferred matches: #{@inferred_accessions}"
396402
@matching_accessions += @inferred_accessions
@@ -399,7 +405,6 @@ def index
399405
end
400406

401407
@matching_accessions = @studies.map { |study| self.class.get_study_attribute(study, :accession) }
402-
logger.info "studies_by_facet: #{@studies_by_facet}"
403408
logger.info "Final list of matching studies: #{@matching_accessions}"
404409
@results = @studies.paginate(page: params[:page], per_page: Study.per_page)
405410
if params[:export].present?

app/javascript/components/search/controls/OptionsButton.jsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,24 @@ export default function OptionsButton() {
1010
const searchContext = useContext(StudySearchContext)
1111
const [showOptions, setShowOptions] = useState(false)
1212
const configuredOptions = [
13-
{ searchProp: 'external', value: 'hca', label: 'Include HCA results' }
13+
{ searchProp: 'external', value: 'hca', label: 'Include HCA results' },
14+
{ searchProp: 'data_types', value: 'raw_counts', label: 'Has raw counts', multiple: true },
15+
{ searchProp: 'data_types', value: 'diff_exp', label: 'Has differential expression', multiple: true },
16+
{ searchProp: 'data_types', value: 'spatial', label: 'Has spatial data', multiple: true }
1417
]
1518

1619
const optionsPopover = <Popover data-analytics-name='search-options-menu' id='search-options-menu'>
1720
<ul className="facet-filter-list">
1821
{
19-
configuredOptions.map((option) => {
22+
configuredOptions.map((option, index) => {
2023
return <OptionsControl
21-
key={option.searchProp}
24+
key={`${option.searchProp}-${index}`}
2225
searchContext={searchContext}
2326
searchProp={option.searchProp}
2427
value={option.value}
25-
label={option.label}/>
28+
label={option.label}
29+
multiple={option.multiple}
30+
/>
2631
})
2732
}
2833
</ul>

app/javascript/components/search/controls/OptionsControl.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import React, { useState } from 'react'
22

3-
export default function OptionsControl({searchContext, searchProp, value, label}) {
3+
/** checkbox control for adding optional parameters to search query */
4+
export default function OptionsControl({ searchContext, searchProp, value, label, multiple = false }) {
45
const defaultChecked = searchContext.params[searchProp] === value
56
const [isChecked, setIsChecked] = useState(defaultChecked)
67

8+
79
/** toggle state of checkbox */
810
function toggleCheckbox(checked) {
911
setIsChecked(checked)
10-
searchContext.updateSearch({ [searchProp] : checked ? value : null })
12+
if (multiple) {
13+
const existingOpts = searchContext.params[searchProp]?.split(',').filter(o => o !== '') || []
14+
const newOpts = checked ? existingOpts.concat(value) : existingOpts.filter(v => v !== value)
15+
searchContext.updateSearch({ [searchProp] : newOpts.join(',') })
16+
} else {
17+
searchContext.updateSearch({ [searchProp] : checked ? value : null })
18+
}
1119
}
1220

1321
return (

app/javascript/lib/scp-api.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ export async function fetchSearch(type, searchParams, mock=false) {
926926
export function buildSearchQueryString(type, searchParams) {
927927
const facetsParam = buildFacetQueryString(searchParams.facets)
928928

929-
const params = ['page', 'order', 'terms', 'external', 'export', 'preset', 'genes', 'genePage']
929+
const params = ['page', 'order', 'terms', 'external', 'export', 'data_types', 'preset', 'genes', 'genePage']
930930
let otherParamString = params.map(param => {
931931
return searchParams[param] ? `&${param}=${searchParams[param]}` : ''
932932
}).join('')

app/javascript/providers/GeneSearchProvider.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export function buildParamsFromQuery(query, preset) {
128128
genes: cleanGeneParams,
129129
terms: queryParams.terms ? queryParams.terms : '',
130130
external: queryParams.external ? queryParams.external : '',
131+
data_types: queryParams.data_types ? queryParams.data_types : '',
131132
facets: buildFacetsFromQueryString(queryParams.facets),
132133
preset: preset ? preset : queryString.preset_search
133134
}

app/javascript/providers/SearchSelectionProvider.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default function SearchSelectionProvider(props) {
4343
const [selection, setSelection] = useState(
4444
appliedSelection ?
4545
appliedSelection :
46-
{ terms: '', facets: {}, external: '' })
46+
{ terms: '', facets: {}, external: '', data_types: '' })
4747
selection.updateSelection = updateSelection
4848
selection.updateFacet = updateFacet
4949
selection.performSearch = performSearch

app/javascript/providers/StudySearchProvider.jsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const emptySearch = {
1818
terms: '',
1919
facets: {},
2020
external: '',
21+
data_types: '',
2122
page: 1,
2223
preset_search: undefined,
2324
order: undefined
@@ -75,12 +76,12 @@ export function useContextStudySearch() {
7576
return useContext(StudySearchContext)
7677
}
7778

78-
/** Merges the external parameter into the searchParams object */
79-
export function mergeExternalParam(searchParams, newParams) {
80-
if (Object.keys(newParams).length === 1 && Object.keys(newParams)[0] === 'external') {
81-
return newParams.external
79+
/** Merges any optional parameter into the searchParams object */
80+
export function mergeOptionalParam(searchParams, newParams, paramName) {
81+
if (Object.keys(newParams).length === 1 && Object.keys(newParams)[0] === paramName) {
82+
return newParams[paramName]
8283
} else {
83-
return searchParams.external
84+
return searchParams[paramName]
8485
}
8586
}
8687

@@ -107,7 +108,8 @@ export function PropsStudySearchProvider(props) {
107108
// reset the page to 1 for new searches, unless otherwise specified
108109
search.page = newParams.page ? newParams.page : 1
109110
search.preset = undefined // for now, exclude preset from the page URL--it's in the component props instead
110-
search.external = mergeExternalParam(searchParams, newParams)
111+
search.external = mergeOptionalParam(searchParams, newParams, 'external')
112+
search.data_types = mergeOptionalParam(searchParams, newParams, 'data_types')
111113
const mergedParams = Object.assign(buildGeneParamsFromQuery(window.location.search), search)
112114
const queryString = buildSearchQueryString('study', mergedParams)
113115
navigate(`?${queryString}`)
@@ -165,6 +167,7 @@ export function buildParamsFromQuery(query, preset) {
165167
terms: queryParams.terms ? queryParams.terms : '',
166168
facets: buildFacetsFromQueryString(queryParams.facets),
167169
external: queryParams.external ? queryParams.external : '',
170+
data_types: queryParams.data_types ? queryParams.data_types : '',
168171
preset: preset ? preset : queryString.preset_search,
169172
order: queryParams.order
170173
}

app/lib/study_search_service.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,30 @@ def self.perform_mongo_facet_search(facets)
199199
end
200200
end
201201

202+
def self.filter_results_by_data_type(studies, data_types)
203+
return studies if data_types.empty?
204+
205+
matches = data_types.index_with { [] }
206+
# note: this has to be updated if a new data_type is added
207+
matchers = {
208+
raw_counts: :has_raw_counts_matrices?,
209+
diff_exp: :has_differential_expression_results?,
210+
spatial: :has_spatial_clustering?
211+
}
212+
213+
# run matching in parallel to reduce UI blocking
214+
Parallel.map(studies, in_threads: 10) do |study|
215+
data_types.each do |data_type|
216+
matches[data_type] << study.accession if study.send(matchers[data_type])
217+
end
218+
end
219+
220+
# find the intersection of all matches by data types
221+
study_matches = matches.values
222+
accessions = study_matches[0].intersection(*study_matches[1..])
223+
studies.where(:accession.in => accessions)
224+
end
225+
202226
# deal with ontology id formatting inconsistencies
203227
def self.convert_id_format(id)
204228
parts = id.split(/[_:]/)

app/models/hca_azul_client.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ class HcaAzulClient
2323
# Default headers for API requests
2424
DEFAULT_HEADERS = {
2525
'Accept' => 'application/json',
26-
'Content-Type' => 'application/json',
2726
'x-app-id' => 'single-cell-portal',
2827
'x-domain-id' => "#{ENV['HOSTNAME']}"
2928
}.freeze
@@ -104,11 +103,23 @@ def process_api_request(http_method, path, payload: nil, retry_count: 0)
104103
# * *raises*
105104
# - (RestClient::Exception) => if HTTP request fails for any reason
106105
def execute_http_request(http_method, path, payload = nil)
107-
response = RestClient::Request.execute(method: http_method, url: path, payload:, headers: DEFAULT_HEADERS)
106+
response = RestClient::Request.execute(method: http_method, url: path, payload:, headers: set_headers(http_method))
108107
# handle response using helper
109108
handle_response(response)
110109
end
111110

111+
# set HTTP headers based on method
112+
# GET requests do not support the Content-Type header, but all PUT/POST/PATCH requests do
113+
#
114+
# * *params*
115+
# - +http_method+ (String, Symbol) => HTTP method, e.g. :get, :post
116+
#
117+
# * *returns*
118+
# - (Hash) => HTTP headers object
119+
def set_headers(http_method)
120+
http_method.to_sym == :get ? DEFAULT_HEADERS : DEFAULT_HEADERS.merge('Content-Type' => 'application/json')
121+
end
122+
112123
# FROM SCP-4592: Temporarily disable automatic retries while we investigate the rise in 503 errors from Azul
113124
def should_retry?(code)
114125
false

app/models/study.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,14 @@ def has_visualization_matrices?
10621062
end.any?
10631063
end
10641064

1065+
def has_differential_expression_results?
1066+
differential_expression_results.any?
1067+
end
1068+
1069+
def has_spatial_clustering?
1070+
spatial_cluster_groups.any?
1071+
end
1072+
10651073
# check if study has any files that can be streamed from the bucket for visualization
10661074
# this includes BAM, BED, inferCNV Ideogram annotations, Image files, and DE files
10671075
#

0 commit comments

Comments
 (0)