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
#