Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement feature flag for geospatial search capabilities #791

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ locally.

## Optional Environment Variables (all ENVs)

- `GEOSPATIAL_SEARCH` toggles the feature flag to enable spatial search.
- `OPENSEARCH_LOG` if `true`, verbosely logs OpenSearch queries.

```text
Expand Down
237 changes: 162 additions & 75 deletions app/graphql/types/query_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,65 +32,128 @@ def record_id(id:, index:)
raise GraphQL::ExecutionError, "Record '#{id}' not found"
end

field :search, SearchType, null: false,
description: 'Search for timdex records' do
argument :searchterm, String, required: false, default_value: nil, description: 'Query all searchable fields'
argument :citation, String, required: false, default_value: nil, description: 'Search by citation information'
argument :contributors, String, required: false, default_value: nil,
description: 'Search by contributor name; e.g., author, editor, etc.'
argument :funding_information, String, required: false, default_value: nil,
description: 'Search by funding information; e.g., funding source, ' \
'award name, etc.'
argument :geodistance, GeodistanceType, required: false, default_value: nil,
description: 'Search within a certain distance of a specific location'
argument :identifiers, String, required: false, default_value: nil,
description: 'Search by unique indentifier; e.g., ISBN, DOI, etc.'
argument :locations, String, required: false, default_value: nil, description: 'Search by locations'
argument :subjects, String, required: false, default_value: nil, description: 'Search by subject terms'
argument :title, String, required: false, default_value: nil, description: 'Search by title'
argument :from, String, required: false, default_value: '0',
description: 'Search result number to begin with (the first result is 0)'
argument :index, String, required: false, default_value: nil,
description: 'It is not recommended to provide an index value unless we have provided ' \
'you with one for your specific use case'

argument :source, String, required: false, default_value: 'All', deprecation_reason: 'Use `sourceFilter`'

# applied filters
argument :content_type_filter, [String], required: false, default_value: nil,
description: 'Filter results by content type. Use the `contentType` ' \
'aggregation for a list of possible values'
argument :contributors_filter, [String], required: false, default_value: nil,
description: 'Filter results by contributor. Use the `contributors` ' \
'aggregation for a list of possible values'
argument :format_filter, [String], required: false, default_value: nil,
description: 'Filter results by format. Use the `format` aggregation for a ' \
'list of possible values'
argument :languages_filter, [String], required: false, default_value: nil,
description: 'Filter results by language. Use the `languages` ' \
'aggregation for a list of possible values'
argument :literary_form_filter, String, required: false, default_value: nil,
description: 'Filter results by fiction or nonfiction'
argument :source_filter, [String], required: false, default_value: nil,
description: 'Filter by source record system. Use the `sources` aggregation ' \
'for a list of possible values'
argument :subjects_filter, [String], required: false, default_value: nil,
description: 'Filter by subject terms. Use the `contentType` aggregation ' \
if Flipflop.enabled? :geospatial_search
field :search, SearchType, null: false,
description: 'Search for timdex records' do
argument :searchterm, String, required: false, default_value: nil, description: 'Query all searchable fields'
argument :citation, String, required: false, default_value: nil, description: 'Search by citation information'
argument :contributors, String, required: false, default_value: nil,
description: 'Search by contributor name; e.g., author, editor, etc.'
argument :funding_information, String, required: false, default_value: nil,
description: 'Search by funding information; e.g., funding source, ' \
'award name, etc.'
argument :geodistance, GeodistanceType, required: false, default_value: nil,
description: 'Search within a certain distance of a specific location'
argument :identifiers, String, required: false, default_value: nil,
description: 'Search by unique indentifier; e.g., ISBN, DOI, etc.'
argument :locations, String, required: false, default_value: nil, description: 'Search by locations'
argument :subjects, String, required: false, default_value: nil, description: 'Search by subject terms'
argument :title, String, required: false, default_value: nil, description: 'Search by title'
argument :from, String, required: false, default_value: '0',
description: 'Search result number to begin with (the first result is 0)'
argument :index, String, required: false, default_value: nil,
description: 'It is not recommended to provide an index value unless we have provided ' \
'you with one for your specific use case'

argument :source, String, required: false, default_value: 'All', deprecation_reason: 'Use `sourceFilter`'

# applied filters
argument :content_type_filter, [String], required: false, default_value: nil,
description: 'Filter results by content type. Use the `contentType` ' \
'aggregation for a list of possible values'
argument :contributors_filter, [String], required: false, default_value: nil,
description: 'Filter results by contributor. Use the `contributors` ' \
'aggregation for a list of possible values'
argument :format_filter, [String], required: false, default_value: nil,
description: 'Filter results by format. Use the `format` aggregation for a ' \
'list of possible values'
argument :languages_filter, [String], required: false, default_value: nil,
description: 'Filter results by language. Use the `languages` ' \
'aggregation for a list of possible values'
argument :literary_form_filter, String, required: false, default_value: nil,
description: 'Filter results by fiction or nonfiction'
argument :source_filter, [String], required: false, default_value: nil,
description: 'Filter by source record system. Use the `sources` aggregation ' \
'for a list of possible values'
argument :subjects_filter, [String], required: false, default_value: nil,
description: 'Filter by subject terms. Use the `contentType` aggregation ' \
'for a list of possible values'
end
else
field :search, SearchType, null: false,
description: 'Search for timdex records' do
argument :searchterm, String, required: false, default_value: nil, description: 'Query all searchable fields'
argument :citation, String, required: false, default_value: nil, description: 'Search by citation information'
argument :contributors, String, required: false, default_value: nil,
description: 'Search by contributor name; e.g., author, editor, etc.'
argument :funding_information, String, required: false, default_value: nil,
description: 'Search by funding information; e.g., funding source, ' \
'award name, etc.'
argument :identifiers, String, required: false, default_value: nil,
description: 'Search by unique indentifier; e.g., ISBN, DOI, etc.'
argument :locations, String, required: false, default_value: nil, description: 'Search by locations'
argument :subjects, String, required: false, default_value: nil, description: 'Search by subject terms'
argument :title, String, required: false, default_value: nil, description: 'Search by title'
argument :from, String, required: false, default_value: '0',
description: 'Search result number to begin with (the first result is 0)'
argument :index, String, required: false, default_value: nil,
description: 'It is not recommended to provide an index value unless we have provided ' \
'you with one for your specific use case'

argument :source, String, required: false, default_value: 'All', deprecation_reason: 'Use `sourceFilter`'

# applied filters
argument :content_type_filter, [String], required: false, default_value: nil,
description: 'Filter results by content type. Use the `contentType` ' \
'aggregation for a list of possible values'
argument :contributors_filter, [String], required: false, default_value: nil,
description: 'Filter results by contributor. Use the `contributors` ' \
'aggregation for a list of possible values'
argument :format_filter, [String], required: false, default_value: nil,
description: 'Filter results by format. Use the `format` aggregation for a ' \
'list of possible values'
argument :languages_filter, [String], required: false, default_value: nil,
description: 'Filter results by language. Use the `languages` ' \
'aggregation for a list of possible values'
argument :literary_form_filter, String, required: false, default_value: nil,
description: 'Filter results by fiction or nonfiction'
argument :source_filter, [String], required: false, default_value: nil,
description: 'Filter by source record system. Use the `sources` aggregation ' \
'for a list of possible values'
argument :subjects_filter, [String], required: false, default_value: nil,
description: 'Filter by subject terms. Use the `contentType` aggregation ' \
'for a list of possible values'
end
end

def search(searchterm:, citation:, contributors:, funding_information:, geodistance:, identifiers:, locations:,
subjects:, title:, index:, source:, from:, **filters)
query = construct_query(searchterm, citation, contributors, funding_information, geodistance, identifiers,
locations, subjects, title, source, filters)
if Flipflop.enabled? :geospatial_search
def search(searchterm:, citation:, contributors:, funding_information:, geodistance:, identifiers:, locations:,
subjects:, title:, index:, source:, from:, **filters)
query = construct_query(searchterm, citation, contributors, funding_information, geodistance, identifiers,
locations, subjects, title, source, filters)

results = Opensearch.new.search(from, query, Timdex::OSClient, highlight_requested?, index)
results = Opensearch.new.search(from, query, Timdex::OSClient, highlight_requested?, index)

response = {}
response[:hits] = results['hits']['total']['value']
response[:records] = inject_hits_fields_into_source(results['hits']['hits'])
response[:aggregations] = collapse_buckets(results['aggregations'])
response
response = {}
response[:hits] = results['hits']['total']['value']
response[:records] = inject_hits_fields_into_source(results['hits']['hits'])
response[:aggregations] = collapse_buckets(results['aggregations'])
response
end
else
def search(searchterm:, citation:, contributors:, funding_information:, identifiers:, locations:,
subjects:, title:, index:, source:, from:, **filters)
query = construct_query(searchterm, citation, contributors, funding_information, identifiers,
locations, subjects, title, source, filters)

results = Opensearch.new.search(from, query, Timdex::OSClient, highlight_requested?, index)

response = {}
response[:hits] = results['hits']['total']['value']
response[:records] = inject_hits_fields_into_source(results['hits']['hits'])
response[:aggregations] = collapse_buckets(results['aggregations'])
response
end
end

def highlight_requested?
Expand All @@ -111,27 +174,51 @@ def inject_hits_fields_into_source(hits)
modded_sources
end

def construct_query(searchterm, citation, contributors, funding_information, geodistance, identifiers, locations,
subjects, title, source, filters)
query = {}
query[:q] = searchterm
query[:citation] = citation
query[:contributors] = contributors
query[:funding_information] = funding_information
query[:geodistance] = geodistance
query[:identifiers] = identifiers
query[:locations] = locations
query[:subjects] = subjects
query[:title] = title
query[:collection_filter] = filters[:collection_filter]
query[:content_format_filter] = filters[:format_filter]
query[:content_type_filter] = filters[:content_type_filter]
query[:contributors_filter] = filters[:contributors_filter]
query[:languages_filter] = filters[:languages_filter]
query[:literary_form_filter] = filters[:literary_form_filter]
query = source_deprecation_handler(query, filters[:source_filter], source)
query[:subjects_filter] = filters[:subjects_filter]
query
if Flipflop.enabled? :geospatial_search
def construct_query(searchterm, citation, contributors, funding_information, geodistance, identifiers, locations,
subjects, title, source, filters)
query = {}
query[:q] = searchterm
query[:citation] = citation
query[:contributors] = contributors
query[:funding_information] = funding_information
query[:geodistance] = geodistance
query[:identifiers] = identifiers
query[:locations] = locations
query[:subjects] = subjects
query[:title] = title
query[:collection_filter] = filters[:collection_filter]
query[:content_format_filter] = filters[:format_filter]
query[:content_type_filter] = filters[:content_type_filter]
query[:contributors_filter] = filters[:contributors_filter]
query[:languages_filter] = filters[:languages_filter]
query[:literary_form_filter] = filters[:literary_form_filter]
query = source_deprecation_handler(query, filters[:source_filter], source)
query[:subjects_filter] = filters[:subjects_filter]
query
end
else
def construct_query(searchterm, citation, contributors, funding_information, identifiers, locations,
subjects, title, source, filters)
query = {}
query[:q] = searchterm
query[:citation] = citation
query[:contributors] = contributors
query[:funding_information] = funding_information
query[:identifiers] = identifiers
query[:locations] = locations
query[:subjects] = subjects
query[:title] = title
query[:collection_filter] = filters[:collection_filter]
query[:content_format_filter] = filters[:format_filter]
query[:content_type_filter] = filters[:content_type_filter]
query[:contributors_filter] = filters[:contributors_filter]
query[:languages_filter] = filters[:languages_filter]
query[:literary_form_filter] = filters[:literary_form_filter]
query = source_deprecation_handler(query, filters[:source_filter], source)
query[:subjects_filter] = filters[:subjects_filter]
query
end
end

# source_deprecation_handler prefers our new `sourceFilter` array but will fall back on the
Expand Down
4 changes: 4 additions & 0 deletions config/features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
# Strategies will be used in the order listed here.
strategy :session
strategy :default

feature :geospatial_search,
default: ActiveModel::Type::Boolean.new.cast(ENV.fetch('GEOSPATIAL_SEARCH', false)),
description: 'Includes geospatial search capabilities'
end
8 changes: 8 additions & 0 deletions test/controllers/graphql_controller_v2_test.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
require 'test_helper'

class GraphqlControllerV2Test < ActionDispatch::IntegrationTest
def enable_geospatial
test_strategy = Flipflop::FeatureSet.current.test!
test_strategy.switch!(:geospatial_search, true)
end

def setup
test_strategy = Flipflop::FeatureSet.current.test!
test_strategy.switch!(:v2, true)
Expand Down Expand Up @@ -184,6 +189,7 @@ def setup
end

test 'graphqlv2 geodistance search returns results' do
enable_geospatial
VCR.use_cassette('graphqlv2 geodistance') do
post '/graphql', params: { query: '{
search(geodistance: {
Expand All @@ -206,6 +212,7 @@ def setup
end

test 'graphqlv2 geodistance search fails without three required arguments' do
enable_geospatial
post '/graphql', params: { query: '{
search(geodistance: {
latitude: 42.3596653,
Expand All @@ -228,6 +235,7 @@ def setup
end

test 'graphqlv2 geodistance search with another argument' do
enable_geospatial
VCR.use_cassette('graphqlv2 geodistance with searchterm') do
post '/graphql', params: { query: '{
search(
Expand Down
Loading
Loading