class DatastaxRails::Relation
DatastaxRails
Relation
¶ ↑
Constants
- MULTI_VALUE_METHODS
- SINGLE_VALUE_METHODS
- SOLR_CHAR_RX
- SOLR_DATE_REGEX
Attributes
Public Class Methods
Initializes the Relation
. Defaults page value to 1, per_page to the class default, and solr use to true. Everything else gets defaulted to nil or empty.
@param [Class] klass the child of DatastaxRails::Base
that this relation searches @param [String, Symbol] column_family
the name of the column family this relation searches
# File lib/datastax_rails/relation.rb, line 41 def initialize(klass, column_family) @klass, @column_family = klass, column_family @loaded = false @results = [] @default_scoped = false @cql = DatastaxRails::Cql.for_class(klass) SINGLE_VALUE_METHODS.each { |v| instance_variable_set(:"@#{v}_value", nil) } MULTI_VALUE_METHODS.each { |v| instance_variable_set(:"@#{v}_values", []) } @highlight_options = {} @per_page_value = @klass.default_page_size @page_value = 1 @use_solr_value = :default @extensions = [] @create_with_value = {} @escape_value = :default @stats = {} if @klass.default_query_parser @query_parser_value = if @klass.default_query_parser.is_a?(Hash) @klass.default_query_parser else { @klass.default_query_parser => {} } end end end
Public Instance Methods
Returns true if the two relations have the same query parameters
# File lib/datastax_rails/relation.rb, line 68 def ==(other) case other when Relation # This is not a valid implementation. It's a placeholder till I figure out the right way. MULTI_VALUE_METHODS.each do |m| return false unless other.send("#{m}_values") == send("#{m}_values") end SINGLE_VALUE_METHODS.each do |m| return false unless other.send("#{m}_value") == send("#{m}_value") end return true when Array to_a == other end end
Returns true if there are any results given the current criteria
# File lib/datastax_rails/relation.rb, line 85 def any? if block_given? to_a.any? { |*block_args| yield(*block_args) } else !empty? end end
Performs a deep copy using Marshal when cloning.
# File lib/datastax_rails/relation.rb, line 191 def clone dup.tap do |r| MULTI_VALUE_METHODS.each do |m| r.send("#{m}_values=", Marshal.load(Marshal.dump(send("#{m}_values")))) end SINGLE_VALUE_METHODS.each do |m| r.send("#{m}_value=", Marshal.load(Marshal.dump(send("#{m}_value")))) if send("#{m}_value") end end end
Sends a commit message to SOLR
# File lib/datastax_rails/relation.rb, line 652 def commit_solr rsolr.commit commit_attributes: {} end
Returns the total number of entries that match the given search. This means the total number of matches regardless of page size. If the relation has not been populated yet, a limit of 1 will be placed on the query before it is executed.
For a grouped query, this still returns the total number of matching documents
Compare with size
.
# File lib/datastax_rails/relation.rb, line 103 def count @count ||= if with_default_scope.path_decision == :solr with_default_scope.count_via_solr else with_default_scope.count_via_cql end end
# File lib/datastax_rails/relation.rb, line 295 def count_via_cql cql = @cql.select(['count(*)']) cql.using(@consistency_value) if @consistency_value @where_values.each do |wv| wv.each do |k, v| attr = (k.to_s == 'id' ? @klass.primary_key : k) col = klass.column_for_attribute(attr) if col.primary v.compact! if v.respond_to?(:compact) return 0 if v.blank? end values = Array(v).map do |val| col.type_cast_for_cql3(val) end cql.conditions(attr => values) end end cql.allow_filtering if @allow_filtering_value cql.execute.first['count'] end
Runs the query with a limit of 1 just to grab the total results attribute off the result set.
# File lib/datastax_rails/relation.rb, line 387 def count_via_solr results = limit(1).select(:id).to_a @group_value ? results.total_for_all : results.total_entries end
Create a new object with all of the criteria from this relation applied
# File lib/datastax_rails/relation.rb, line 241 def create(*args, &block) scoping { @klass.create(*args, &block) } end
Like create
but throws an exception on failure
# File lib/datastax_rails/relation.rb, line 246 def create!(*args, &block) scoping { @klass.create!(*args, &block) } end
Returns the current page for will_paginate compatibility
# File lib/datastax_rails/relation.rb, line 117 def current_page page_value.try(:to_i) end
Gets a default scope with no conditions or search attributes set.
# File lib/datastax_rails/relation.rb, line 132 def default_scope klass.scoped.with_default_scope end
Everything that gets indexed into solr is downcased as part of the analysis phase. Normally, this is done to the query as well, but if your query includes wildcards then analysis isn't performed. This means that the query does not get downcased. We therefore need to perform the downcasing ourselves. This does it while still leaving boolean operations (AND, OR, NOT, TO) and dates upcased.
# File lib/datastax_rails/relation.rb, line 662 def downcase_query(value) # rubocop:disable Style/MultilineBlockChain if value.is_a?(String) value.split(/\bAND\b/).map do |a| a.split(/\bOR\b/).map do |o| o.split(/\bNOT\b/).map do |n| n.split(/\bTO\b/).map(&:downcase).join('TO') end.join('NOT') end.join('OR') end.join('AND').gsub(SOLR_DATE_REGEX) { Regexp.last_match[1].upcase } else value end # rubocop:enable Style/MultilineBlockChain end
Returns true if there are no results given the current criteria
# File lib/datastax_rails/relation.rb, line 147 def empty? return @results.empty? if loaded? c = count c.respond_to?(:zero?) ? c.zero? : c.empty? end
# File lib/datastax_rails/relation.rb, line 403 def full_solr_range(attr) if klass.attribute_definitions[attr] klass.attribute_definitions[attr].full_solr_range else '[\"\" TO *]' end end
Copies will have changes made to the criteria and so need to be reset.
# File lib/datastax_rails/relation.rb, line 185 def initialize_copy(_other) reset @search = nil end
Inspects the results of the search instead of the Relation
itself. Passing true causes the Relation
to be inspected.
@param [Boolean] just_me if true, inspect the Relation
, otherwise the results
# File lib/datastax_rails/relation.rb, line 622 def inspect(just_me = false) just_me ? super() : to_a.inspect end
Returns true if there are multiple results given the current criteria
# File lib/datastax_rails/relation.rb, line 155 def many? if block_given? to_a.many? { |*block_args| yield(*block_args) } else count > 1 end end
If we index something into both cassandra and solr, we rename the cassandra column. This method maps the column names as necessary
# File lib/datastax_rails/relation.rb, line 283 def map_cassandra_columns(conditions) {}.tap do |mapped| conditions.each do |k, v| if (klass.attribute_definitions[k].indexed == :both) mapped["__#{k}"] = v else mapped[k] = v end end end end
Constructs a new instance of the class this relation points to with any criteria from this relation applied
# File lib/datastax_rails/relation.rb, line 165 def new(*args, &block) scoping { @klass.new(*args, &block) } end
current_page
+ 1 or nil if there is no next page
# File lib/datastax_rails/relation.rb, line 127 def next_page current_page < total_pages ? (current_page + 1) : nil end
# File lib/datastax_rails/relation.rb, line 255 def path_decision return :cassandra if klass <= DatastaxRails::CassandraOnlyModel case use_solr_value when false return :cassandra when true return :solr else [order_values, where_not_values, fulltext_values, greater_than_values, less_than_values, field_facet_values, range_facet_values, group_value, facet_threads_value].each do |solr_only_stuff| return :solr unless solr_only_stuff.blank? end return :solr unless group_value.blank? return :solr unless page_value == 1 @where_values.each do |wv| wv.each do |k, _v| col = klass.column_for_attribute(k) next if col && (col.options[:cql_index] || col.primary) return :solr end end # If we get here, we can safely run this query via Cassandra return :cassandra end end
current_page
- 1 or nil if there is no previous page
# File lib/datastax_rails/relation.rb, line 122 def previous_page current_page > 1 ? (current_page - 1) : nil end
Constructs a CQL query and runs it against Cassandra directly. For this to work, you need to run against either the primary key or a secondary index. For ad-hoc queries, you will have to use Solr. rubocop:disable Metrics/MethodLength rubocop:disable Metrics/PerceivedComplexity rubocop:disable Metrics/CyclomaticComplexity
# File lib/datastax_rails/relation.rb, line 322 def query_via_cql select_columns = select_values.empty? ? (@klass.attribute_definitions.keys - @klass.lazy_attributes) : select_values.flatten cql = @cql.select((select_columns + [@klass.primary_key]).uniq) cql.using(@consistency_value) if @consistency_value @where_values.each do |wv| wv.each do |k, v| attr = (k.to_s == 'id' ? @klass.primary_key : k) col = klass.column_for_attribute(attr) if col.primary v.compact! if v.respond_to?(:compact!) return [] if v.blank? end values = Array(v).map do |val| col.type_cast_for_cql3(val) end cql.conditions(attr => values) end end @greater_than_values.each do |gtv| gtv.each do |k, v| # Special case if inequality is equal to the primary key (we're paginating) cql.paginate(v) if (k.to_s == @klass.primary_key) end end cql.limit(@per_page_value) if @per_page_value cql.allow_filtering if @allow_filtering_value results = [] begin cql.execute.each do |row| results << @klass.instantiate(row[@klass.primary_key], row, select_columns) end rescue Cassandra::Errors::ValidationError => e # If we get an exception about an empty key, ignore it. We'll return an empty set. raise unless e.message =~ /Key may not be empty/ end if @slow_order_values.any? results.sort! do |a, b| values = slow_ordering(a, b) values[0] <=> values[1] end end results end
Constructs a solr query to run against SOLR.
It's worth noting that where and where_not make use of individual filter_queries. If that's not what you want, you might be better off constructing your own fulltext query and sending that in.
TODO: break this apart into multiple methods
# File lib/datastax_rails/relation.rb, line 418 def query_via_solr # rubocop:disable all filter_queries = [] orders = [] @where_values.each do |wv| wv.each do |k, v| # If v is blank but not false, check that there is no value for the field in the document if v.blank? && v != false filter_queries << "-#{k}:#{full_solr_range(k)}" else filter_queries << "#{k}:(#{solr_format(k, v)})" end end end @where_not_values.each do |wnv| wnv.each do |k, v| # If v is blank but not false, check for any value in the field in the document if v.blank? && v != false filter_queries << "#{k}:#{full_solr_range(k)}" else filter_queries << "-#{k}:(#{solr_format(k, v)})" end end end @greater_than_values.each do |gtv| gtv.each do |k, v| filter_queries << "#{k}:[#{solr_format(k, v)} TO *]" end end @less_than_values.each do |ltv| ltv.each do |k, v| filter_queries << "#{k}:[* TO #{solr_format(k, v)}]" end end @order_values.each do |ov| ov.each do |k, v| if @reverse_order_value orders << "#{k} #{v == :asc ? 'desc' : 'asc'}" else orders << "#{k} #{v == :asc ? 'asc' : 'desc'}" end end end sort = orders.join(',') q = @fulltext_values.empty? ? '*:*' : @fulltext_values.map { |ftv| '(' + ftv[:query] + ')' }.join(' AND ') params = { q: q, commit: true } params[:sort] = sort params[:fq] = filter_queries unless filter_queries.empty? if @query_parser_value params['defType'] = @query_parser_value.keys.first params.merge(@query_parser_value.values.first) end # Facets # facet=true to enable faceting, facet.field=<field_name> (can appear more than once for multiple fields) # Additional options: f.<field_name>.facet.<option> [e.g. f.author.facet.sort=index] # Facet Fields unless field_facet_values.empty? params['facet'] = 'true' params['facet.threads'] = facet_threads_value if facet_threads_value.present? facet_fields = [] field_facet_values.each do |facet| facet_field = facet[:field] facet_fields << facet_field facet[:options].each do |key, value| params["f.#{facet_field}.facet.#{key}"] = value.to_s end end params['facet.field'] = facet_fields end # Facet Ranges unless range_facet_values.empty? params['facet'] = 'true' facet_fields = [] range_facet_values.each do |facet| facet_field = facet[:field] facet_fields << facet_field facet[:options].each do |key, value| params["f.#{facet_field}.facet.range.#{key}"] = value.to_s end end params['facet.range'] = facet_fields end if @highlight_options[:fields].present? params[:hl] = true params['hl.fl'] = @highlight_options[:fields] params['hl.snippets'] = @highlight_options[:snippets] if @highlight_options[:snippets] params['hl.fragsize'] = @highlight_options[:fragsize] if @highlight_options[:fragsize] params['hl.requireFieldMatch'] = @highlight_options[:require_field_match] if @highlight_options[:require_field_match].present? if @highlight_options[:use_fast_vector] params['hl.useFastVectorHighlighter'] = true params['hl.tag.pre'] = @highlight_options[:pre_tag] if @highlight_options[:pre_tag].present? params['hl.tag.post'] = @highlight_options[:post_tag] if @highlight_options[:post_tag].present? else params['hl.mergeContiguous'] = @highlight_options[:merge_contiguous].present? params['hl.simple.pre'] = @highlight_options[:pre_tag] if @highlight_options[:pre_tag].present? params['hl.simple.post'] = @highlight_options[:post_tag] if @highlight_options[:post_tag].present? params['hl.maxAnalyzedChars'] = @highlight_options[:max_analyzed_chars] if @highlight_options[:max_analyzed_chars].present? end end select_columns = select_values.empty? ? (@klass.attribute_definitions.keys - @klass.lazy_attributes) : select_values.flatten select_columns << @klass.primary_key select_columns.map! { |c| @klass.column_for_attribute(c).try(:type) == :map ? "#{c}*" : c.to_s } params[:fl] = select_columns.uniq.join(',') unless @stats_values.empty? params[:stats] = 'true' @stats_values.flatten.each do |sv| params['stats.field'] = sv end params['stats.facet'] = @group_value end solr_response = nil if @group_value results = DatastaxRails::GroupedCollection.new params[:group] = 'true' params[:rows] = 10_000 params['group.field'] = @group_value params['group.limit'] = @per_page_value params['group.offset'] = (@page_value - 1) * @per_page_value params['group.ngroups'] = 'false' # must be false due to issues with solr sharding ActiveSupport::Notifications.instrument( 'solr.datastax_rails', name: 'Search', klass: @klass.name, search: params) do solr_response = rsolr.post('select', data: params) response = solr_response['grouped'][@group_value.to_s] results.total_groups = response['groups'].size results.total_for_all = response['matches'].to_i results.total_entries = 0 response['groups'].each do |group| results[group['groupValue']] = parse_docs(group['doclist'], select_columns) if results[group['groupValue']].total_entries > results.total_entries results.total_entries = results[group['groupValue']].total_entries end end end else ActiveSupport::Notifications.instrument( 'solr.datastax_rails', name: 'Search', klass: @klass.name, search: params.merge(page: @page_value, per_page: @per_page_value)) do solr_response = rsolr.paginate(@page_value, @per_page_value, 'select', data: params, method: :post) response = solr_response['response'] results = parse_docs(response, select_columns) results.highlights = solr_response['highlighting'] end end if solr_response['stats'] @stats = solr_response['stats']['stats_fields'].with_indifferent_access end # Apply Facets if they exist if solr_response['facet_counts'] results.facets = {} results.facets = results.facets.merge(solr_response['facet_counts']['facet_fields'].to_h) results.facets = results.facets.merge(solr_response['facet_counts']['facet_ranges'].to_h) end results end
Reloads the results from cassandra or solr as appropriate
# File lib/datastax_rails/relation.rb, line 170 def reload reset to_a self end
Empties out the current results. The next call to to_a
will re-run the query.
# File lib/datastax_rails/relation.rb, line 178 def reset @loaded = @first = @last = @scope_for_create = @count = nil @stats = {} @results = [] end
Override respond_to? so that it matches method_missing
# File lib/datastax_rails/relation.rb, line 251 def respond_to?(method, include_private = false) Array.method_defined?(method) || @klass.respond_to?(method, include_private) || super end
Creates a scope that includes all of the where values plus anything that is in create_with_value
.
# File lib/datastax_rails/relation.rb, line 647 def scope_for_create @scope_for_create ||= where_values_hash.merge(create_with_value) end
Scope all queries to the current scope.
Example¶ ↑
Comment.where(:post_id => 1).scoping do Comment.first # SELECT * FROM comments WHERE post_id = 1 end
Please check unscoped if you want to remove all previous scopes (including the default_scope
) during the execution of a block.
# File lib/datastax_rails/relation.rb, line 636 def scoping @klass.send(:with_scope, self, :overwrite) { yield } end
Returns the size of the total result set for the given criteria NOTE that this takes pagination into account so will only return the number of results in the current page. DatastaxRails
models can have a default_page_size
set which will cause them to be paginated all the time.
For a grouped query, this returns the size of the largest group.
Compare with count
# File lib/datastax_rails/relation.rb, line 211 def size return @results.size if loaded? && !@group_value total_entries = count (per_page_value && total_entries > per_page_value) ? per_page_value : total_entries end
# File lib/datastax_rails/relation.rb, line 367 def slow_ordering(obj1, obj2) [[], []].tap do |values| i = 0 @slow_order_values.each do |ordering| ordering.each do |k, v| if v == :asc values[0][i] = obj1.send(k) values[1][i] = obj2.send(k) else values[1][i] = obj1.send(k) values[0][i] = obj2.send(k) end end i += 1 end end end
Escapes values that might otherwise mess up the URL or confuse SOLR. If you want to handle escaping yourself for a particular query then SearchMethods#dont_escape
is what you're looking for.
# File lib/datastax_rails/relation.rb, line 395 def solr_escape(str) if str.is_a?(String) && escape_value str.gsub(SOLR_CHAR_RX, '\\\\\1') else str end end
# File lib/datastax_rails/relation.rb, line 111 def stats loaded? || to_a @stats end
Actually executes the query if not already executed. Returns a standard array thus no more methods may be chained.
# File lib/datastax_rails/relation.rb, line 226 def to_a return @results if loaded? if with_default_scope.path_decision == :solr @results = with_default_scope.query_via_solr @count = @group_value ? @results.total_for_all : @results.total_entries else @results = with_default_scope.query_via_cql end @loaded = true @results end
Returns the total number of pages required to display the results given the current page size. Used by will_paginate.
# File lib/datastax_rails/relation.rb, line 219 def total_pages return 1 unless @per_page_value (count / @per_page_value.to_f).ceil end
Merges all of the where values together into a single hash
# File lib/datastax_rails/relation.rb, line 641 def where_values_hash where_values.reduce({}) { |a, e| a.merge(e) } end
Protected Instance Methods
Parse out a set of documents and return the results
@param response [Hash] the response hash from SOLR with a set of documents @param select_columns [Array] the columns that we actually selected from SOLR
@return [DatastaxRails::Collection] the resulting collection
# File lib/datastax_rails/relation.rb, line 600 def parse_docs(response, select_columns) results = DatastaxRails::Collection.new results.per_page = @per_page_value results.current_page = @page_value || 1 results.total_entries = response['numFound'].to_i response['docs'].each do |doc| id = @klass.attribute_definitions[@klass.primary_key].type_cast(doc[@klass.primary_key]) if @consistency_value obj = @klass.with_cassandra.consistency(@consistency_value).find_by_id(id) results << obj if obj else results << @klass.instantiate(id, doc, select_columns) end end results end
Calculates the solr URL and sets up an RSolr connection
# File lib/datastax_rails/relation.rb, line 691 def rsolr @klass.solr_connection end