Module: SearchHelper
- Defined in:
- app/helpers/search_helper.rb
Instance Method Summary collapse
-
#active_filter ⇒ Hash{Symbol => #to_s}
Provides a filter-like object containing keys for each of the filter parameters.
- #check_posts_permissions ⇒ Object
-
#date_value_sql(value) ⇒ Array(String, String, String)
private
Parses a qualifier value string, including operator, as a date value.
-
#filter_to_qualifiers(filter) ⇒ Array<Hash{Symbol => Object}>
Converts a Filter record into a form parseable by the search function.
-
#numeric_value_sql(value) ⇒ Array(String, String)
private
Parses a qualifier value string, including operator, as a numeric value.
-
#params_to_qualifiers ⇒ Array<Hash{Symbol => Object}>
Retrieves parameters from
params
, validates their values, and adds them to a qualifiers hash. -
#parse_qualifier_strings(qualifiers) ⇒ Array<Hash{Symbol => Object}>
Parses a full qualifier string into an array of qualifier objects.
-
#parse_search(raw_search) ⇒ Hash{Symbol => String}
Parses a raw search string and returns the base search term and qualifier strings separately.
-
#qualifiers_to_sql(qualifiers, query) ⇒ ActiveRecord::Relation
Parses a qualifiers hash and applies it to an ActiveRecord query.
-
#search_posts ⇒ ActiveRecord::Relation<Post>
Search & sort a default posts list based on parameters in the current request.
Instance Method Details
#active_filter ⇒ Hash{Symbol => #to_s}
Provides a filter-like object containing keys for each of the filter parameters.
66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'app/helpers/search_helper.rb', line 66 def active_filter { default: nil, name: params[:predefined_filter], min_score: params[:min_score], max_score: params[:max_score], min_answers: params[:min_answers], max_answers: params[:max_answers], include_tags: params[:include_tags], exclude_tags: params[:exclude_tags], status: params[:status] } end |
#check_posts_permissions ⇒ Object
2 3 4 5 |
# File 'app/helpers/search_helper.rb', line 2 def (current_user&.is_moderator || current_user&.is_admin ? Post : Post.undeleted) .qa_only.list_includes end |
#date_value_sql(value) ⇒ Array(String, String, String)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Parses a qualifier value string, including operator, as a date value.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'app/helpers/search_helper.rb', line 300 def date_value_sql(value) operator = '' while ['<', '>', '='].include? value[0] operator += value[0] value = value[1..-1] end # working with dates: <1y ('less than one year ago') is SQL: > 1y ago operator = { '<' => '>', '>' => '<', '<=' => '>=', '>=' => '<=' }[operator] || '' val = '' while value[0] =~ /[[:digit:]]/ val += value[0] value = value[1..-1] end timeframe = { s: 'SECOND', m: 'MINUTE', h: 'HOUR', d: 'DAY', w: 'WEEK', mo: 'MONTH', y: 'YEAR' }[value.to_sym] [operator, val, timeframe || 'MONTH'] end |
#filter_to_qualifiers(filter) ⇒ Array<Hash{Symbol => Object}>
Converts a Filter record into a form parseable by the search function.
51 52 53 54 55 56 57 58 59 60 61 |
# File 'app/helpers/search_helper.rb', line 51 def filter_to_qualifiers(filter) qualifiers = [] qualifiers.append({ param: :score, operator: '>=', value: filter.min_score }) unless filter.min_score.nil? qualifiers.append({ param: :score, operator: '<=', value: filter.max_score }) unless filter.max_score.nil? qualifiers.append({ param: :answers, operator: '>=', value: filter.min_answers }) unless filter.min_answers.nil? qualifiers.append({ param: :answers, operator: '<=', value: filter.max_answers }) unless filter.max_answers.nil? qualifiers.append({ param: :include_tags, tag_ids: filter. }) unless filter..nil? qualifiers.append({ param: :exclude_tags, tag_ids: filter. }) unless filter..nil? qualifiers.append({ param: :status, value: filter.status }) unless filter.status.nil? qualifiers end |
#numeric_value_sql(value) ⇒ Array(String, String)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Parses a qualifier value string, including operator, as a numeric value.
283 284 285 286 287 288 289 290 291 292 293 |
# File 'app/helpers/search_helper.rb', line 283 def numeric_value_sql(value) operator = '' while ['<', '>', '='].include? value[0] operator += value[0] value = value[1..-1] end # whatever's left after stripping operator is the number # validated by regex in qualifiers_to_sql [operator, value] end |
#params_to_qualifiers ⇒ Array<Hash{Symbol => Object}>
Retrieves parameters from params
, validates their values, and adds them to a qualifiers hash.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'app/helpers/search_helper.rb', line 83 def params_to_qualifiers valid_value = { date: /^[\d.]+(?:s|m|h|d|w|mo|y)?$/, status: /any|open|closed/, numeric: /^[\d.]+$/, integer: /^\d+$/ } filter_qualifiers = [] if params[:min_score]&.match?(valid_value[:numeric]) filter_qualifiers.append({ param: :score, operator: '>=', value: params[:min_score].to_f }) end if params[:max_score]&.match?(valid_value[:numeric]) filter_qualifiers.append({ param: :score, operator: '<=', value: params[:max_score].to_f }) end if params[:min_answers]&.match?(valid_value[:numeric]) filter_qualifiers.append({ param: :answers, operator: '>=', value: params[:min_answers].to_i }) end if params[:max_answers]&.match?(valid_value[:numeric]) filter_qualifiers.append({ param: :answers, operator: '<=', value: params[:max_answers].to_i }) end if params[:status]&.match?(valid_value[:status]) filter_qualifiers.append({ param: :status, value: params[:status] }) end if params[:include_tags]&.all? { |id| id.match? valid_value[:integer] } filter_qualifiers.append({ param: :include_tags, tag_ids: params[:include_tags] }) end if params[:exclude_tags]&.all? { |id| id.match? valid_value[:integer] } filter_qualifiers.append({ param: :exclude_tags, tag_ids: params[:exclude_tags] }) end filter_qualifiers end |
#parse_qualifier_strings(qualifiers) ⇒ Array<Hash{Symbol => Object}>
Parses a full qualifier string into an array of qualifier objects.
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'app/helpers/search_helper.rb', line 144 def parse_qualifier_strings(qualifiers) valid_value = { date: /^[<>=]{0,2}[\d.]+(?:s|m|h|d|w|mo|y)?$/, status: /any|open|closed/, numeric: /^[<>=]{0,2}[\d.]+$/ } qualifiers.map do |qualifier| # rubocop:disable Metrics/BlockLength splat = qualifier.split ':' parameter = splat[0] value = splat[1] case parameter when 'score' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value { param: :score, operator: operator.presence || '=', value: val.to_f } when 'created' next unless value.match?(valid_value[:date]) operator, val, timeframe = date_value_sql value { param: :created, operator: operator.presence || '=', timeframe: timeframe, value: val.to_i } when 'user' operator, val = if value.match?(valid_value[:numeric]) numeric_value_sql value elsif value == 'me' ['=', current_user&.id&.to_i] else next end { param: :user, operator: operator.presence || '=', user_id: val } when 'upvotes' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value { param: :upvotes, operator: operator.presence || '=', value: val.to_i } when 'downvotes' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value { param: :downvotes, operator: operator.presence || '=', value: val.to_i } when 'votes' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value { param: :net_votes, operator: operator.presence || '=', value: val.to_i } when 'tag' { param: :include_tag, tag_id: Tag.where(name: value).select(:id) } when '-tag' { param: :exclude_tag, tag_id: Tag.where(name: value).select(:id) } when 'category' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value { param: :category, operator: operator.presence || '=', category_id: val.to_i } when 'post_type' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value { param: :post_type, operator: operator.presence || '=', post_type_id: val.to_i } when 'answers' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value { param: :answers, operator: operator.presence || '=', value: val.to_i } when 'status' next unless value.match?(valid_value[:status]) { param: :status, value: value } end end.compact # Consider partitioning and telling the user which filters were invalid end |
#parse_search(raw_search) ⇒ Hash{Symbol => String}
Parses a raw search string and returns the base search term and qualifier strings separately.
127 128 129 130 131 132 133 134 135 136 |
# File 'app/helpers/search_helper.rb', line 127 def parse_search(raw_search) qualifiers_regex = /([\w\-_]+(?<!\\):[^ ]+)/ qualifiers = raw_search.scan(qualifiers_regex).flatten search = raw_search qualifiers.each do |q| search = search.gsub(q, '') end search = search.gsub(/\\:/, ':').strip { qualifiers: qualifiers, search: search } end |
#qualifiers_to_sql(qualifiers, query) ⇒ ActiveRecord::Relation
Parses a qualifiers hash and applies it to an ActiveRecord query.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'app/helpers/search_helper.rb', line 225 def qualifiers_to_sql(qualifiers, query) trust_level = current_user&.trust_level || 0 allowed_categories = Category.where('IFNULL(min_view_trust_level, -1) <= ?', trust_level) query = query.where(category_id: allowed_categories) qualifiers.each do |qualifier| # rubocop:disable Metrics/BlockLength case qualifier[:param] when :score query = query.where("score #{qualifier[:operator]} ?", qualifier[:value]) when :created query = query.where("created_at #{qualifier[:operator]} DATE_SUB(CURRENT_TIMESTAMP, " \ "INTERVAL ? #{qualifier[:timeframe]})", qualifier[:value]) when :user query = query.where("user_id #{qualifier[:operator]} ?", qualifier[:user_id]) when :upvotes query = query.where("upvote_count #{qualifier[:operator]} ?", qualifier[:value]) when :downvotes query = query.where("downvote_count #{qualifier[:operator]} ?", qualifier[:value]) when :net_votes query = query.where("(upvote_count - downvote_count) #{qualifier[:operator]} ?", qualifier[:value]) when :include_tag query = query.where(posts: { id: PostsTag.where(tag_id: qualifier[:tag_id]).select(:post_id) }) when :include_tags qualifier[:tag_ids].each do |id| query = query.where(id: PostsTag.where(tag_id: id).select(:post_id)) end when :exclude_tag query = query.where.not(posts: { id: PostsTag.where(tag_id: qualifier[:tag_id]).select(:post_id) }) when :exclude_tags query = query.where.not(id: PostsTag.where(tag_id: qualifier[:tag_ids]).select(:post_id)) when :category query = query.where("category_id #{qualifier[:operator]} ?", qualifier[:category_id]) when :post_type query = query.where("post_type_id #{qualifier[:operator]} ?", qualifier[:post_type_id]) when :answers post_types_with_answers = PostType.where(has_answers: true) query = query.where("answer_count #{qualifier[:operator]} ?", qualifier[:value]) .where(post_type_id: post_types_with_answers) when :status case qualifier[:value] when 'open' query = query.where(closed: false) when 'closed' query = query.where(closed: true) end end end query end |
#search_posts ⇒ ActiveRecord::Relation<Post>
Search & sort a default posts list based on parameters in the current request.
Generates initial post list using Post#qa_only, including deleted posts for mods and admins. Takes search string from params[:search]
, applies any qualifiers, and searches post bodies for the remaining term(s).
Search uses MySQL fulltext search in boolean mode which is what provides advanced search syntax (excluding qualifiers) - see MySQL manual 14.9.2.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'app/helpers/search_helper.rb', line 17 def search_posts posts = qualifiers = params_to_qualifiers search_string = params[:search] # Filter based on search string qualifiers if search_string.present? search_data = parse_search(search_string) qualifiers += parse_qualifier_strings search_data[:qualifiers] search_string = search_data[:search] end posts = qualifiers_to_sql(qualifiers, posts) posts = posts.paginate(page: params[:page], per_page: 25) posts = if search_string.present? posts.search(search_data[:search]).user_sort({ term: params[:sort], default: :search_score }, relevance: :search_score, score: :score, age: :created_at, activity: :updated_at) else posts.user_sort({ term: params[:sort], default: :score }, score: :score, age: :created_at, activity: :updated_at) end [posts, qualifiers] end |