• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pulibrary / orangelight / 78cb5ae0-be36-44e7-901d-e0a372c29ca0

01 Apr 2025 07:43PM UTC coverage: 95.448% (-0.02%) from 95.472%
78cb5ae0-be36-44e7-901d-e0a372c29ca0

Pull #4850

circleci

maxkadel
Mark tests not ready for CI as pending with xdescribe

Co-authored-by: Jane Sandberg <sandbergja@users.noreply.github.com>
Pull Request #4850: I4806 advanced boolean behavior

1 of 1 new or added line in 1 file covered. (100.0%)

3 existing lines in 1 file now uncovered.

6039 of 6327 relevant lines covered (95.45%)

1515.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.44
/app/models/search_builder.rb
1
# frozen_string_literal: true
2

3
class SearchBuilder < Blacklight::SearchBuilder
3✔
4
  include Blacklight::Solr::SearchBuilderBehavior
3✔
5
  include BlacklightRangeLimit::RangeLimitBuilder
3✔
6
  include BlacklightHelper
3✔
7

8
  default_processor_chain.unshift(:conditionally_configure_json_query_dsl)
3✔
9

10
  self.default_processor_chain += %i[parslet_trick cleanup_boolean_operators
3✔
11
                                     cjk_mm wildcard_char_strip
12
                                     only_home_facets prepare_left_anchor_search fancy_booleans
13
                                     series_title_results pul_holdings html_facets
14
                                     numismatics_facets numismatics_advanced
15
                                     adjust_mm remove_unneeded_facets]
16

17
  # mutate the solr_parameters to remove words that are
18
  # boolean operators, but not intended as such.
19
  def cleanup_boolean_operators(solr_parameters)
3✔
20
    transform_queries!(solr_parameters) { |query| cleaned_query(query) }
385✔
21
  end
22

23
  # left_anchor_search
24
  # duplication of logic with other booleans
25
  # SO LONG
26
  # Process-y, not OO
27
  # failing tests
28
  # tests that don't run in CI
29
  def fancy_booleans(solr_parameters)
3✔
30
    # Find phrase with OR
31
    phrases_with_or = solr_parameters.dig('json', 'query', 'bool', 'must')&.select do |clause|
258✔
32
      # still need to deal with ALL CAPS QUERIES WITH OR IN THE MIDDLE
33
      clause[:edismax][:query].include?('OR')
99✔
34
    end
35
    # take phrase with OR out of "must" array
36
    phrases_with_or&.each do |phrase|
258✔
37
      solr_parameters['json']['query']['bool']['must'].delete(phrase)
7✔
38
    end
39
    # split into individual phrases for each part of "should" and put in "should" array
40
    should_array = []
258✔
41
    phrases_with_or&.map do |phrase|
258✔
42
      sub_phrases = phrase[:edismax][:query].split(' OR ')
7✔
43
      sub_phrases.each do |sub_phrase|
7✔
44
        should_array << { edismax: { query: sub_phrase } }
11✔
45
      end
46
    end
47
    # create "should" clause
48
    # What if there's already a should clause from another field?
49
    solr_parameters['json']['query']['bool']['should'] = should_array if should_array.present?
258✔
50

51
    return unless solr_parameters.dig('json', 'query', 'bool', 'must') && solr_parameters.dig('json', 'query', 'bool', 'must').empty?
258✔
52
    solr_parameters['json']['query']['bool'].delete('must')
6✔
53
  end
54

55
  # Blacklight uses Parslet https://rubygems.org/gems/parslet/versions/2.0.0 to parse the user query
56
  # and unfortunately Parslet gets confused when the user's query ends with "()". Here we tweak the
57
  # query to prevent the error and let the query to be parsed as if the ending "()" was not present.
58
  # Notice that we must update the value in `blacklight_params[:q]`
59
  def parslet_trick(_solr_parameters)
3✔
60
    return unless blacklight_params[:q].is_a?(String)
254✔
61
    return unless blacklight_params[:q].strip.end_with?("()")
73✔
62
    blacklight_params[:q] = blacklight_params[:q].strip.gsub("()", "")
×
63
  end
64

65
  # Only search for coin records when querying with the numismatics advanced search
66
  def numismatics_advanced(solr_parameters)
3✔
67
    return unless blacklight_params[:advanced_type] == 'numismatics'
254✔
68
    solr_parameters[:fq] ||= []
5✔
69
    solr_parameters[:fq] << "format:Coin"
5✔
70
  end
71

72
  def numismatics_facets(solr_parameters)
3✔
73
    return unless blacklight_params[:action] == 'numismatics'
254✔
74
    blacklight_config.advanced_search[:form_solr_parameters]['facet.field'] = blacklight_config.numismatics_search['facet_fields']
9✔
75
    solr_parameters['facet.field'] = blacklight_config.numismatics_search['facet_fields']
9✔
76
  end
77

78
  def facets_for_advanced_search_form(solr_p)
3✔
79
    # Reject any facets that are meant to display on the advanced
80
    # search form, so that the form displays accurate counts for
81
    # them in its dropdowns
82
    advanced_search_facets = blacklight_config.advanced_search.form_solr_parameters['facet.field']
10✔
83
    solr_p[:fq]&.compact!
10✔
84
    solr_p[:fq]&.reject! do |facet_from_query|
