class Pursuit::Search
Attributes
options[R]
@return [SearchOptions] The options to use when building the search query.
Public Class Methods
new(options)
click to toggle source
Create a new instance to search a specific ActiveRecord record class.
@param options [SearchOptions]
# File lib/pursuit/search.rb, line 13 def initialize(options) @options = options end
Public Instance Methods
perform(query)
click to toggle source
Perform a search for the specified query.
@param query [String] The query to transform into a SQL search. @return [ActiveRecord::Relation] The search results.
# File lib/pursuit/search.rb, line 22 def perform(query) options.record_class.where(build_arel(query)) end
Private Instance Methods
build_arel(query)
click to toggle source
# File lib/pursuit/search.rb, line 28 def build_arel(query) parser = SearchTermParser.new(query, keys: options.keys) unkeyed_arel = build_arel_for_unkeyed_term(parser.unkeyed_term) keyed_arel = build_arel_for_keyed_terms(parser.keyed_terms) if unkeyed_arel && keyed_arel unkeyed_arel.and(keyed_arel) else unkeyed_arel || keyed_arel end end
build_arel_for_belongs_to_reflection_join(reflection)
click to toggle source
# File lib/pursuit/search.rb, line 114 def build_arel_for_belongs_to_reflection_join(reflection) reflection_table = reflection.klass.arel_table reflection_table.where( reflection_table[reflection.join_primary_key].eq( options.record_class.arel_table[reflection.join_foreign_key] ) ) end
build_arel_for_has_reflection_join(reflection)
click to toggle source
# File lib/pursuit/search.rb, line 123 def build_arel_for_has_reflection_join(reflection) reflection_table = reflection.klass.arel_table reflection_through = reflection.through_reflection if reflection_through.present? # :has_one through / :has_many through reflection_through_table = reflection_through.klass.arel_table reflection_table.join(reflection_through_table).on( reflection_through_table[reflection.foreign_key].eq(reflection_table[reflection.klass.primary_key]) ).where( reflection_through_table[reflection_through.foreign_key].eq( options.record_class.arel_table[options.record_class.primary_key] ) ) else # :has_one / :has_many reflection_table.where( reflection_table[reflection.foreign_key].eq( options.record_class.arel_table[options.record_class.primary_key] ) ) end end
build_arel_for_keyed_terms(terms)
click to toggle source
# File lib/pursuit/search.rb, line 50 def build_arel_for_keyed_terms(terms) return nil if terms.blank? terms.reduce(nil) do |chain, term| attribute_name = term.key.to_sym reflection = options.relations.key?(attribute_name) ? options.record_class.reflections[term.key] : nil node = if reflection.present? attribute_names = options.relations[attribute_name] build_arel_for_reflection(reflection, attribute_names, term.operator, term.value) else node_builder = options.keyed_attributes[attribute_name] build_arel_for_node(node_builder.call, term.operator, term.value) end chain ? chain.and(node) : node end end
build_arel_for_node(node, operator, value)
click to toggle source
# File lib/pursuit/search.rb, line 68 def build_arel_for_node(node, operator, value) sanitized_value = ActiveRecord::Base.sanitize_sql_like(value) sanitized_value = sanitized_value.to_i if sanitized_value =~ /^[0-9]+$/ case operator when '>' then node.gt(sanitized_value) when '>=' then node.gteq(sanitized_value) when '<' then node.lt(sanitized_value) when '<=' then node.lteq(sanitized_value) when '*=' then node.matches("%#{sanitized_value}%") when '!*=' then node.does_not_match("%#{sanitized_value}%") when '!=' then node.not_eq(sanitized_value) when '==' if value.present? node.eq(sanitized_value) else node.eq(nil).or(node.eq('')) end else raise "The operator '#{operator}' is not supported." end end
build_arel_for_reflection(reflection, attribute_names, operator, value)
click to toggle source
# File lib/pursuit/search.rb, line 91 def build_arel_for_reflection(reflection, attribute_names, operator, value) nodes = build_arel_for_reflection_join(reflection) count_nodes = build_arel_for_relation_count(nodes, operator, value) unless reflection.belongs_to? return count_nodes if count_nodes.present? match_nodes = attribute_names.reduce(nil) do |chain, attribute_name| node = build_arel_for_node(reflection.klass.arel_table[attribute_name], operator, value) chain ? chain.or(node) : node end return nil if match_nodes.blank? nodes.where(match_nodes).project(1).exists end
build_arel_for_reflection_join(reflection)
click to toggle source
# File lib/pursuit/search.rb, line 106 def build_arel_for_reflection_join(reflection) if reflection.belongs_to? build_arel_for_belongs_to_reflection_join(reflection) else build_arel_for_has_reflection_join(reflection) end end
build_arel_for_relation_count(nodes, operator, value)
click to toggle source
# File lib/pursuit/search.rb, line 147 def build_arel_for_relation_count(nodes, operator, value) node_builder = proc do |klass| count = ActiveRecord::Base.sanitize_sql_like(value).to_i klass.new(nodes.project(Arel.star.count), count) end case operator when '>' then node_builder.call(Arel::Nodes::GreaterThan) when '>=' then node_builder.call(Arel::Nodes::GreaterThanOrEqual) when '<' then node_builder.call(Arel::Nodes::LessThan) when '<=' then node_builder.call(Arel::Nodes::LessThanOrEqual) else return nil unless value =~ /^([0-9]+)$/ case operator when '==' then node_builder.call(Arel::Nodes::Equality) when '!=' then node_builder.call(Arel::Nodes::NotEqual) when '*=' then node_builder.call(Arel::Nodes::Matches) when '!*=' then node_builder.call(Arel::Nodes::DoesNotMatch) end end end
build_arel_for_unkeyed_term(value)
click to toggle source
# File lib/pursuit/search.rb, line 40 def build_arel_for_unkeyed_term(value) return nil if value.blank? sanitized_value = "%#{ActiveRecord::Base.sanitize_sql_like(value)}%" options.unkeyed_attributes.reduce(nil) do |chain, (attribute_name, node_builder)| node = node_builder.call.matches(sanitized_value) chain ? chain.or(node) : node end end