class LSolr
A query builder of Apache Solr standard Lucene type query for Ruby.
@example How to use. Part 1:
LSolr.build(field1: 'hoge', field2: true).to_s #=> 'field1:hoge AND field2:true'
@example How to use. Part 2:
params = { field01: 'hoge', field02: :fuga, field03: 14, field04: 7.3, field05: true, field06: false, field07: Date.new(7000, 7, 1), field08: DateTime.new(6000, 5, 31, 6, 31, 43), field09: Time.new(5000, 6, 30, 12, 59, 3), field10: LSolr.new(:field10).fuzzy_match('foo'), field11: [1, 2, 3], field12: 1..10, field13: 20...40, field14: Date.new(3000, 1, 1)..Date.new(4000, 12, 31), field15: (3.0..4.0).step(0.1) } LSolr.build(params).to_s #=> 'field01:hoge AND # field02:fuga AND # field03:14 AND # field04:7.3 AND # field05:true AND # field06:false AND # field07:"7000-07-01T00:00:00Z" AND # field08:"6000-05-31T06:31:43Z" AND # field09:"5000-06-30T12:59:03Z" AND # field10:foo~2.0 AND # field11:(1 2 3) AND # field12:[1 TO 10] AND # field13:[20 TO 40} AND # field14:[3000-01-01T00:00:00Z TO 4000-12-31T00:00:00Z] AND # field15:[3.0 TO 4.0]'
@example How to use. Part 3:
bool1 = LSolr.new(:bool_field).match(true) bool2 = LSolr.new(:bool_field).match(false) date1 = LSolr.new(:date_field1).greater_than_or_equal_to('*').less_than_or_equal_to(Time.new(2000, 6, 30, 23, 59, 59)) date2 = LSolr.new(:date_field2).greater_than(Time.new(2000, 7, 1, 0, 0, 0)).less_than(Time.new(2001, 1, 1, 0, 0, 0)) left = bool1.and(date1).and(date2).wrap right = bool2.and(date1.or(date2).wrap).wrap left.or(right).to_s #=> '(bool_field:true AND date_field1:[* TO 2000-06-30T23:59:59Z] AND date_field2:{2000-07-01T00:00:00Z TO 2001-01-01T00:00:00Z}) # OR (bool_field:false AND (date_field1:[* TO 2000-06-30T23:59:59Z] OR date_field2:{2000-07-01T00:00:00Z TO 2001-01-01T00:00:00Z}))'
@example How to use. Part 4:
%w[a b c].map { |v| LSolr.new(:field).prefix_match("#{v}*") }.reduce { |a, e| a.or(e) }.wrap.not.to_s #=> 'NOT (field:a* OR field:b* OR field:c*)'
@example How to use. Part 5:
LSolr.build('a:1').and(b: 2).to_s #=> 'a:1 AND b:2'
Constants
- AND
- ArgumentError
- BOOST
- CONSTANT_SCORE
- DELIMITER_SPACE
- FORMAT_DATE_TIME
- FORMAT_INSPECT
- FORMAT_MILLISECOND_FOR_DATE_TYPE
- FORMAT_MILLISECOND_FOR_TIME_TYPE
- FORMAT_SECOND
- GREATER_THAN
- GREATER_THAN_OR_EQUAL_TO
- IncompleteQueryError
- LESS_THAN
- LESS_THAN_OR_EQUAL_TO
- NOT
- OR
- PARENTHESIS_LEFT
- PARENTHESIS_RIGHT
- PROXIMITY
- RANGE_FUZZY_MATCH_DISTANCE
- REPLACEMENT_CHAR
- RESERVED_SYMBOLS
- RESERVED_WORDS
- TO
- TypeError
- WILD_CARD
Attributes
Public Class Methods
Builds composite query and returns builder instance.
@param params [Hash{Symbol => String, Symbol, Integer, Float, true, false, Range, Date, Time, Array<String, Symbol, Integer>}, String] query terms or a raw query
@return [LSolr] a instance
@raise [LSolr::ArgumentError] if specified parameters have a not supported type value
# File lib/lsolr.rb, line 114 def build(params) case params when Hash then params.map { |f, v| build_query(f, v) }.reduce { |a, e| a.and(e) } when String then build_raw_query(params) else raise TypeError, "Could not build solr query. Please specify a Hash or String value. `#{params}` given." end rescue TypeError => e raise ArgumentError, "#{e.message} It is not a supported type." end
Create a new query builder instance.
@param field_name [String, Symbol] a field name @return [LSolr] a instance
# File lib/lsolr.rb, line 165 def initialize(field_name = nil) if field_name.nil? @field = '' else field(field_name) end @expr_not = @value = @range_first = @range_last = @boost = @constant_score = @raw = '' @left_parentheses = [] @right_parentheses = [] end
Private Class Methods
# File lib/lsolr.rb, line 138 def build_array_query(field, values) values.empty? ? new(field) : new(field).match_in(values) end
# File lib/lsolr.rb, line 150 def build_enumerator_query(field, values) last = nil values.each { |v| last = v } new(field).greater_than_or_equal_to(values.first).less_than_or_equal_to(last) end
# File lib/lsolr.rb, line 126 def build_query(field, value) # rubocop:disable Metrics/CyclomaticComplexity case value when String, Symbol, Integer, Float, true, false then new(field).match(value) when Date, Time then new(field).date_time_match(value) when LSolr then value when Array then build_array_query(field, value) when Range then build_range_query(field, value) when Enumerator then build_enumerator_query(field, value) else raise TypeError, "Could not build solr query. field: `#{field}`, value: `#{value}` given." end end
# File lib/lsolr.rb, line 142 def build_range_query(field, value) if value.exclude_end? new(field).greater_than_or_equal_to(value.first).less_than(value.last) else new(field).greater_than_or_equal_to(value.first).less_than_or_equal_to(value.last) end end
# File lib/lsolr.rb, line 156 def build_raw_query(query) query.empty? ? new : new.raw(query) end
Public Instance Methods
Builds a composite query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#the-boolean-operator-and The Boolean Operator AND
(“&&”)
@param another [LSolr, Hash, String] another query builder instance or query params or raw query string
@return [LSolr] copied another query builder instance
# File lib/lsolr.rb, line 448 def and(another) link(another, AND) end
A query is blank if term is incomplete in expression.
@return [true, false]
# File lib/lsolr.rb, line 202 def blank? managed_query_absence = @field.empty? || (@value.empty? && (@range_first.empty? || @range_last.empty?)) managed_query_absence && @raw.empty? end
Boosts a query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#boosting-a-term-with Boosting a Term with “^”
@param factor [Float] a boost factor number
@return [LSolr] self instance
@raise [LSolr::ArgumentError] if specified boost factor is invalid
# File lib/lsolr.rb, line 274 def boost(factor) raise ArgumentError, "The boost factor must be a positive number. `#{factor}` given." unless valid_boost_factor?(factor) @boost = "#{BOOST}#{factor}" self end
Specifies scoring result in expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#constant-score-with Constant Score with “^=”
@param score [Float] a constant score
@return [LSolr] self instance
@raise [LSolr::ArgumentError] if specified score number is invalid
# File lib/lsolr.rb, line 290 def constant_score(score) raise ArgumentError, "The constant score must be a number. `#{score}` given." unless valid_score?(score) @constant_score = "#{CONSTANT_SCORE}#{score}" self end
Builds a normal query expression with dates and times.
@see lucene.apache.org/solr/guide/7_2/working-with-dates.html Working with Dates
@param value [String, Date, Time] a filter value
@return [LSolr] self instance
# File lib/lsolr.rb, line 342 def date_time_match(value) value = stringify(value, symbols: RESERVED_SYMBOLS - %w[- : . / +]) @value = %("#{value}") self end
Sets a field name.
@param name [String, Symbol] a field name
@return [LSolr] self instance
@raise [LSolr::ArgumentError] if specified field name is empty
# File lib/lsolr.rb, line 221 def field(name) raise ArgumentError, "The field name must be a not empty string value. `#{name}` given." unless present_string?(name) @field = name.to_s self end
Builds a fuzzy search query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#fuzzy-searches Fuzzy Searches
@param value [String] a search word @param distance [Float] a proximity distance
@return [LSolr] self instance
@raise [LSolr::ArgumentError] if specified distance is out of range
# File lib/lsolr.rb, line 386 def fuzzy_match(value, distance: 2.0) raise ArgumentError, "Out of #{RANGE_FUZZY_MATCH_DISTANCE}. `#{distance}` given." unless valid_fuzzy_match_distance?(distance) @value = "#{clean(value).split.join}#{PROXIMITY}#{distance}" self end
Builds a range search query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#range-searches Range Searches
@param value [String, Integer, Date, Time] a filter value
@return [LSolr] self instance
# File lib/lsolr.rb, line 400 def greater_than(value) @range_first = "#{GREATER_THAN}#{stringify(value)}" self end
Builds a range search query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#range-searches Range Searches
@param value [String, Integer, Date, Time] a filter value
@return [LSolr] self instance
# File lib/lsolr.rb, line 424 def greater_than_or_equal_to(value) @range_first = "#{GREATER_THAN_OR_EQUAL_TO}#{stringify(value)}" self end
Returns a first term of query.
@return [LSolr] a first term of query.
# File lib/lsolr.rb, line 466 def head if present_query?(prev) prev.head else self end end
Returns instance information.
@return [String] instance information
# File lib/lsolr.rb, line 193 def inspect format(FORMAT_INSPECT, class: self.class.name, object: object_id << 1, query: present? ? to_s : '') end
Builds a range search query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#range-searches Range Searches
@param value [String, Integer, Date, Time] a filter value
@return [LSolr] self instance
# File lib/lsolr.rb, line 412 def less_than(value) @range_last = "#{stringify(value)}#{LESS_THAN}" self end
Builds a range search query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#range-searches Range Searches
@param value [String, Integer, Date, Time] a filter value
@return [LSolr] self instance
# File lib/lsolr.rb, line 436 def less_than_or_equal_to(value) @range_last = "#{stringify(value)}#{LESS_THAN_OR_EQUAL_TO}" self end
Builds a normal query expression.
@param value [String, Integer, true, false] a search word or a filter value
@return [LSolr] self instance
@raise [LSolr::ArgumentError] if specified value is empty
# File lib/lsolr.rb, line 304 def match(value) raise ArgumentError, "`#{value}` given. It must be a not empty value." unless present_string?(value.to_s) return match_in(value) if value.is_a?(Array) return date_time_match(value) if value.is_a?(Date) || value.is_a?(Time) values = clean(value).split if values.size > 1 phrase_match(values) else @value = values.join self end end
Builds a normal multi value query expression.
@param value [Array<String, Symbol, Integer>] a search words or a filter values
@return [LSolr] self instance
@raise [LSolr::ArgumentError] if specified value is a empty array or not array
# File lib/lsolr.rb, line 326 def match_in(values) raise ArgumentError, "`#{values}` given. It must be a not empty array." unless present_array?(values) return match(values.first) if values.size == 1 values = values.map { |v| clean(v) } @value = "(#{values.join(DELIMITER_SPACE)})" self end
Adds the boolean operator `NOT` to query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#the-boolean-operator-not The Boolean Operator NOT
(“!”)
@return [LSolr] self instance
# File lib/lsolr.rb, line 259 def not this = dup this.head.expr_not = "#{NOT} " this end
Builds a composite query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#boolean-operators-supported-by-the-standard-query-parser Boolean Operators Supported by the Standard Query Parser
@param another [LSolr, Hash, String] another query builder instance or query params or raw query string
@return [LSolr] copied another query builder instance
# File lib/lsolr.rb, line 459 def or(another) link(another, OR) end
Builds a phrase or proximity search query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#grouping-clauses-within-a-field Grouping Clauses within a Field @see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#proximity-searches Proximity Searches
@param values [Array<String>] search words @param distance [Integer] proximity distance
@return [LSolr] self instance
# File lib/lsolr.rb, line 369 def phrase_match(values, distance: 0) value = values.map { |v| clean(v).split }.flatten.join(DELIMITER_SPACE) proximity_match = distance.to_s.to_i > 0 ? "#{PROXIMITY}#{distance}" : '' @value = %("#{value}"#{proximity_match}) self end
Builds a prefix search query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#wildcard-searches Wildcard Searches
@param value [String] a search word
@return [LSolr] self instance
# File lib/lsolr.rb, line 355 def prefix_match(value) @value = clean(value, symbols: RESERVED_SYMBOLS - %w[* ?]).split.join(WILD_CARD) self end
A query is present if it's not blank.
@return [true, false]
# File lib/lsolr.rb, line 210 def present? !blank? end
Sets a raw query.
@param query [String] a raw query string
@return [LSolr] self instance
@raise [LSolr::ArgumentError] if specified raw query string is empty
# File lib/lsolr.rb, line 235 def raw(query) raise ArgumentError, "The raw query must be a not empty string value. `#{query}` given." unless present_string?(query) @raw = query.to_s self end
Returns Apache Solr standard lucene type query string.
@return [String] a stringified query
@raise [LSolr::IncompleteQueryError] if the query is incompletely
# File lib/lsolr.rb, line 182 def to_s raise IncompleteQueryError, 'Please specify a term of search.' if blank? decorate_linked_expressions_if_needed(build_expression) end
Adds parentheses to query expression.
@see lucene.apache.org/solr/guide/7_2/the-standard-query-parser.html#grouping-terms-to-form-sub-queries Grouping Terms to Form Sub-Queries
@return [LSolr] copied self instance
# File lib/lsolr.rb, line 247 def wrap this = dup this.head.left_parentheses << PARENTHESIS_LEFT this.right_parentheses << PARENTHESIS_RIGHT this end
Private Instance Methods
# File lib/lsolr.rb, line 558 def build_expression if raw? @raw elsif range_search? "#{@field}:#{@range_first} #{TO} #{@range_last}" else "#{@field}:#{@value}" end end
# File lib/lsolr.rb, line 551 def build_instance_if_needed(another) case another when self.class then another when Hash, String then self.class.build(another) end end
# File lib/lsolr.rb, line 514 def clean(value, symbols: RESERVED_SYMBOLS) value.to_s .tr(symbols.join, REPLACEMENT_CHAR) .gsub(RESERVED_WORDS) { |match| "\\#{match}" } end
# File lib/lsolr.rb, line 568 def decorate_linked_expressions_if_needed(expr) expr = "#{expr_not}#{left_parentheses.join}#{expr}#{right_parentheses.join}" expr = "#{prev} #{operator} #{expr}" if present_query?(prev) scoring = present_string?(@constant_score) ? @constant_score : @boost "#{expr}#{scoring}" end
# File lib/lsolr.rb, line 528 def format_date(date) msec_str = case date when Date then date.strftime(FORMAT_MILLISECOND_FOR_DATE_TYPE).gsub(date.strftime(FORMAT_SECOND), '') when Time then date.strftime(FORMAT_MILLISECOND_FOR_TIME_TYPE) else raise TypeError, "Could not format dates or times. `#{date}` given." end return date.strftime(FORMAT_DATE_TIME) if msec_str == '000' "#{date.strftime('%Y-%m-%dT%H:%M:%S')}.#{msec_str}Z" end
# File lib/lsolr.rb, line 476 def initialize_copy(obj) obj.prev = obj.prev.dup if present_query?(obj.prev) obj.left_parentheses = obj.left_parentheses.dup obj.right_parentheses = obj.right_parentheses.dup end
# File lib/lsolr.rb, line 540 def link(another, operator) another = build_instance_if_needed(another) return self unless present_query?(another) another = another.dup head = another.head head.prev = dup head.operator = operator another end
# File lib/lsolr.rb, line 494 def present_array?(val) !val.nil? && val.is_a?(Array) && !val.compact.empty? && val.map(&:to_s).map(&:empty?).none? end
# File lib/lsolr.rb, line 498 def present_query?(val) !val.nil? && val.present? end
# File lib/lsolr.rb, line 490 def present_string?(val) !val.nil? && (val.is_a?(String) || val.is_a?(Symbol)) && !val.empty? end
# File lib/lsolr.rb, line 482 def range_search? @value.empty? && !@range_first.empty? && !@range_last.empty? end
# File lib/lsolr.rb, line 486 def raw? !@raw.empty? end
# File lib/lsolr.rb, line 520 def stringify(value, symbols: RESERVED_SYMBOLS - %w[- : . / + *]) if value.is_a?(Date) || value.is_a?(Time) format_date(value) else clean(value, symbols: symbols) end end
# File lib/lsolr.rb, line 502 def valid_boost_factor?(val) (val.is_a?(Float) || val.is_a?(Integer)) && val > 0 end
# File lib/lsolr.rb, line 510 def valid_fuzzy_match_distance?(val) (val.is_a?(Float) || val.is_a?(Integer)) && RANGE_FUZZY_MATCH_DISTANCE.member?(val) end
# File lib/lsolr.rb, line 506 def valid_score?(val) val.is_a?(Float) || val.is_a?(Integer) end