sphinxsearchlogic 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|