Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BlockJoin improvements for Rails applications #23

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions sunspot/lib/sunspot/dsl/field_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>order_by(:random)</code>
#
Expand Down
29 changes: 18 additions & 11 deletions sunspot/lib/sunspot/field_factory.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
#
Expand All @@ -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
Expand All @@ -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
Expand All @@ -122,7 +122,7 @@ def signature
end
end

#
#
# DynamicFieldFactories create dynamic field instances based on dynamic
# configuration.
#
Expand All @@ -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.
#
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
4 changes: 2 additions & 2 deletions sunspot/lib/sunspot/indexer.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
#
Expand Down
34 changes: 29 additions & 5 deletions sunspot/lib/sunspot/query/block_join.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
end
4 changes: 2 additions & 2 deletions sunspot/lib/sunspot/query/block_join_json_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -78,4 +78,4 @@ def generate_filter
end
end
end
end
end
35 changes: 25 additions & 10 deletions sunspot/lib/sunspot/query/sort.rb
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -15,7 +15,7 @@ module Sort #:nodoc: all
}

class <<self
#
#
# Certain field names are "special", referring to specific non-field
# sorts, which are generally by other metrics associated with hits.
#
Expand All @@ -30,7 +30,7 @@ def special(name)
end
end

#
#
# Base class for sorts. All subclasses should implement the #to_param
# method, which is a string that is then concatenated with other sort
# strings by the SortComposite to form the sort parameter.
Expand All @@ -42,19 +42,19 @@ def initialize(direction)

private

#
#
# Translate fairly forgiving direction argument into solr direction
#
def direction_for_solr
DIRECTIONS[@direction] ||
DIRECTIONS[@direction] ||
raise(
ArgumentError,
"Unknown sort direction #{@direction}. Acceptable input is: #{DIRECTIONS.keys.map { |input| input.inspect } * ', '}"
)
end
end

#
#
# A FieldSort is the usual kind of sort, by the value of a particular
# field, ascending or descending
#
Expand All @@ -71,15 +71,15 @@ def to_param
end
end

#
#
# A RandomSort uses Solr's random field functionality to sort results
# (usually) randomly.
#
class RandomSort < Abstract
def initialize(options_or_direction=nil)
if options_or_direction.is_a?(Hash)
@seed, @direction = options_or_direction[:seed], options_or_direction[:direction]
else
else
@direction = options_or_direction
end

Expand All @@ -91,7 +91,7 @@ def to_param
end
end

#
#
# A ScoreSort sorts by keyword relevance score. This is only useful when
# performing fulltext search.
#
Expand Down Expand Up @@ -155,6 +155,21 @@ def to_s
"#{function}(#{fields.map(&:to_s).join(",")})"
end
end

#
# A ChildDocumentSort sorts on child documents.
#
class ChildDocumentSort < Abstract
def initialize(field_name, block_join, direction)
@field_name = field_name
@block_join = block_join
@direction = (direction || :asc).to_sym
end
def to_param
field = Setup.for(@block_join[:type]).field(@field_name)
"childfield(#{field.indexed_name}) #{@direction}"
end
end
end
end
end
3 changes: 2 additions & 1 deletion sunspot/lib/sunspot/search/field_json_facet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ class FieldJsonFacet
attr_reader :name

def initialize(field, search, options)
@name, @search, @options = name, search, options
@search, @options = search, options
@name = (options[:name] || field.name)
@field = field
end

Expand Down
20 changes: 13 additions & 7 deletions sunspot/lib/sunspot/search/hit.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Sunspot
module Search
#
#
# Hit objects represent the raw information returned by Solr for a single
# document. As well as the primary key and class name, hit objects give
# access to stored field values, keyword relevance score, and keyword
Expand All @@ -9,15 +9,15 @@ module Search
class Hit
SPECIAL_KEYS = Set.new(%w(id type score)) #:nodoc:

#
#
# Primary key of object associated with this hit, as string.
#
attr_reader :primary_key
#
#
# Class name of object associated with this hit, as string.
#
attr_reader :class_name
#
#
# Keyword relevance score associated with this result. Nil if this hit
# is not from a keyword search.
#
Expand All @@ -33,8 +33,9 @@ def initialize(raw_hit, highlights, search) #:nodoc:
@stored_values = raw_hit
@stored_cache = {}
@highlights = highlights
@child_documents = (raw_hit['_childDocuments_'] || []).map { |h| Hit.new(h, highlights, search) }
end

#
# Returns all highlights for this hit when called without parameters.
# When a field_name is provided, returns only the highlight for this field.
Expand All @@ -55,7 +56,7 @@ def highlight(field_name)
highlights(field_name).first
end

#
#
# Retrieve stored field value. For any attribute field configured with
# :stored => true, the Hit object will contain the stored value for
# that field. The value of this field will be typecast according to the
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading