From ae73a6b507305981f77f73348ccfe494a2888686 Mon Sep 17 00:00:00 2001 From: Jeremy Prevost Date: Tue, 16 Dec 2025 14:13:55 -0500 Subject: [PATCH] Allows searching the fulltext field when requested Why are these changes being introduced: * We are introducing a fulltext field in our OpenSearch index to allow searching the full text of documents. This change allows users to include the fulltext field in their search if they want. The default is false to support existing behavior (i.e. this is opt-in). Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/TIMX-588 How does this address that need: * Moves fields to search into its own method so that it can conditionally include the fulltext field based on a parameter passed to the search method. * Updates GraphQL query_type to accept a fulltext parameter (defaulting to false) and pass that to the OpenSearch search method. --- app/graphql/types/query_type.rb | 6 ++++-- app/models/opensearch.rb | 22 ++++++++++++++++++---- test/models/opensearch_test.rb | 15 +++++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 573844c..3950b8d 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -54,6 +54,8 @@ def record_id(id:, index:) 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 :fulltext, Boolean, required: false, default_value: false, + description: 'Include fulltext field in search? Defaults to false.' 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' @@ -99,11 +101,11 @@ def record_id(id:, index:) end def search(searchterm:, citation:, contributors:, funding_information:, geodistance:, geobox:, identifiers:, - locations:, subjects:, title:, index:, source:, from:, boolean_type:, **filters) + locations:, subjects:, title:, index:, source:, from:, boolean_type:, fulltext:, **filters) query = construct_query(searchterm, citation, contributors, funding_information, geodistance, geobox, identifiers, locations, subjects, title, source, boolean_type, filters) - results = Opensearch.new.search(from, query, Timdex::OSClient, highlight_requested?, index) + results = Opensearch.new.search(from, query, Timdex::OSClient, highlight_requested?, index, fulltext) response = {} response[:hits] = results['hits']['total']['value'] diff --git a/app/models/opensearch.rb b/app/models/opensearch.rb index 996def4..6aeb27f 100644 --- a/app/models/opensearch.rb +++ b/app/models/opensearch.rb @@ -4,14 +4,20 @@ class Opensearch SIZE = 20 MAX_PAGE = 200 - def search(from, params, client, highlight = false, index = nil) + def search(from, params, client, highlight = false, index = nil, fulltext = false) @params = params @highlight = highlight + @fulltext = fulltext?(fulltext) index = default_index unless index.present? client.search(index:, body: build_query(from)) end + # Only treat fulltext as true if it is boolean true or the string 'true' (case insensitive) + def fulltext?(fulltext_param) + fulltext_param == true || fulltext_param.to_s.downcase == 'true' + end + def default_index ENV.fetch('OPENSEARCH_INDEX', nil) end @@ -132,15 +138,23 @@ def minimum_should_match end end + # Fields to be searched in multi_match query. Adds 'fulltext' field if fulltext search is enabled. + def fields_to_search + fields = ['alternate_titles', 'call_numbers', 'citation', 'contents', 'contributors.value', 'dates.value', + 'edition', 'funding_information.*', 'identifiers.value', 'languages', 'locations.value', + 'notes.value', 'numbering', 'publication_information', 'subjects.value', 'summary', 'title'] + fields << 'fulltext' if @fulltext + + fields + end + def matches m = [] if @params[:q].present? m << { multi_match: { query: @params[:q].downcase, - fields: ['alternate_titles', 'call_numbers', 'citation', 'contents', 'contributors.value', 'dates.value', - 'edition', 'funding_information.*', 'identifiers.value', 'languages', 'locations.value', - 'notes.value', 'numbering', 'publication_information', 'subjects.value', 'summary', 'title'], + fields: fields_to_search, minimum_should_match: } } diff --git a/test/models/opensearch_test.rb b/test/models/opensearch_test.rb index f5173ef..7f4b89f 100644 --- a/test/models/opensearch_test.rb +++ b/test/models/opensearch_test.rb @@ -86,6 +86,21 @@ class OpensearchTest < ActiveSupport::TestCase end end + test 'fulltext is included when requested' do + os = Opensearch.new + os.instance_variable_set(:@params, { q: 'this' }) + os.instance_variable_set(:@fulltext, true) + + assert(os.matches.to_json.include?('"fields":["alternate_titles","call_numbers","citation","contents","contributors.value","dates.value","edition","funding_information.*","identifiers.value","languages","locations.value","notes.value","numbering","publication_information","subjects.value","summary","title","fulltext"]')) + end + + test 'fulltext is not included by default' do + os = Opensearch.new + os.instance_variable_set(:@params, { q: 'this' }) + + assert(os.matches.to_json.include?('"fields":["alternate_titles","call_numbers","citation","contents","contributors.value","dates.value","edition","funding_information.*","identifiers.value","languages","locations.value","notes.value","numbering","publication_information","subjects.value","summary","title"]')) + end + test 'searches a single field' do VCR.use_cassette('opensearch single field') do params = { title: 'spice it up' }