diff --git a/sunspot/lib/sunspot/dsl/field_query.rb b/sunspot/lib/sunspot/dsl/field_query.rb index b66eeacac..e61aada49 100644 --- a/sunspot/lib/sunspot/dsl/field_query.rb +++ b/sunspot/lib/sunspot/dsl/field_query.rb @@ -52,6 +52,12 @@ def order_by_geodist(field_name, lat, lon, direction = nil) ) end + def order_by_child_document(field_name, direction = nil, block_join:) + @query.add_sort( + Sunspot::Query::Sort::ChildDocumentSort.new(field_name, block_join, direction) + ) + end + # # DEPRECATED Use order_by(:random) # diff --git a/sunspot/lib/sunspot/field_factory.rb b/sunspot/lib/sunspot/field_factory.rb index a01c84324..c4f4e941f 100644 --- a/sunspot/lib/sunspot/field_factory.rb +++ b/sunspot/lib/sunspot/field_factory.rb @@ -1,5 +1,5 @@ module Sunspot - # + # # The FieldFactory module contains classes for generating fields. FieldFactory # implementation classes should implement a #build method, although the arity # of the method depends on the type of factory. They also must implement a @@ -41,7 +41,7 @@ def extract_value(model, options = {}) end end - # + # # A StaticFieldFactory generates normal static fields. Each factory instance # contains an eager-initialized field instance, which is returned by the # #build method. @@ -60,14 +60,14 @@ def initialize(name, type, options = {}, &block) end end - # + # # Return the field instance built by this factory # def build @field end - # + # # Extract the encapsulated field's data from the given model and add it # into the Solr document for indexing. # @@ -90,7 +90,7 @@ def populate_document(document, model, options = {}) #:nodoc: end end - # + # # A unique signature identifying this field by name and type. # def signature @@ -107,7 +107,7 @@ def initialize(name, type, options = {}, &block) @field = JoinField.new(self.name, type, options) end - # + # # Return the field instance built by this factory # def build @@ -122,7 +122,7 @@ def signature end end - # + # # DynamicFieldFactories create dynamic field instances based on dynamic # configuration. # @@ -141,13 +141,13 @@ def initialize(name, type, options = {}, &block) def build(dynamic_name) AttributeField.new([@name, dynamic_name].join(separator), @type, @options.dup) end - # + # # This alias allows a DynamicFieldFactory to be used in place of a Setup # or CompositeSetup instance by query components. # alias_method :field, :build - # + # # Generate dynamic fields based on hash returned by data accessor and # add the field data to the document. # @@ -167,7 +167,7 @@ def populate_document(document, model, options = {}) end end - # + # # Unique signature identifying this dynamic field based on name and type # def signature @@ -193,7 +193,7 @@ def extract_value(model, options = {}) # TODO(ar3s3ru): how to handle incorrect field values? values = @extractor.value_for(model) adapter = options[:adapter] - unless values.is_a? Array + unless values.is_a?(Array) || rails_association?(values) raise 'Child documents field must be an Array of indexable documents' end if adapter.nil? || !adapter.respond_to?(:call) @@ -206,6 +206,13 @@ def extract_value(model, options = {}) def signature [field, ::RSolr::Document::CHILD_DOCUMENT_KEY] end + + private + + def rails_association?(values) + return false unless defined?(ActiveRecord::Associations::CollectionProxy) + values.is_a?(ActiveRecord::Associations::CollectionProxy) + end end end end diff --git a/sunspot/lib/sunspot/indexer.rb b/sunspot/lib/sunspot/indexer.rb index 8cb2419dc..70702bded 100644 --- a/sunspot/lib/sunspot/indexer.rb +++ b/sunspot/lib/sunspot/indexer.rb @@ -1,7 +1,7 @@ require 'sunspot/batcher' module Sunspot - # + # # This class presents a service for adding, updating, and removing data # from the Solr index. An Indexer instance is associated with a particular # setup, and thus is capable of indexing instances of a certain class (and its @@ -12,7 +12,7 @@ def initialize(connection) @connection = connection end - # + # # Construct a representation of the model for indexing and send it to the # connection for indexing # diff --git a/sunspot/lib/sunspot/query/block_join.rb b/sunspot/lib/sunspot/query/block_join.rb index 4a661903b..fe54c4eed 100644 --- a/sunspot/lib/sunspot/query/block_join.rb +++ b/sunspot/lib/sunspot/query/block_join.rb @@ -152,7 +152,11 @@ def all_parents_filter # to select which parents are used in the query. fq = filter_query.to_params[:fq] raise 'allParents filter must be non-empty!' if fq.nil? - fq[0] # Type filter used by Sunspot + Util.escape(fq[0]) # Type filter used by Sunspot + end + + def facet_type_filter + filter_query.to_params[:fq][0] end def secondary_filter @@ -176,9 +180,18 @@ def to_params class ParentWhich < Abstract alias some_children_filter secondary_filter - def all_parents_filter + def all_parents_parts # Use top-level scope (on parent type) as allParents filter. - scope.to_params[:fq].flatten.join(' AND ') + parts = scope.to_params[:fq].flatten + parts.map { |v| Util.escape(v) } + end + + def all_parents_filter(*args) + all_parents_parts(*args).join(' AND ') + end + + def facet_type_filter + scope.to_params[:fq].flatten[0] end def secondary_filter @@ -190,10 +203,21 @@ def secondary_filter q end + def field_list_string + parts = [] + parts << '[child' + parts << 'parentFilter="' + all_parents_parts[0] + '"' + parts << 'childFilter="' + secondary_filter.map { |f| Util.escape(f) }.join(' AND ') + '"]' + parts.join(' ') + end + def to_params - { q: render_query_string('parent', 'which') } + { + q: render_query_string('parent', 'which'), + fl: [:id] + [field_list_string] + } end end end end -end \ No newline at end of file +end diff --git a/sunspot/lib/sunspot/query/block_join_json_facet.rb b/sunspot/lib/sunspot/query/block_join_json_facet.rb index eeaf0b80c..2adb9d532 100644 --- a/sunspot/lib/sunspot/query/block_join_json_facet.rb +++ b/sunspot/lib/sunspot/query/block_join_json_facet.rb @@ -62,7 +62,7 @@ def field_name_with_local_params type: 'terms', field: @field.indexed_name, domain: { - @operator => @query.all_parents_filter, + @operator => @query.facet_type_filter, FILTER_OP => generate_filter } }.merge!(init_params) @@ -78,4 +78,4 @@ def generate_filter end end end -end \ No newline at end of file +end diff --git a/sunspot/lib/sunspot/query/sort.rb b/sunspot/lib/sunspot/query/sort.rb index dcc1bb7aa..31ff99198 100644 --- a/sunspot/lib/sunspot/query/sort.rb +++ b/sunspot/lib/sunspot/query/sort.rb @@ -1,11 +1,11 @@ module Sunspot module Query - # + # # The classes in this module implement query components that build sort # parameters for Solr. As well as regular sort on fields, there are several # "special" sorts that allow ordering for metrics calculated during the # search. - # + # module Sort #:nodoc: all DIRECTIONS = { :asc => 'asc', @@ -15,7 +15,7 @@ module Sort #:nodoc: all } class < true, the Hit object will contain the stored value for # that field. The value of this field will be typecast according to the @@ -80,7 +81,7 @@ def stored(field_name, dynamic_field_name = nil) @stored_cache[field_key] = stored_value(field_name, dynamic_field_name) end - # + # # Retrieve the instance associated with this hit. This is lazy-loaded, but # the first time it is called on any hit, all the hits for the search will # load their instances using the adapter's #load_all method. @@ -110,6 +111,11 @@ def to_param self.primary_key end + def children(type = nil) + return @child_documents if type.nil? + @child_documents.select { |d| d.class_name == type.name } + end + private def setup diff --git a/sunspot/lib/sunspot/search/hit_enumerable.rb b/sunspot/lib/sunspot/search/hit_enumerable.rb index 6e2b58aa9..1806bfd16 100644 --- a/sunspot/lib/sunspot/search/hit_enumerable.rb +++ b/sunspot/lib/sunspot/search/hit_enumerable.rb @@ -18,7 +18,7 @@ def verified_hits hits.select { |h| h.result } end - # + # # Populate the Hit objects with their instances. This is invoked the first # time any hit has its instance requested, and all hits are loaded as a # batch. @@ -27,10 +27,13 @@ def populate_hits #:nodoc: id_hit_hash = Hash.new { |h, k| h[k] = {} } hits.each do |hit| id_hit_hash[hit.class_name][hit.primary_key] = hit + hit.children.each do |child_hit| + id_hit_hash[child_hit.class_name][child_hit.primary_key] = child_hit + end end id_hit_hash.each_pair do |class_name, hits| ids = hits.map { |id, hit| hit.primary_key } - data_accessor = data_accessor_for(Util.full_const_get(class_name)) + data_accessor = data_accessor_for(Util.full_const_get(class_name)) hits_for_class = id_hit_hash[class_name] data_accessor.load_all(ids).each do |result| hit = hits_for_class.delete(Adapters::InstanceAdapter.adapt(result).id.to_s) @@ -53,7 +56,7 @@ def each_hit_with_result verified_hits.each { |hit| yield hit, hit.result } end - # + # # Get the data accessor that will be used to load a particular class out of # persistent storage. Data accessors can implement any methods that may be # useful for refining how data is loaded out of storage. When building a diff --git a/sunspot/lib/sunspot/session.rb b/sunspot/lib/sunspot/session.rb index 15f455c6e..252ddbe71 100644 --- a/sunspot/lib/sunspot/session.rb +++ b/sunspot/lib/sunspot/session.rb @@ -87,6 +87,7 @@ def more_like_this(object, *types, &block) # def index(*objects) objects.flatten! + verify_indexing_parents!(objects) @adds += objects.length indexer.add(objects) end @@ -279,5 +280,14 @@ def setup_for_types(types) CompositeSetup.for(types) end end + + def verify_indexing_parents!(objects) + # http://yonik.com/solr-nested-objects#Limitations + # Child and parent documents must be indexed in the same block + objects.each do |object| + next unless Setup.for(object.class).is_child + raise 'Child documents must be indexed with their parent' + end + end end end diff --git a/sunspot_rails/lib/sunspot/rails/searchable.rb b/sunspot_rails/lib/sunspot/rails/searchable.rb index f308c5212..24bfff1be 100644 --- a/sunspot_rails/lib/sunspot/rails/searchable.rb +++ b/sunspot_rails/lib/sunspot/rails/searchable.rb @@ -1,6 +1,6 @@ module Sunspot #:nodoc: module Rails #:nodoc: - # + # # This module adds Sunspot functionality to ActiveRecord models. As well as # providing class and instance methods, it optionally adds lifecycle hooks # to automatically add and remove models from the Solr index as they are @@ -16,7 +16,7 @@ def included(base) #:nodoc: end module ActsAsMethods - # + # # Makes a class searchable if it is not already, or adds search # configuration if it is. Note that the options passed in are only used # the first time this method is called for a particular class; so, @@ -28,7 +28,7 @@ module ActsAsMethods # complete information on the functionality provided by that method. # # ==== Options (+options+) - # + # # :auto_index:: # Automatically index models in Solr when they are saved. # Default: true @@ -53,9 +53,9 @@ module ActsAsMethods # to ignore. # :include:: # Define default ActiveRecord includes, set this to allow ActiveRecord - # to load required associations when indexing. See ActiveRecord's + # to load required associations when indexing. See ActiveRecord's # documentation on eager-loading for examples on how to set this - # Default: [] + # Default: [] # :unless:: # Only index models in Solr if the method, proc or string evaluates # to false (e.g. :unless => :should_not_index? or @@ -109,7 +109,7 @@ def searchable(options = {}, &block) end end - # + # # This method is defined on all ActiveRecord::Base subclasses. It # is false for classes on which #searchable has not been called, and # true for classes on which #searchable has been called. @@ -138,7 +138,7 @@ class < nil) + # Post.index(:batch_size => nil) # # # index in batches of 50, commit when all batches complete - # Post.index(:batch_commit => false) + # Post.index(:batch_commit => false) # # # include the associated +author+ object when loading to index - # Post.index(:include => :author) + # Post.index(:include => :author) # def solr_index(opts={}) options = { @@ -315,22 +315,22 @@ def solr_atomic_update!(updates = {}) Sunspot.atomic_update!(self, updates) end - # + # # Return the IDs of records of this class that are indexed in Solr but # do not exist in the database. Under normal circumstances, this should # never happen, but this method is provided in case something goes # wrong. Usually you will want to rectify the situation by calling # #clean_index_orphans or #reindex - # + # # ==== Options (passed as a hash) # # batch_size:: Override default batch size with which to load records. - # + # # ==== Returns # # Array:: Collection of IDs that exist in Solr but not in the database def solr_index_orphans(opts={}) - batch_size = opts[:batch_size] || Sunspot.config.indexing.default_batch_size + batch_size = opts[:batch_size] || Sunspot.config.indexing.default_batch_size solr_page = 0 solr_ids = [] @@ -343,7 +343,7 @@ def solr_index_orphans(opts={}) return solr_ids - self.connection.select_values("SELECT id FROM #{quoted_table_name}").collect(&:to_i) end - # + # # Find IDs of records of this class that are indexed in Solr but do not # exist in the database, and remove them from Solr. Under normal # circumstances, this should not be necessary; this method is provided @@ -352,7 +352,7 @@ def solr_index_orphans(opts={}) # ==== Options (passed as a hash) # # batch_size:: Override default batch size with which to load records - # + # def solr_clean_index_orphans(opts={}) solr_index_orphans(opts).each do |id| new do |fake_instance| @@ -361,7 +361,7 @@ def solr_clean_index_orphans(opts={}) end end - # + # # Classes that have been defined as searchable return +true+ for this # method. # @@ -372,7 +372,7 @@ def solr_clean_index_orphans(opts={}) def searchable? true end - + def solr_execute_search(options = {}) inherited_attributes = [:include, :select, :scopes] options.assert_valid_keys(*inherited_attributes) @@ -393,10 +393,10 @@ def solr_execute_search_ids(options = {}) search = yield search.raw_results.map { |raw_result| raw_result.primary_key.to_i } end - + protected - - # + + # # Does some logging for benchmarking indexing performance # def solr_benchmark(batch_size, counter, &block) @@ -422,7 +422,7 @@ def self.included(base) #:nodoc: alias_method :atomic_update!, :solr_atomic_update! unless method_defined? :atomic_update! end end - # + # # Index the model in Solr. If the model is already indexed, it will be # updated. Using the defaults, you will usually not need to call this # method, as models are indexed automatically when they are created or @@ -434,7 +434,7 @@ def solr_index Sunspot.index(self) end - # + # # Index the model in Solr and immediately commit. See #index # def solr_index! @@ -457,8 +457,8 @@ def solr_atomic_update(updates = {}) def solr_atomic_update!(updates = {}) Sunspot.atomic_update!(self.class, self.id => updates) end - - # + + # # Remove the model from the Solr index. Using the defaults, this should # not be necessary, as models will automatically be removed from the # index when they are destroyed. If you disable automatic removal @@ -469,7 +469,7 @@ def solr_remove_from_index Sunspot.remove(self) end - # + # # Remove the model from the Solr index and commit immediately. See # #remove_from_index #