Skip to content

Commit

Permalink
Merge pull request sunspot#470 from mikeauclair/solr_joins
Browse files Browse the repository at this point in the history
Solr joins
  • Loading branch information
njakobsen committed Jan 12, 2014
2 parents c40139e + f85032a commit 6e067fc
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 4 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,35 @@ Post.search do
end
```

### Joins

**Solr 4 and above**

Solr joins allow you to filter objects by joining on additional documents. More information can be found on the [Solr Wiki](http://wiki.apache.org/solr/Join).

```ruby
class Photo < ActiveRecord::Base
searchable do
text :caption, :default_boost => 1.5
time :created_at
integer :photo_container_id
end
end

class PhotoContainer < ActiveRecord::Base
searchable do
text :name
join(:caption, :type => :string, :join_string => 'from=photo_container_id to=id')
join(:photos_created, :type => :time, :join_string => 'from=photo_container_id to=id', :as => 'created_at_d')
end
end

PhotoContainer.search do
with(:caption).from_join('blah')
with(:photos_created).between(Date.new(2011,3,1), Date.new(2011,4,1))
end
```

### Highlighting

Highlighting allows you to display snippets of the part of the document
Expand Down
11 changes: 10 additions & 1 deletion sunspot/lib/sunspot/dsl/fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,17 @@ def boost(attr_name = nil, &block)
#
def method_missing(method, *args, &block)
options = Util.extract_options_from(args)
type_const_name = "#{Util.camel_case(method.to_s.sub(/^dynamic_/, ''))}Type"
if method.to_s == 'join'
type_string = options.delete(:type).to_s
else
type_string = method.to_s
end
type_const_name = "#{Util.camel_case(type_string.sub(/^dynamic_/, ''))}Type"
trie = options.delete(:trie)
type_const_name = "Trie#{type_const_name}" if trie
begin

type_class = options[:type]
type_class = Type.const_get(type_const_name)
rescue(NameError)
if trie
Expand All @@ -94,6 +101,8 @@ def method_missing(method, *args, &block)
else
super(method, *args, &block)
end
elsif method.to_s == 'join'
@setup.add_join_field_factory(name, type, options, &block)
else
@setup.add_field_factory(name, type, options, &block)
end
Expand Down
15 changes: 15 additions & 0 deletions sunspot/lib/sunspot/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,21 @@ def initialize(name, type, options = {})

end

class JoinField < Field #:nodoc:

def initialize(name, type, options = {})
@multiple = !!options.delete(:multiple)
super(name, type, options)
@join_string = options.delete(:join_string)
raise ArgumentError, "Unknown field option #{options.keys.first.inspect} provided for field #{name.inspect}" unless options.empty?
end

def local_params
"{!join #{@join_string}}"
end

end

class TypeField #:nodoc:
class <<self
def instance
Expand Down
33 changes: 33 additions & 0 deletions sunspot/lib/sunspot/field_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,39 @@ def signature
end
end

class Join < Abstract
def initialize(name, type, options = {}, &block)
super(name, options, &block)
unless name.to_s =~ /^\w+$/
raise ArgumentError, "Invalid field name #{name}: only letters, numbers, and underscores are allowed."
end
@field =
JoinField.new(name, type, options)
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. (noop here for joins)
#
def populate_document(document, model) #:nodoc:

end

#
# A unique signature identifying this field by name and type.
#
def signature
['join', @field.name, @field.type]
end
end

#
# DynamicFieldFactories create dynamic field instances based on dynamic
# configuration.
Expand Down
7 changes: 5 additions & 2 deletions sunspot/lib/sunspot/query/restriction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,14 @@ def to_params
# on whether this restriction is negated.
#
def to_boolean_phrase
phrase = []
phrase << @field.local_params if @field.respond_to? :local_params
unless negated?
to_positive_boolean_phrase
phrase << to_positive_boolean_phrase
else
to_negated_boolean_phrase
phrase << to_negated_boolean_phrase
end
phrase.join
end

#
Expand Down
6 changes: 6 additions & 0 deletions sunspot/lib/sunspot/setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ def add_field_factory(name, type, options = {}, &block)
end
end

def add_join_field_factory(name, type, options = {}, &block)
field_factory = FieldFactory::Join.new(name, type, options, &block)
@field_factories[field_factory.signature] = field_factory
@field_factories_cache[field_factory.name] = field_factory
end

#
# Add field_factories for fulltext search
#
Expand Down
5 changes: 5 additions & 0 deletions sunspot/spec/api/indexer/fixed_fields_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
connection.should have_add_with(:type => ['Post', 'SuperClass', 'MockRecord'])
end

it 'should not index join fields' do
session.index PhotoContainer.new
connection.should_not have_add_with(:photo_caption => 'blah')
end

it 'should index class name' do
session.index post
connection.should have_add_with(:class_name => 'Post')
Expand Down
19 changes: 19 additions & 0 deletions sunspot/spec/api/query/join_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require File.expand_path('spec_helper', File.dirname(__FILE__))

describe 'join' do
it 'should search by join' do
session.search PhotoContainer do
with(:caption, 'blah')
end
connection.should have_last_search_including(
:fq, "{!join from=photo_container_id to=id}caption_s:blah")
end

it 'should greater_than search by join' do
session.search PhotoContainer do
with(:photo_rating).greater_than(3)
end
connection.should have_last_search_including(
:fq, "{!join from=photo_container_id to=id}average_rating_ft:{3\\.0 TO *}")
end
end
15 changes: 14 additions & 1 deletion sunspot/spec/mocks/photo.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
class Photo < MockRecord
attr_accessor :caption, :lat, :lng, :size, :average_rating, :created_at
attr_accessor :caption, :lat, :lng, :size, :average_rating, :created_at, :post_id, :photo_container_id
end

Sunspot.setup(Photo) do
text :caption, :default_boost => 1.5
string :caption
integer :photo_container_id
boost 0.75
integer :size, :trie => true
float :average_rating, :trie => true
time :created_at, :trie => true
end

class PhotoContainer < MockRecord
def id
1
end
end

Sunspot.setup(PhotoContainer) do
join(:caption, :type => :string, :join_string => 'from=photo_container_id to=id')
join(:photo_rating, :type => :trie_float, :join_string => 'from=photo_container_id to=id', :as => 'average_rating_ft')
end

0 comments on commit 6e067fc

Please sign in to comment.