sphinxsearchlogic 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +161 -0
- data/VERSION.yml +4 -0
- data/init.rb +7 -0
- data/lib/rails_helpers.rb +45 -0
- data/lib/sphinxsearchlogic.rb +322 -0
- data/rails/init.rb +7 -0
- data/test/sphinxsearchlogic_test.rb +159 -0
- data/test/test_helper.rb +19 -0
- metadata +76 -0
data/CHANGELOG.rdoc
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
= Sphinxsearchlogic
|
2
|
+
|
3
|
+
Sphinxsearchlogic is for ThinkingSphinx what Searchlogic is for ActiveRecord.. or at least something similar.
|
4
|
+
|
5
|
+
== Helpful links
|
6
|
+
|
7
|
+
* <b>Github:</b> http://github.com/joost/sphinxsearchlogic
|
8
|
+
|
9
|
+
Sphinxsearchlogic is largely based on / using:
|
10
|
+
|
11
|
+
* <b>Searchlogic:</b> http://github.com/binarylogic/searchlogic/
|
12
|
+
* <b>ThinkingSphinx:</b> http://freelancing-god.github.com/ts/en/
|
13
|
+
* <b>Sphinx:</b> http://www.sphinxsearch.com/
|
14
|
+
|
15
|
+
If you're not familiar with ThinkingSphinx check {this presentation!}[http://www.slideshare.net/freelancing_god/solving-the-riddle-of-search-using-sphinx-with-rails-1406954]
|
16
|
+
|
17
|
+
== Install
|
18
|
+
|
19
|
+
First you need Sphinx and ThinkingSphinx. Simply because they rock. Check the ThinkingSphinx pages for this.
|
20
|
+
Use the gem in your environment.rb like:
|
21
|
+
|
22
|
+
config.gem "freelancing-god-thinking-sphinx", :lib => 'thinking_sphinx', :version => '1.2.7'
|
23
|
+
|
24
|
+
Install as gem from Github (recommended).
|
25
|
+
|
26
|
+
gem sources -a http://gems.github.com (you only have to do this once)
|
27
|
+
sudo gem install joost-sphinxsearchlogic
|
28
|
+
|
29
|
+
Next use it in your environment.rb like:
|
30
|
+
|
31
|
+
config.gem 'joost-sphinxsearchlogic', :lib => 'sphinxsearchlogic'
|
32
|
+
|
33
|
+
Install as plugin from Github.
|
34
|
+
|
35
|
+
./script/plugin install git://github.com/joost/sphinxsearchlogic.git
|
36
|
+
|
37
|
+
== Usage
|
38
|
+
|
39
|
+
Use Sphinxsearchlogic as you use the Searchlogic search method:
|
40
|
+
|
41
|
+
@search = Movie.sphinxsearchlogic(params[:search])
|
42
|
+
|
43
|
+
The search params you can pass:
|
44
|
+
|
45
|
+
Search:
|
46
|
+
|
47
|
+
:all => 'something' # search('something')
|
48
|
+
:name => 'john' # search(:conditions => {:name => 'john'})
|
49
|
+
|
50
|
+
Filters:
|
51
|
+
|
52
|
+
:with_age => 20 # search(:with => {:age => 20})
|
53
|
+
:with_age => [21, 22] # search(:with => {:age => [21, 22]})
|
54
|
+
:with_age => 20..25 # search(:with => {:age => 20..25})
|
55
|
+
|
56
|
+
:without_age => 20 # search(:without => {:age => 20})
|
57
|
+
|
58
|
+
For MVAs you can also use:
|
59
|
+
|
60
|
+
:with_all_tags => [1,2,3] # search(:with_all => {:tags => [1,2,3]})
|
61
|
+
|
62
|
+
Thinking Sphinx scopes:
|
63
|
+
:my_scope => true # my_scope (actually called with my_scope(true))
|
64
|
+
:some_scope => 'sweet' # some_scope(sweet)
|
65
|
+
|
66
|
+
=== Ordering
|
67
|
+
|
68
|
+
Ordering is implemented similar to Searchlogic.
|
69
|
+
|
70
|
+
:order => 'ascend_by_created_at' # :order => :attribute,
|
71
|
+
:order => 'descend_by_created_at' # :order => :attribute, :sort_mode => :desc
|
72
|
+
More advanced ordering? Use scopes! Like for {:order => 'rating DESC, votes DESC'} or {:sort_mode => :expr, :sort_by => '@weight * ranking'}
|
73
|
+
:order => 'my_order_scope'
|
74
|
+
|
75
|
+
For your views see the order helper below.
|
76
|
+
|
77
|
+
=== Pagination
|
78
|
+
|
79
|
+
Unsimilar to Searchlogic Sphinxsearchlogic does pagination in the search.
|
80
|
+
You can add them as follows since all arguments are merged.
|
81
|
+
|
82
|
+
@search = Movie.sphinxsearchlogic(params[:search], :page => params[:page], :per_page => params[:per_page])
|
83
|
+
|
84
|
+
If not specified default limits and pagination is used. As pagination is 'Always on' with ThinkingSphinx.
|
85
|
+
|
86
|
+
== Examples
|
87
|
+
|
88
|
+
=== Your controller
|
89
|
+
|
90
|
+
An example controller action:
|
91
|
+
|
92
|
+
class MovieController < ApplicationController
|
93
|
+
def index
|
94
|
+
@search = Movie.sphinxsearchlogic(params[:search], :page => params[:page], :per_page => params[:per_page])
|
95
|
+
@movies = @sphinxsearch.results
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
=== Your search forms
|
100
|
+
|
101
|
+
An example view search form:
|
102
|
+
|
103
|
+
<% sphinxsearchlogic_form_for @search do |form| %>
|
104
|
+
<p>
|
105
|
+
<%= form.label :all %>
|
106
|
+
<%= form.text_field :all %>
|
107
|
+
</p>
|
108
|
+
<p>
|
109
|
+
<%= form.check_box :scary_movies, {}, '1', nil %>
|
110
|
+
Only scary movies
|
111
|
+
</p>
|
112
|
+
<% end %>
|
113
|
+
|
114
|
+
The first field will send search[:all] params which will fulltext search through your data.
|
115
|
+
The second is making use of a ThinkingSphinx scope (http://freelancing-god.github.com/ts/en/scopes.html) so it
|
116
|
+
only works if you've defined it in your model.
|
117
|
+
|
118
|
+
=== Helpers
|
119
|
+
|
120
|
+
You can use a similar order helper as Searchlogic offers. You can order by attributes and fields (only if they
|
121
|
+
are specified as sortable in your ThinkingSphinx index).
|
122
|
+
|
123
|
+
<%= order(@search, :by => :title, :as => 'Movie Title')
|
124
|
+
|
125
|
+
When you create two ThinkingSphinx scopes in your model you can even do special exotic ordering.
|
126
|
+
|
127
|
+
sphinx_scope(:ascend_by_rating_and_votes) {
|
128
|
+
{:order => "rating ASC, votes ASC"}
|
129
|
+
}
|
130
|
+
|
131
|
+
sphinx_scope(:descend_by_rating_and_votes) {
|
132
|
+
{:order => "rating DESC, votes DESC"}
|
133
|
+
}
|
134
|
+
|
135
|
+
You can also use this in the helper:
|
136
|
+
|
137
|
+
<%= order(@search, :by => :rating_and_votes, :as => 'Special ordering')
|
138
|
+
|
139
|
+
== TODO
|
140
|
+
|
141
|
+
Things that might be in next versions. Please contact me via Github if you've any suggestions or want to
|
142
|
+
contribute.
|
143
|
+
|
144
|
+
=== Sanitize
|
145
|
+
|
146
|
+
Sanitize params so we don't f*ck with ThinkingSphinx.
|
147
|
+
|
148
|
+
=== Facets
|
149
|
+
|
150
|
+
Easy facets (http://freelancing-god.github.com/ts/en/facets.html) support.
|
151
|
+
|
152
|
+
=== Defaults
|
153
|
+
|
154
|
+
On the Model you want to search specify the defaults for the search. Eg. on the Movie model:
|
155
|
+
|
156
|
+
sphinxsearchlogic_default_order = 'descend_by_weight'
|
157
|
+
sphinxsearchlogic_protected = :order, :per_page, :age, :name, :match_mode
|
158
|
+
sphinxsearchlogic_max_per_page = 100
|
159
|
+
sphinxsearchlogic_match_mode = :any
|
160
|
+
|
161
|
+
Copyright (c) 2009 Joost Hietbrink, released under the MIT license
|
data/VERSION.yml
ADDED
data/init.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Sphinxsearchlogic
|
2
|
+
module RailsHelpers
|
3
|
+
|
4
|
+
# Creates a form with a :search scope. Use to create search form in your views.
|
5
|
+
def sphinxsearchlogic_form_for(*args, &block)
|
6
|
+
if search_obj = args.find { |arg| arg.is_a?(Sphinxsearchlogic::Search) }
|
7
|
+
options = args.extract_options!
|
8
|
+
options[:html] ||= {}
|
9
|
+
options[:html][:method] ||= :get
|
10
|
+
options[:url] ||= url_for
|
11
|
+
args.unshift(:search) if args.first == search_obj
|
12
|
+
args << options
|
13
|
+
end
|
14
|
+
form_for(*args, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Similar to the Searchlogic order helper.
|
18
|
+
def order(search, options = {}, html_options = {})
|
19
|
+
options[:params_scope] ||= :search
|
20
|
+
options[:as] ||= options[:by].to_s.humanize
|
21
|
+
options[:ascend_scope] ||= "ascend_by_#{options[:by]}"
|
22
|
+
options[:descend_scope] ||= "descend_by_#{options[:by]}"
|
23
|
+
ascending = search.order.to_s == options[:ascend_scope]
|
24
|
+
new_scope = ascending ? options[:descend_scope] : options[:ascend_scope]
|
25
|
+
selected = [options[:ascend_scope], options[:descend_scope]].include?(search.order.to_s)
|
26
|
+
if selected
|
27
|
+
css_classes = html_options[:class] ? html_options[:class].split(" ") : []
|
28
|
+
if ascending
|
29
|
+
options[:as] = "▲ #{options[:as]}"
|
30
|
+
css_classes << "ascending"
|
31
|
+
else
|
32
|
+
options[:as] = "▼ #{options[:as]}"
|
33
|
+
css_classes << "descending"
|
34
|
+
end
|
35
|
+
html_options[:class] = css_classes.join(" ")
|
36
|
+
end
|
37
|
+
params = controller.params.clone
|
38
|
+
params[options[:params_scope]] ||= {}
|
39
|
+
params[options[:params_scope]].merge!(:order => new_scope)
|
40
|
+
url_options = {:controller => params[:controller], :action => params[:action], options[:params_scope] => params[options[:params_scope]]}
|
41
|
+
link_to(options[:as], url_options, html_options)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,322 @@
|
|
1
|
+
# Sphinxsearchlogic
|
2
|
+
#
|
3
|
+
# A adapted version of the Searchlogic::Search class to attach to an ActiveRecord::Base#sphinxsearchlogic
|
4
|
+
# method.
|
5
|
+
module Sphinxsearchlogic
|
6
|
+
class Search
|
7
|
+
module Implementation
|
8
|
+
# Use like:
|
9
|
+
# Movie.sphinxsearchlogic(params[:search], :page => params[:page], :per_page => [:per_page])
|
10
|
+
def sphinxsearchlogic(conditions = {}, pagination = {})
|
11
|
+
conditions ||= {} # params[:search] might be nil
|
12
|
+
conditions.merge!(pagination)
|
13
|
+
# Merge array of hashes, but doesn't work if Hash value is an Array.
|
14
|
+
# conditions = Hash[*args.collect {|h| h.to_a}.flatten]
|
15
|
+
Search.new(self, scope(:find), conditions)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Is an invalid condition is used this error will be raised. Ex:
|
20
|
+
#
|
21
|
+
# User.search(:unkown => true)
|
22
|
+
#
|
23
|
+
# Where unknown is not a valid named scope for the User model.
|
24
|
+
class UnknownConditionError < StandardError
|
25
|
+
def initialize(condition)
|
26
|
+
msg = "The #{condition} is not a valid condition. You may only use conditions that map to a thinking sphinx named scope or attribute."
|
27
|
+
super(msg)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# class OutofboundsError < StandardError
|
32
|
+
# def initialize(page, per_page)
|
33
|
+
# msg = "The page #{page} is out of bounds. Using per_page #{per_page}."
|
34
|
+
# super(msg)
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
|
38
|
+
# Accessors that define this Search object.
|
39
|
+
attr_accessor :klass, :current_scope, :with, :without, :with_all, :params, :conditions, :scopes, :all
|
40
|
+
undef :id if respond_to?(:id)
|
41
|
+
|
42
|
+
module Pagination
|
43
|
+
attr_writer :page, :per_page, :max_matches
|
44
|
+
|
45
|
+
def default_per_page
|
46
|
+
default_per_page = 20
|
47
|
+
default_per_page = klass.per_page if klass.respond_to?(:per_page)
|
48
|
+
default_per_page
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a set max_matches or 1000.
|
52
|
+
def max_matches
|
53
|
+
@max_matches || 1000
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the last page we have in this search based on the max_matches.
|
57
|
+
# So we don't get a Riddle error for an offset that is too high.
|
58
|
+
def last_page
|
59
|
+
(max_matches.to_f / per_page).ceil
|
60
|
+
end
|
61
|
+
|
62
|
+
def offset
|
63
|
+
(page-1)*per_page
|
64
|
+
end
|
65
|
+
|
66
|
+
def page
|
67
|
+
page = (@page || 1).to_i
|
68
|
+
page = 1 if page < 1 # Fixes pages like -1 and 0.
|
69
|
+
# Fix riddle error.. we always return the last page? Think should be handled by application!
|
70
|
+
# However this isn't yet the case for pages > total_results.
|
71
|
+
# raise OutofboundsError.new(page, per_page) if page > last_page
|
72
|
+
page = last_page if page > last_page
|
73
|
+
page
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns 20 by default (ThinkingSphinx/Riddle default)
|
77
|
+
def per_page
|
78
|
+
per_page = (@per_page || default_per_page).to_i
|
79
|
+
per_page = default_per_page if per_page < 1 # Fixes per_page like -1 and 0.
|
80
|
+
per_page
|
81
|
+
end
|
82
|
+
|
83
|
+
def pagination_options
|
84
|
+
options = {}
|
85
|
+
options[:page] = page
|
86
|
+
options[:per_page] = per_page
|
87
|
+
options
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
include Pagination
|
92
|
+
|
93
|
+
module Ordering
|
94
|
+
|
95
|
+
attr_reader :order, :order_direction, :order_attribute
|
96
|
+
|
97
|
+
# Sets the order. If the order is incorrect this won't be set. Similar to pagination the
|
98
|
+
# defaults will be used.
|
99
|
+
def order=(order)
|
100
|
+
@order = order
|
101
|
+
return if order.blank?
|
102
|
+
if is_sphinx_scope?(order) # We first check for scopes since they might be named ascend_by_scopename.
|
103
|
+
scopes[order.to_sym] = true
|
104
|
+
elsif order.to_s =~ /^(ascend|descend)_by_(\w+)$/
|
105
|
+
@order_direction = ($1 == 'ascend') ? :asc : :desc
|
106
|
+
if is_sphinx_attribute?($2)
|
107
|
+
@order_attribute = $2.to_sym
|
108
|
+
elsif [:weight, :relevance, :rank, :id, :random, :geodist].include?($2.to_sym)
|
109
|
+
@order_attribute = "@#{$2} #{@order_direction}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def ordering_options
|
115
|
+
if order_attribute.blank?
|
116
|
+
{}
|
117
|
+
elsif order_attribute.is_a?(Symbol)
|
118
|
+
{
|
119
|
+
:order => order_attribute,
|
120
|
+
:sort_mode => order_direction
|
121
|
+
}
|
122
|
+
else
|
123
|
+
{:order => order_attribute}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
include Ordering
|
129
|
+
|
130
|
+
# Creates a new search object for the given class. Ex:
|
131
|
+
#
|
132
|
+
# Searchlogic::Search.new(User, {}, {:username_like => "bjohnson"})
|
133
|
+
def initialize(klass, current_scope, params = {})
|
134
|
+
@with = {}
|
135
|
+
@without = {}
|
136
|
+
@with_all = {}
|
137
|
+
@conditions = {}
|
138
|
+
@scopes = {}
|
139
|
+
|
140
|
+
self.klass = klass
|
141
|
+
raise "No Sphinx indexes found on #{klass.to_s}!" unless has_sphinx_index?
|
142
|
+
self.current_scope = current_scope
|
143
|
+
self.params = params if params.is_a?(Hash)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Accepts a hash of conditions.
|
147
|
+
def params=(values)
|
148
|
+
values.each do |param, value|
|
149
|
+
value.delete_if { |v| v.blank? } if value.is_a?(Array)
|
150
|
+
next if value.blank?
|
151
|
+
send("#{param}=", value)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns actual search results.
|
156
|
+
# Movie.sphinxsearchlogic.results
|
157
|
+
def results
|
158
|
+
Rails.logger.debug("Sphinxsearchlogic: #{klass.to_s}.search('#{all}', #{search_options.inspect})")
|
159
|
+
if scopes.empty?
|
160
|
+
klass.search(all, search_options)
|
161
|
+
else
|
162
|
+
cloned_scopes = scopes.clone # Clone scopes since we're deleting form the hash.
|
163
|
+
# Get the first scope and call all others on this one..
|
164
|
+
first_scope = cloned_scopes.keys.first
|
165
|
+
first_args = cloned_scopes.delete(first_scope)
|
166
|
+
result = klass.send(first_scope, first_args)
|
167
|
+
# Call remaining scopes on this scope.
|
168
|
+
cloned_scopes.each do |scope, args|
|
169
|
+
result = result.send(scope, args)
|
170
|
+
end
|
171
|
+
result.search(all, search_options)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# private
|
176
|
+
|
177
|
+
# Handles (in order):
|
178
|
+
# * with / without / with_all conditions (Filters)
|
179
|
+
# * field conditions (Regular searches)
|
180
|
+
# * scope conditions
|
181
|
+
def method_missing(name, *args, &block)
|
182
|
+
name = name.to_s
|
183
|
+
if name =~ /^(\w+)=$/ # If we have a setter
|
184
|
+
name = $1
|
185
|
+
if name =~ /^with(out|_all)?_(\w+)$/
|
186
|
+
attribute_name = $2.to_sym
|
187
|
+
if is_sphinx_attribute?(attribute_name)
|
188
|
+
# Put in with / without / with_all depending on what the regexp matched.
|
189
|
+
if $1 == 'out'
|
190
|
+
without[attribute_name] = type_cast(args.first, cast_type(attribute_name))
|
191
|
+
elsif $1 == '_all'
|
192
|
+
with_all[attribute_name] = type_cast(args.first, cast_type(attribute_name))
|
193
|
+
else
|
194
|
+
with[attribute_name] = type_cast(args.first, cast_type(attribute_name))
|
195
|
+
end
|
196
|
+
else
|
197
|
+
raise UnknownConditionError.new(attribute_name)
|
198
|
+
end
|
199
|
+
elsif is_sphinx_field?(name)
|
200
|
+
conditions[name.to_sym] = args.first
|
201
|
+
elsif is_sphinx_scope?(name)
|
202
|
+
scopes[name.to_sym] = args.first
|
203
|
+
else
|
204
|
+
# If we have an unknown setter..
|
205
|
+
# raise UnknownConditionError.new(attribute_name)
|
206
|
+
super
|
207
|
+
end
|
208
|
+
else
|
209
|
+
if name =~ /^with(out|_all)?_(\w+)$/
|
210
|
+
attribute_name = $2.to_sym
|
211
|
+
if is_sphinx_attribute?(attribute_name)
|
212
|
+
# Put in with / without / with_all depending on what the regexp matched.
|
213
|
+
if $1 == 'out'
|
214
|
+
without[attribute_name]
|
215
|
+
elsif $1 == '_all'
|
216
|
+
with_all[attribute_name]
|
217
|
+
else
|
218
|
+
with[attribute_name]
|
219
|
+
end
|
220
|
+
else
|
221
|
+
raise UnknownConditionError.new(attribute_name)
|
222
|
+
end
|
223
|
+
elsif is_sphinx_field?(name)
|
224
|
+
conditions[name.to_sym]
|
225
|
+
elsif is_sphinx_scope?(name)
|
226
|
+
scopes[name.to_sym]
|
227
|
+
else
|
228
|
+
# If we have something else than a setter..
|
229
|
+
# raise UnknownConditionError.new(attribute_name)
|
230
|
+
super
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Returns a hash for the ThinkingSphinx search method. Eg.
|
236
|
+
# {
|
237
|
+
# :with => {:year => 2001}
|
238
|
+
# }
|
239
|
+
def attribute_filter_options
|
240
|
+
options = {}
|
241
|
+
options[:with] = with unless with.blank?
|
242
|
+
options[:without] = without unless without.blank?
|
243
|
+
options[:with_all] = with_all unless with_all.blank? # See http://www.mailinglistarchive.com/thinking-sphinx@googlegroups.com/msg00351.html
|
244
|
+
options
|
245
|
+
end
|
246
|
+
|
247
|
+
# Returns a hash for the ThinkingSphinx search method. Eg.
|
248
|
+
# {
|
249
|
+
# :conditions => {:name => 'John'}
|
250
|
+
# }
|
251
|
+
def search_options
|
252
|
+
options = {}
|
253
|
+
options[:conditions] = conditions unless conditions.blank?
|
254
|
+
options.merge(attribute_filter_options).merge(ordering_options).merge(pagination_options)
|
255
|
+
end
|
256
|
+
|
257
|
+
# # cleanup_hash removes empty and nil stuff from params hashes.
|
258
|
+
# def cleanup_hash(hash)
|
259
|
+
# hash.collect do |condition, value|
|
260
|
+
# value.delete_if { |v| v.blank? } if value.is_a?(Array)
|
261
|
+
# value unless value.blank?
|
262
|
+
# end.compact
|
263
|
+
# end
|
264
|
+
|
265
|
+
# Returns the ThinkingSphinx index for the klass we search on.
|
266
|
+
def sphinx_index
|
267
|
+
klass.sphinx_indexes.first
|
268
|
+
end
|
269
|
+
|
270
|
+
# Returns true if the class of this Search has a Sphinx index.
|
271
|
+
def has_sphinx_index?
|
272
|
+
sphinx_index.is_a?(ThinkingSphinx::Index)
|
273
|
+
rescue
|
274
|
+
false
|
275
|
+
end
|
276
|
+
|
277
|
+
# Returns particular ThinkingSphinx::Attribute.
|
278
|
+
def sphinx_attribute(attribute_name)
|
279
|
+
sphinx_index.attributes.find do |index_attribute|
|
280
|
+
index_attribute.public? && index_attribute.unique_name.to_s =~ /^#{attribute_name}(_sort)?$/ # Also check for :sortable attributes (they are given prefix _sort)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Returns true if the class of this search has a public attribute with this name (or name_sort if field is :sortable).
|
285
|
+
def is_sphinx_attribute?(attribute_name)
|
286
|
+
!!sphinx_attribute(attribute_name)
|
287
|
+
end
|
288
|
+
|
289
|
+
# Returns true if the class of this search has a public field with this name.
|
290
|
+
def is_sphinx_field?(field_name)
|
291
|
+
!sphinx_index.fields.find do |index_field|
|
292
|
+
index_field.public? && (index_field.unique_name.to_s == field_name.to_s)
|
293
|
+
end.nil?
|
294
|
+
end
|
295
|
+
|
296
|
+
# Returns true if class of this search has a sphinx scope with this name.
|
297
|
+
def is_sphinx_scope?(scope_name)
|
298
|
+
klass.sphinx_scopes.include?(scope_name.to_sym)
|
299
|
+
end
|
300
|
+
|
301
|
+
# Returns the type we should type_cast a ThinkingSphinx::Attribute to, eg. :integer.
|
302
|
+
def cast_type(name)
|
303
|
+
sphinx_attribute(name).type
|
304
|
+
end
|
305
|
+
|
306
|
+
# type_cast method of Searchlogic plugin
|
307
|
+
def type_cast(value, type)
|
308
|
+
case value
|
309
|
+
when Array
|
310
|
+
value.collect { |v| type_cast(v, type) }
|
311
|
+
else
|
312
|
+
# Let's leverage ActiveRecord's type casting, so that casting is consistent
|
313
|
+
# with the other models.
|
314
|
+
column_for_type_cast = ::ActiveRecord::ConnectionAdapters::Column.new("", nil)
|
315
|
+
column_for_type_cast.instance_variable_set(:@type, type)
|
316
|
+
value = column_for_type_cast.type_cast(value)
|
317
|
+
Time.zone && value.is_a?(Time) ? value.in_time_zone : value
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
end
|
322
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
# Define classes to test on..
|
4
|
+
class Book < ActiveRecord::Base
|
5
|
+
|
6
|
+
establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
7
|
+
|
8
|
+
define_index do
|
9
|
+
indexes title, :sortable => true
|
10
|
+
indexes description
|
11
|
+
has :production_year
|
12
|
+
end
|
13
|
+
|
14
|
+
sphinx_scope(:millenium) do
|
15
|
+
{:with => {:production_year => 2000..9999}}
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
class SphinxsearchlogicTest < ActiveSupport::TestCase
|
21
|
+
|
22
|
+
def setup
|
23
|
+
end
|
24
|
+
|
25
|
+
# General
|
26
|
+
|
27
|
+
test 'book model should have sphinx index' do
|
28
|
+
assert Book.sphinxsearchlogic.send(:has_sphinx_index?)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Attribute Filters
|
32
|
+
|
33
|
+
test 'valid with params' do
|
34
|
+
search = Book.sphinxsearchlogic(:with_production_year => '2006')
|
35
|
+
assert_equal({:production_year => '2006'}, search.with)
|
36
|
+
end
|
37
|
+
|
38
|
+
test 'invalid with params' do
|
39
|
+
assert_raise Sphinxsearchlogic::Search::UnknownConditionError do
|
40
|
+
search = Book.sphinxsearchlogic(:with_non_existing_stuff => '2006')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
test 'valid without params' do
|
45
|
+
search = Book.sphinxsearchlogic(:without_production_year => '2006')
|
46
|
+
assert_equal({:production_year => '2006'}, search.without)
|
47
|
+
end
|
48
|
+
|
49
|
+
test 'invalid without params' do
|
50
|
+
assert_raise Sphinxsearchlogic::Search::UnknownConditionError do
|
51
|
+
search = Book.sphinxsearchlogic(:without_non_existing_stuff => '2006')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Regular field and scope searches
|
56
|
+
|
57
|
+
test 'empty search' do
|
58
|
+
search = Book.sphinxsearchlogic()
|
59
|
+
assert_equal({}, search.search_options)
|
60
|
+
end
|
61
|
+
|
62
|
+
test 'search on all' do
|
63
|
+
search = Book.sphinxsearchlogic(:all => 'test')
|
64
|
+
assert_equal('test', search.all)
|
65
|
+
end
|
66
|
+
|
67
|
+
test 'search on field' do
|
68
|
+
search = Book.sphinxsearchlogic(:title => 'test')
|
69
|
+
assert_equal('test', search.title)
|
70
|
+
assert_equal({:conditions => {:title => 'test'}}, search.search_options)
|
71
|
+
end
|
72
|
+
|
73
|
+
test 'scope search' do
|
74
|
+
search = Book.sphinxsearchlogic(:millenium => true)
|
75
|
+
assert_equal(true, search.millenium)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Ordering
|
79
|
+
|
80
|
+
test 'invalid ordering by non existing scope' do
|
81
|
+
search = Book.sphinxsearchlogic(:order => 'non_existing_scope')
|
82
|
+
assert_equal({}, search.ordering_options)
|
83
|
+
end
|
84
|
+
|
85
|
+
test 'invalid ordering by non sortable field' do
|
86
|
+
search = Book.sphinxsearchlogic(:order => 'ascend_by_description')
|
87
|
+
assert_equal({}, search.ordering_options)
|
88
|
+
end
|
89
|
+
|
90
|
+
test 'ascend ordering by sortable field' do
|
91
|
+
search = Book.sphinxsearchlogic(:order => 'ascend_by_title')
|
92
|
+
assert_equal({:order => :title, :sort_mode => :asc}, search.ordering_options)
|
93
|
+
end
|
94
|
+
|
95
|
+
test 'descend ordering by sortable field' do
|
96
|
+
search = Book.sphinxsearchlogic(:order => 'descend_by_title')
|
97
|
+
assert_equal({:order => :title, :sort_mode => :desc}, search.ordering_options)
|
98
|
+
end
|
99
|
+
|
100
|
+
test 'ascend ordering by attribute' do
|
101
|
+
search = Book.sphinxsearchlogic(:order => 'ascend_by_production_year')
|
102
|
+
assert_equal({:order => :production_year, :sort_mode => :asc}, search.ordering_options)
|
103
|
+
end
|
104
|
+
|
105
|
+
test 'descend ordering by @relevance' do
|
106
|
+
search = Book.sphinxsearchlogic(:order => 'descend_by_relevance')
|
107
|
+
assert_equal({:order => '@relevance desc'}, search.ordering_options)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Pagination
|
111
|
+
|
112
|
+
test 'pagination' do
|
113
|
+
search = Book.sphinxsearchlogic(:per_page => '123', :page => '12')
|
114
|
+
assert_equal(12, search.page)
|
115
|
+
assert_equal(123, search.per_page)
|
116
|
+
# We also check if the search_options are correctly set..
|
117
|
+
assert_equal(12, search.search_options[:page])
|
118
|
+
assert_equal(123, search.search_options[:per_page])
|
119
|
+
end
|
120
|
+
|
121
|
+
test 'invalid pagination' do
|
122
|
+
search = Book.sphinxsearchlogic(:per_page => 'asdasd', :page => 'as')
|
123
|
+
assert_equal(1, search.page)
|
124
|
+
assert_equal(10, search.per_page)
|
125
|
+
|
126
|
+
search = Book.sphinxsearchlogic(:per_page => '0', :page => '0')
|
127
|
+
assert_equal(1, search.page)
|
128
|
+
assert_equal(10, search.per_page)
|
129
|
+
|
130
|
+
search = Book.sphinxsearchlogic(:per_page => '-123', :page => '-12')
|
131
|
+
assert_equal(1, search.page)
|
132
|
+
assert_equal(10, search.per_page)
|
133
|
+
end
|
134
|
+
|
135
|
+
test 'no pagination' do
|
136
|
+
search = Book.sphinxsearchlogic(:per_page => '', :page => '')
|
137
|
+
assert_equal(nil, search.page)
|
138
|
+
assert_equal(nil, search.per_page)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Other methods
|
142
|
+
|
143
|
+
test 'attribute finding' do
|
144
|
+
assert Book.sphinxsearchlogic.is_sphinx_attribute?('title') # sortable so attribute
|
145
|
+
assert !Book.sphinxsearchlogic.is_sphinx_attribute?('notitle') # not existing
|
146
|
+
assert !Book.sphinxsearchlogic.is_sphinx_attribute?('description') # attribute
|
147
|
+
end
|
148
|
+
|
149
|
+
test 'field finding' do
|
150
|
+
assert Book.sphinxsearchlogic.is_sphinx_field?('title') # but also field
|
151
|
+
assert Book.sphinxsearchlogic.is_sphinx_field?('description')
|
152
|
+
assert !Book.sphinxsearchlogic.is_sphinx_field?('production_year')
|
153
|
+
end
|
154
|
+
|
155
|
+
test 'scope finding' do
|
156
|
+
assert Book.sphinxsearchlogic.is_sphinx_scope?('millenium')
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/test_case'
|
4
|
+
|
5
|
+
# Create some test db stuff..
|
6
|
+
require 'activerecord'
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
9
|
+
ActiveRecord::Base.configurations = true
|
10
|
+
|
11
|
+
ActiveRecord::Schema.verbose = false
|
12
|
+
ActiveRecord::Schema.define(:version => 1) do
|
13
|
+
create_table :books do |t|
|
14
|
+
t.string :title
|
15
|
+
t.string :description
|
16
|
+
t.integer :production_year
|
17
|
+
t.timestamps
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sphinxsearchlogic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joost Hietbrink
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-08-25 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.0.0
|
24
|
+
version:
|
25
|
+
description: Searchlogic provides common named scopes and object based searching for ActiveRecord.
|
26
|
+
email: joost@joopp.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- MIT-LICENSE
|
33
|
+
- README.rdoc
|
34
|
+
- CHANGELOG.rdoc
|
35
|
+
files:
|
36
|
+
- CHANGELOG.rdoc
|
37
|
+
- MIT-LICENSE
|
38
|
+
- README.rdoc
|
39
|
+
- VERSION.yml
|
40
|
+
- init.rb
|
41
|
+
- lib/sphinxsearchlogic.rb
|
42
|
+
- lib/rails_helpers.rb
|
43
|
+
- rails/init.rb
|
44
|
+
- test/sphinxsearchlogic_test.rb
|
45
|
+
- test/test_helper.rb
|
46
|
+
has_rdoc: true
|
47
|
+
homepage: http://github.com/joost/sphinxsearchlogic
|
48
|
+
licenses: []
|
49
|
+
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options:
|
52
|
+
- --charset=UTF-8
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: "0"
|
66
|
+
version:
|
67
|
+
requirements: []
|
68
|
+
|
69
|
+
rubyforge_project: sphinxsearchlogic
|
70
|
+
rubygems_version: 1.3.5
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: Sphinxsearchlogic is for ThinkingSphinx what Searchlogic is for ActiveRecord.. or at least something similar.
|
74
|
+
test_files:
|
75
|
+
- test/sphinxsearchlogic_test.rb
|
76
|
+
- test/test_helper.rb
|