10✔
85
      advanced_search_facets.any? { |facet_to_exclude| facet_from_query.include? facet_to_exclude }
29✔
86
    end
87
  end
88

89
  def only_home_facets(solr_parameters)
3✔
90
    return if search_parameters? || advanced_search?
258✔
91
    solr_parameters['facet.field'] = blacklight_config.facet_fields.select { |_, v| v[:home] }.keys
5,372✔
92
    solr_parameters['facet.pivot'] = []
60✔
93
  end
94

95
  ##
96
  # Check if we are on an advanced search page
97
  # @return [Boolean]
98
  def advanced_search?
3✔
99
    blacklight_params[:advanced_type] == 'advanced' ||
116✔
100
      search_state.controller.try(:params).try(:[], :action) == 'advanced_search' ||
101
      blacklight_params[:advanced_type] == 'numismatics'
102
  end
103

104
  ##
105
  # Check if any search parameters have been set
106
  # @return [Boolean]
107
  def search_parameters?
3✔
108
    search_query_present? || facet_query_present?
258✔
109
  end
110

111
  def conditionally_configure_json_query_dsl(_solr_parameters)
3✔
112
    advanced_fields = %w[all_fields title author subject left_anchor publisher in_series notes series_title isbn issn]
256✔
113
    add_edismax(advanced_fields:)
256✔
114
  end
115

116
  def adjust_mm(solr_parameters)
3✔
117
    # If the user is attempting a boolean OR query,
118
    # for example: activism OR "social justice"
119
    # don't want to cancel out the boolean OR with
120
    # an mm configuration that requires all the clauses
121
    # to be in the document
122
    return unless blacklight_params[:q].to_s.split.include? 'OR'
257✔
123
    solr_parameters['mm'] = 0
3✔
124
  end
125

126
  def includes_written_boolean?
3✔
UNCOV
127
    if advanced_search? && search_query_present?
×
UNCOV
128
      json_query_dsl_clauses&.any? { |clause| clause&.dig('query')&.include?('OR') }
×
129
    else
UNCOV
130
      blacklight_params[:q].to_s.split.include? 'OR'
×
131
    end
132
  end
133

134
  # When the user is viewing the values of a specific facet
135
  # by clicking the "more" link in a facet, solr doesn't
136
  # need to perform expensive calculations related to other
137
  # facets that the user is not displaying
138
  # :reek:FeatureEnvy
139
  def remove_unneeded_facets(solr_parameters)
3✔
140
    return unless facet
261✔
141
    remove_unneeded_stats(solr_parameters)
6✔
142
    solr_parameters.delete('facet.pivot') unless solr_parameters['facet.pivot']&.split(',')&.include? facet
6✔
143
    solr_parameters.delete('facet.query') unless solr_parameters['facet.query']&.any? { |query| query.partition(':').first == facet }
8✔
144
  end
145

146
  def wildcard_char_strip(solr_parameters)
3✔
147
    transform_queries!(solr_parameters) { |query| query.delete('?') }
379✔
148
  end
149

150
  private
3✔
151

152
    def search_query_present?
3✔
153
      !blacklight_params[:q].nil? || json_query_dsl_clauses&.any? { |clause| clause.dig('query')&.present? }
321✔
154
    end
155

156
    def facet_query_present?
3✔
157
      blacklight_params[:f].present? || blacklight_params[:action] == 'facet'
133✔
158
    end
159

160
    def json_query_dsl_clauses
3✔
161
      blacklight_params.dig('clause')&.values
174✔
162
    end
163

164
    def q_param_needs_boolean_cleanup(solr_parameters)
3✔
165
      solr_parameters[:q].present? &&
×
166
        cleaned_query(solr_parameters[:q]) == solr_parameters[:q]
167
    end
168

169
    def add_edismax(advanced_fields:)
3✔
170
      advanced_fields.each do |field|
256✔
171
        solr_params = blacklight_config.search_fields[field]['solr_parameters']
2,816✔
172
        edismax = solr_params.present? ? solr_params.dup : {}
2,816✔
173
        blacklight_config.search_fields[field]['clause_params'] = { edismax: }
2,816✔
174
      end
175
    end
176

177
    # :reek:FeatureEnvy
178
    def remove_unneeded_stats(solr_parameters)
3✔
179
      return if solr_parameters['stats.field'].to_a.include? facet
6✔
180
      solr_parameters.delete('stats')
5✔
181
      solr_parameters.delete('stats.field')
5✔
182
    end
183

184
    # :reek:DuplicateMethodCall
185
    # :reek:MissingSafeMethod
186
    # :reek:UtilityFunction
187
    def transform_queries!(solr_parameters)
3✔
188
      solr_parameters[:q] = yield solr_parameters[:q] if solr_parameters[:q]
515✔
189
      solr_parameters.dig('json', 'query', 'bool', 'must')&.map! do |search_element|
515✔
190
        search_element[:edismax][:query] = yield search_element[:edismax][:query]
193✔
191
        search_element
193✔
192
      end
193
    end
194

195
    def cleaned_query(query)
3✔
196
      return query if query.nil?
126✔
197
      query.gsub(/([A-Z]) (NOT|OR|AND) ([A-Z])/) do
126✔
198
        "#{Regexp.last_match(1)} #{Regexp.last_match(2).downcase} #{Regexp.last_match(3)}"
3✔
199
      end
200
    end
201
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc