skalee-thinking-sphinx 1.3.14.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/LICENCE +20 -0
- data/README.textile +201 -0
- data/Rakefile +3 -0
- data/VERSION +1 -0
- data/contribute.rb +385 -0
- data/cucumber.yml +1 -0
- data/features/abstract_inheritance.feature +10 -0
- data/features/alternate_primary_key.feature +27 -0
- data/features/attribute_transformation.feature +22 -0
- data/features/attribute_updates.feature +51 -0
- data/features/deleting_instances.feature +67 -0
- data/features/direct_attributes.feature +11 -0
- data/features/excerpts.feature +13 -0
- data/features/extensible_delta_indexing.feature +9 -0
- data/features/facets.feature +82 -0
- data/features/facets_across_model.feature +29 -0
- data/features/handling_edits.feature +92 -0
- data/features/retry_stale_indexes.feature +24 -0
- data/features/searching_across_models.feature +20 -0
- data/features/searching_by_index.feature +40 -0
- data/features/searching_by_model.feature +175 -0
- data/features/searching_with_find_arguments.feature +56 -0
- data/features/sphinx_detection.feature +25 -0
- data/features/sphinx_scopes.feature +42 -0
- data/features/step_definitions/alpha_steps.rb +16 -0
- data/features/step_definitions/beta_steps.rb +7 -0
- data/features/step_definitions/common_steps.rb +188 -0
- data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
- data/features/step_definitions/facet_steps.rb +96 -0
- data/features/step_definitions/find_arguments_steps.rb +36 -0
- data/features/step_definitions/gamma_steps.rb +15 -0
- data/features/step_definitions/scope_steps.rb +15 -0
- data/features/step_definitions/search_steps.rb +89 -0
- data/features/step_definitions/sphinx_steps.rb +35 -0
- data/features/sti_searching.feature +19 -0
- data/features/support/database.example.yml +3 -0
- data/features/support/db/.gitignore +1 -0
- data/features/support/db/fixtures/alphas.rb +10 -0
- data/features/support/db/fixtures/authors.rb +1 -0
- data/features/support/db/fixtures/betas.rb +10 -0
- data/features/support/db/fixtures/boxes.rb +9 -0
- data/features/support/db/fixtures/categories.rb +1 -0
- data/features/support/db/fixtures/cats.rb +3 -0
- data/features/support/db/fixtures/comments.rb +24 -0
- data/features/support/db/fixtures/developers.rb +29 -0
- data/features/support/db/fixtures/dogs.rb +3 -0
- data/features/support/db/fixtures/extensible_betas.rb +10 -0
- data/features/support/db/fixtures/foxes.rb +3 -0
- data/features/support/db/fixtures/gammas.rb +10 -0
- data/features/support/db/fixtures/music.rb +4 -0
- data/features/support/db/fixtures/people.rb +1001 -0
- data/features/support/db/fixtures/posts.rb +6 -0
- data/features/support/db/fixtures/robots.rb +14 -0
- data/features/support/db/fixtures/tags.rb +27 -0
- data/features/support/db/migrations/create_alphas.rb +8 -0
- data/features/support/db/migrations/create_animals.rb +5 -0
- data/features/support/db/migrations/create_authors.rb +3 -0
- data/features/support/db/migrations/create_authors_posts.rb +6 -0
- data/features/support/db/migrations/create_betas.rb +5 -0
- data/features/support/db/migrations/create_boxes.rb +5 -0
- data/features/support/db/migrations/create_categories.rb +3 -0
- data/features/support/db/migrations/create_comments.rb +10 -0
- data/features/support/db/migrations/create_developers.rb +9 -0
- data/features/support/db/migrations/create_extensible_betas.rb +5 -0
- data/features/support/db/migrations/create_gammas.rb +3 -0
- data/features/support/db/migrations/create_genres.rb +3 -0
- data/features/support/db/migrations/create_music.rb +6 -0
- data/features/support/db/migrations/create_people.rb +13 -0
- data/features/support/db/migrations/create_posts.rb +5 -0
- data/features/support/db/migrations/create_robots.rb +4 -0
- data/features/support/db/migrations/create_taggings.rb +5 -0
- data/features/support/db/migrations/create_tags.rb +4 -0
- data/features/support/env.rb +21 -0
- data/features/support/lib/generic_delta_handler.rb +8 -0
- data/features/support/models/alpha.rb +22 -0
- data/features/support/models/animal.rb +5 -0
- data/features/support/models/author.rb +3 -0
- data/features/support/models/beta.rb +8 -0
- data/features/support/models/box.rb +8 -0
- data/features/support/models/cat.rb +3 -0
- data/features/support/models/category.rb +4 -0
- data/features/support/models/comment.rb +10 -0
- data/features/support/models/developer.rb +16 -0
- data/features/support/models/dog.rb +3 -0
- data/features/support/models/extensible_beta.rb +9 -0
- data/features/support/models/fox.rb +5 -0
- data/features/support/models/gamma.rb +5 -0
- data/features/support/models/genre.rb +3 -0
- data/features/support/models/medium.rb +5 -0
- data/features/support/models/music.rb +8 -0
- data/features/support/models/person.rb +23 -0
- data/features/support/models/post.rb +21 -0
- data/features/support/models/robot.rb +12 -0
- data/features/support/models/tag.rb +3 -0
- data/features/support/models/tagging.rb +4 -0
- data/ginger_scenarios.rb +28 -0
- data/init.rb +5 -0
- data/install.rb +5 -0
- data/lib/cucumber/thinking_sphinx/external_world.rb +8 -0
- data/lib/cucumber/thinking_sphinx/internal_world.rb +126 -0
- data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
- data/lib/thinking_sphinx/active_record/attribute_updates.rb +19 -0
- data/lib/thinking_sphinx/active_record/delta.rb +47 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
- data/lib/thinking_sphinx/active_record/scopes.rb +75 -0
- data/lib/thinking_sphinx/active_record.rb +348 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +143 -0
- data/lib/thinking_sphinx/association.rb +164 -0
- data/lib/thinking_sphinx/attribute.rb +362 -0
- data/lib/thinking_sphinx/auto_version.rb +22 -0
- data/lib/thinking_sphinx/class_facet.rb +15 -0
- data/lib/thinking_sphinx/configuration.rb +300 -0
- data/lib/thinking_sphinx/context.rb +68 -0
- data/lib/thinking_sphinx/core/array.rb +7 -0
- data/lib/thinking_sphinx/core/string.rb +15 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
- data/lib/thinking_sphinx/deltas.rb +28 -0
- data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
- data/lib/thinking_sphinx/excerpter.rb +22 -0
- data/lib/thinking_sphinx/facet.rb +125 -0
- data/lib/thinking_sphinx/facet_search.rb +136 -0
- data/lib/thinking_sphinx/field.rb +82 -0
- data/lib/thinking_sphinx/index/builder.rb +296 -0
- data/lib/thinking_sphinx/index/faux_column.rb +110 -0
- data/lib/thinking_sphinx/index.rb +157 -0
- data/lib/thinking_sphinx/property.rb +162 -0
- data/lib/thinking_sphinx/rails_additions.rb +150 -0
- data/lib/thinking_sphinx/search.rb +769 -0
- data/lib/thinking_sphinx/search_methods.rb +439 -0
- data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
- data/lib/thinking_sphinx/source/sql.rb +130 -0
- data/lib/thinking_sphinx/source.rb +153 -0
- data/lib/thinking_sphinx/tasks.rb +131 -0
- data/lib/thinking_sphinx/test.rb +52 -0
- data/lib/thinking_sphinx.rb +225 -0
- data/rails/init.rb +16 -0
- data/recipes/thinking_sphinx.rb +3 -0
- data/spec/fixtures/data.sql +32 -0
- data/spec/fixtures/database.yml.default +3 -0
- data/spec/fixtures/models.rb +145 -0
- data/spec/fixtures/structure.sql +125 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/sphinx_helper.rb +81 -0
- data/spec/thinking_sphinx/active_record/delta_spec.rb +128 -0
- data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +55 -0
- data/spec/thinking_sphinx/active_record/scopes_spec.rb +177 -0
- data/spec/thinking_sphinx/active_record_spec.rb +622 -0
- data/spec/thinking_sphinx/association_spec.rb +239 -0
- data/spec/thinking_sphinx/attribute_spec.rb +570 -0
- data/spec/thinking_sphinx/auto_version_spec.rb +39 -0
- data/spec/thinking_sphinx/configuration_spec.rb +234 -0
- data/spec/thinking_sphinx/context_spec.rb +119 -0
- data/spec/thinking_sphinx/core/array_spec.rb +9 -0
- data/spec/thinking_sphinx/core/string_spec.rb +9 -0
- data/spec/thinking_sphinx/excerpter_spec.rb +57 -0
- data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
- data/spec/thinking_sphinx/facet_spec.rb +333 -0
- data/spec/thinking_sphinx/field_spec.rb +154 -0
- data/spec/thinking_sphinx/index/builder_spec.rb +479 -0
- data/spec/thinking_sphinx/index/faux_column_spec.rb +30 -0
- data/spec/thinking_sphinx/index_spec.rb +183 -0
- data/spec/thinking_sphinx/rails_additions_spec.rb +203 -0
- data/spec/thinking_sphinx/search_methods_spec.rb +152 -0
- data/spec/thinking_sphinx/search_spec.rb +1181 -0
- data/spec/thinking_sphinx/source_spec.rb +235 -0
- data/spec/thinking_sphinx_spec.rb +204 -0
- data/tasks/distribution.rb +41 -0
- data/tasks/rails.rake +1 -0
- data/tasks/testing.rb +72 -0
- data/vendor/after_commit/.gitignore +1 -0
- data/vendor/after_commit/lib/after_commit/active_record.rb +122 -0
- data/vendor/after_commit/lib/after_commit/connection_adapters.rb +168 -0
- data/vendor/after_commit/lib/after_commit/test_bypass.rb +30 -0
- data/vendor/after_commit/lib/after_commit.rb +70 -0
- data/vendor/riddle/lib/riddle/0.9.8.rb +1 -0
- data/vendor/riddle/lib/riddle/0.9.9/client/filter.rb +22 -0
- data/vendor/riddle/lib/riddle/0.9.9/client.rb +49 -0
- data/vendor/riddle/lib/riddle/0.9.9/configuration/searchd.rb +28 -0
- data/vendor/riddle/lib/riddle/0.9.9.rb +7 -0
- data/vendor/riddle/lib/riddle/auto_version.rb +11 -0
- data/vendor/riddle/lib/riddle/client/filter.rb +62 -0
- data/vendor/riddle/lib/riddle/client/message.rb +70 -0
- data/vendor/riddle/lib/riddle/client/response.rb +94 -0
- data/vendor/riddle/lib/riddle/client.rb +745 -0
- data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +49 -0
- data/vendor/riddle/lib/riddle/configuration/index.rb +149 -0
- data/vendor/riddle/lib/riddle/configuration/indexer.rb +20 -0
- data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
- data/vendor/riddle/lib/riddle/configuration/searchd.rb +28 -0
- data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
- data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
- data/vendor/riddle/lib/riddle/configuration/sql_source.rb +53 -0
- data/vendor/riddle/lib/riddle/configuration/xml_source.rb +29 -0
- data/vendor/riddle/lib/riddle/configuration.rb +33 -0
- data/vendor/riddle/lib/riddle/controller.rb +78 -0
- data/vendor/riddle/lib/riddle.rb +51 -0
- metadata +312 -0
@@ -0,0 +1,769 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module ThinkingSphinx
|
3
|
+
# Once you've got those indexes in and built, this is the stuff that
|
4
|
+
# matters - how to search! This class provides a generic search
|
5
|
+
# interface - which you can use to search all your indexed models at once.
|
6
|
+
# Most times, you will just want a specific model's results - to search and
|
7
|
+
# search_for_ids methods will do the job in exactly the same manner when
|
8
|
+
# called from a model.
|
9
|
+
#
|
10
|
+
class Search
|
11
|
+
CoreMethods = %w( == class class_eval extend frozen? id instance_eval
|
12
|
+
instance_of? instance_values instance_variable_defined?
|
13
|
+
instance_variable_get instance_variable_set instance_variables is_a?
|
14
|
+
kind_of? member? method methods nil? object_id respond_to? send should
|
15
|
+
type )
|
16
|
+
SafeMethods = %w( partition private_methods protected_methods
|
17
|
+
public_methods send class )
|
18
|
+
|
19
|
+
instance_methods.select { |method|
|
20
|
+
method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
|
21
|
+
}.each { |method|
|
22
|
+
undef_method method
|
23
|
+
}
|
24
|
+
|
25
|
+
HashOptions = [:conditions, :with, :without, :with_all]
|
26
|
+
ArrayOptions = [:classes, :without_ids]
|
27
|
+
|
28
|
+
attr_reader :args, :options
|
29
|
+
|
30
|
+
# Deprecated. Use ThinkingSphinx.search
|
31
|
+
def self.search(*args)
|
32
|
+
log 'ThinkingSphinx::Search.search is deprecated. Please use ThinkingSphinx.search instead.'
|
33
|
+
ThinkingSphinx.search *args
|
34
|
+
end
|
35
|
+
|
36
|
+
# Deprecated. Use ThinkingSphinx.search_for_ids
|
37
|
+
def self.search_for_ids(*args)
|
38
|
+
log 'ThinkingSphinx::Search.search_for_ids is deprecated. Please use ThinkingSphinx.search_for_ids instead.'
|
39
|
+
ThinkingSphinx.search_for_ids *args
|
40
|
+
end
|
41
|
+
|
42
|
+
# Deprecated. Use ThinkingSphinx.search_for_ids
|
43
|
+
def self.search_for_id(*args)
|
44
|
+
log 'ThinkingSphinx::Search.search_for_id is deprecated. Please use ThinkingSphinx.search_for_id instead.'
|
45
|
+
ThinkingSphinx.search_for_id *args
|
46
|
+
end
|
47
|
+
|
48
|
+
# Deprecated. Use ThinkingSphinx.count
|
49
|
+
def self.count(*args)
|
50
|
+
log 'ThinkingSphinx::Search.count is deprecated. Please use ThinkingSphinx.count instead.'
|
51
|
+
ThinkingSphinx.count *args
|
52
|
+
end
|
53
|
+
|
54
|
+
# Deprecated. Use ThinkingSphinx.facets
|
55
|
+
def self.facets(*args)
|
56
|
+
log 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
|
57
|
+
ThinkingSphinx.facets *args
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.matching_fields(fields, bitmask)
|
61
|
+
matches = []
|
62
|
+
bitstring = bitmask.to_s(2).rjust(32, '0').reverse
|
63
|
+
|
64
|
+
fields.each_with_index do |field, index|
|
65
|
+
matches << field if bitstring[index, 1] == '1'
|
66
|
+
end
|
67
|
+
matches
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(*args)
|
71
|
+
ThinkingSphinx.context.define_indexes
|
72
|
+
|
73
|
+
@array = []
|
74
|
+
@options = args.extract_options!
|
75
|
+
@args = args
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_a
|
79
|
+
populate
|
80
|
+
@array
|
81
|
+
end
|
82
|
+
|
83
|
+
# Indication of whether the request has been made to Sphinx for the search
|
84
|
+
# query.
|
85
|
+
#
|
86
|
+
# @return [Boolean] true if the results have been requested.
|
87
|
+
#
|
88
|
+
def populated?
|
89
|
+
!!@populated
|
90
|
+
end
|
91
|
+
|
92
|
+
# The query result hash from Riddle.
|
93
|
+
#
|
94
|
+
# @return [Hash] Raw Sphinx results
|
95
|
+
#
|
96
|
+
def results
|
97
|
+
populate
|
98
|
+
@results
|
99
|
+
end
|
100
|
+
|
101
|
+
def method_missing(method, *args, &block)
|
102
|
+
if is_scope?(method)
|
103
|
+
add_scope(method, *args, &block)
|
104
|
+
return self
|
105
|
+
elsif method == :search_count
|
106
|
+
return scoped_count
|
107
|
+
elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
|
108
|
+
super
|
109
|
+
elsif !SafeMethods.include?(method.to_s)
|
110
|
+
populate
|
111
|
+
end
|
112
|
+
|
113
|
+
if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
|
114
|
+
each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
|
115
|
+
else
|
116
|
+
@array.send(method, *args, &block)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns true if the Search object or the underlying Array object respond
|
121
|
+
# to the requested method.
|
122
|
+
#
|
123
|
+
# @param [Symbol] method The method name
|
124
|
+
# @return [Boolean] true if either Search or Array responds to the method.
|
125
|
+
#
|
126
|
+
def respond_to?(method)
|
127
|
+
super || @array.respond_to?(method)
|
128
|
+
end
|
129
|
+
|
130
|
+
# The current page number of the result set. Defaults to 1 if no page was
|
131
|
+
# explicitly requested.
|
132
|
+
#
|
133
|
+
# @return [Integer]
|
134
|
+
#
|
135
|
+
def current_page
|
136
|
+
@options[:page].blank? ? 1 : @options[:page].to_i
|
137
|
+
end
|
138
|
+
|
139
|
+
# The next page number of the result set. If there are no more pages
|
140
|
+
# available, nil is returned.
|
141
|
+
#
|
142
|
+
# @return [Integer, nil]
|
143
|
+
#
|
144
|
+
def next_page
|
145
|
+
current_page >= total_pages ? nil : current_page + 1
|
146
|
+
end
|
147
|
+
|
148
|
+
# The previous page number of the result set. If this is the first page,
|
149
|
+
# then nil is returned.
|
150
|
+
#
|
151
|
+
# @return [Integer, nil]
|
152
|
+
#
|
153
|
+
def previous_page
|
154
|
+
current_page == 1 ? nil : current_page - 1
|
155
|
+
end
|
156
|
+
|
157
|
+
# The amount of records per set of paged results. Defaults to 20 unless a
|
158
|
+
# specific page size is requested.
|
159
|
+
#
|
160
|
+
# @return [Integer]
|
161
|
+
#
|
162
|
+
def per_page
|
163
|
+
@options[:limit] ||= @options[:per_page]
|
164
|
+
@options[:limit] ||= 20
|
165
|
+
@options[:limit].to_i
|
166
|
+
end
|
167
|
+
|
168
|
+
# The total number of pages available if the results are paginated.
|
169
|
+
#
|
170
|
+
# @return [Integer]
|
171
|
+
#
|
172
|
+
def total_pages
|
173
|
+
populate
|
174
|
+
return 0 if @results[:total].nil?
|
175
|
+
|
176
|
+
@total_pages ||= (@results[:total] / per_page.to_f).ceil
|
177
|
+
end
|
178
|
+
# Compatibility with older versions of will_paginate
|
179
|
+
alias_method :page_count, :total_pages
|
180
|
+
|
181
|
+
# The total number of search results available.
|
182
|
+
#
|
183
|
+
# @return [Integer]
|
184
|
+
#
|
185
|
+
def total_entries
|
186
|
+
populate
|
187
|
+
return 0 if @results[:total_found].nil?
|
188
|
+
|
189
|
+
@total_entries ||= @results[:total_found]
|
190
|
+
end
|
191
|
+
|
192
|
+
# The current page's offset, based on the number of records per page.
|
193
|
+
# Or explicit :offset if given.
|
194
|
+
#
|
195
|
+
# @return [Integer]
|
196
|
+
#
|
197
|
+
def offset
|
198
|
+
@options[:offset] || ((current_page - 1) * per_page)
|
199
|
+
end
|
200
|
+
|
201
|
+
def indexes
|
202
|
+
return options[:index] if options[:index]
|
203
|
+
return '*' if classes.empty?
|
204
|
+
|
205
|
+
classes.collect { |klass| klass.sphinx_index_names }.flatten.join(',')
|
206
|
+
end
|
207
|
+
|
208
|
+
def each_with_groupby_and_count(&block)
|
209
|
+
populate
|
210
|
+
results[:matches].each_with_index do |match, index|
|
211
|
+
yield self[index],
|
212
|
+
match[:attributes]["@groupby"],
|
213
|
+
match[:attributes]["@count"]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
alias_method :each_with_group_and_count, :each_with_groupby_and_count
|
217
|
+
|
218
|
+
def each_with_weighting(&block)
|
219
|
+
populate
|
220
|
+
results[:matches].each_with_index do |match, index|
|
221
|
+
yield self[index], match[:weight]
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def excerpt_for(string, model = nil)
|
226
|
+
if model.nil? && one_class
|
227
|
+
model ||= one_class
|
228
|
+
end
|
229
|
+
|
230
|
+
populate
|
231
|
+
client.excerpts(
|
232
|
+
:docs => [string],
|
233
|
+
:words => results[:words].keys.join(' '),
|
234
|
+
:index => "#{model.source_of_sphinx_index.sphinx_name}_core"
|
235
|
+
).first
|
236
|
+
end
|
237
|
+
|
238
|
+
def search(*args)
|
239
|
+
add_default_scope
|
240
|
+
merge_search ThinkingSphinx::Search.new(*args)
|
241
|
+
self
|
242
|
+
end
|
243
|
+
|
244
|
+
private
|
245
|
+
|
246
|
+
def config
|
247
|
+
ThinkingSphinx::Configuration.instance
|
248
|
+
end
|
249
|
+
|
250
|
+
def populate
|
251
|
+
return if @populated
|
252
|
+
@populated = true
|
253
|
+
|
254
|
+
retry_on_stale_index do
|
255
|
+
begin
|
256
|
+
log "Querying: '#{query}'"
|
257
|
+
runtime = Benchmark.realtime {
|
258
|
+
@results = client.query query, indexes, comment
|
259
|
+
}
|
260
|
+
log "Found #{@results[:total_found]} results", :debug,
|
261
|
+
"Sphinx (#{sprintf("%f", runtime)}s)"
|
262
|
+
rescue Errno::ECONNREFUSED => err
|
263
|
+
raise ThinkingSphinx::ConnectionError,
|
264
|
+
'Connection to Sphinx Daemon (searchd) failed.'
|
265
|
+
end
|
266
|
+
|
267
|
+
if options[:ids_only]
|
268
|
+
replace @results[:matches].collect { |match|
|
269
|
+
match[:attributes]["sphinx_internal_id"]
|
270
|
+
}
|
271
|
+
else
|
272
|
+
replace instances_from_matches
|
273
|
+
add_excerpter
|
274
|
+
add_sphinx_attributes
|
275
|
+
add_matching_fields if client.rank_mode == :fieldmask
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def add_excerpter
|
281
|
+
each do |object|
|
282
|
+
next if object.respond_to?(:excerpts)
|
283
|
+
|
284
|
+
excerpter = ThinkingSphinx::Excerpter.new self, object
|
285
|
+
block = lambda { excerpter }
|
286
|
+
|
287
|
+
object.singleton_class.instance_eval do
|
288
|
+
define_method(:excerpts, &block)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def add_sphinx_attributes
|
294
|
+
each do |object|
|
295
|
+
next if object.nil? || object.respond_to?(:sphinx_attributes)
|
296
|
+
|
297
|
+
match = match_hash object
|
298
|
+
next if match.nil?
|
299
|
+
|
300
|
+
object.singleton_class.instance_eval do
|
301
|
+
define_method(:sphinx_attributes) { match[:attributes] }
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def add_matching_fields
|
307
|
+
each do |object|
|
308
|
+
next if object.nil? || object.respond_to?(:matching_fields)
|
309
|
+
|
310
|
+
match = match_hash object
|
311
|
+
next if match.nil?
|
312
|
+
fields = ThinkingSphinx::Search.matching_fields(
|
313
|
+
@results[:fields], match[:weight]
|
314
|
+
)
|
315
|
+
|
316
|
+
object.singleton_class.instance_eval do
|
317
|
+
define_method(:matching_fields) { fields }
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def match_hash(object)
|
323
|
+
@results[:matches].detect { |match|
|
324
|
+
match[:attributes]['sphinx_internal_id'] == object.
|
325
|
+
primary_key_for_sphinx &&
|
326
|
+
match[:attributes]['class_crc'] == object.class.to_crc32
|
327
|
+
}
|
328
|
+
end
|
329
|
+
|
330
|
+
def self.log(message, method = :debug, identifier = 'Sphinx')
|
331
|
+
return if ::ActiveRecord::Base.logger.nil?
|
332
|
+
identifier_color, message_color = "4;32;1", "0" # 0;1 = Bold
|
333
|
+
info = " \e[#{identifier_color}m#{identifier}\e[0m "
|
334
|
+
info << "\e[#{message_color}m#{message}\e[0m"
|
335
|
+
::ActiveRecord::Base.logger.send method, info
|
336
|
+
end
|
337
|
+
|
338
|
+
def log(*args)
|
339
|
+
self.class.log(*args)
|
340
|
+
end
|
341
|
+
|
342
|
+
def client
|
343
|
+
client = config.client
|
344
|
+
|
345
|
+
index_options = one_class ?
|
346
|
+
one_class.sphinx_indexes.first.local_options : {}
|
347
|
+
|
348
|
+
[
|
349
|
+
:max_matches, :group_by, :group_function, :group_clause,
|
350
|
+
:group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
|
351
|
+
:rank_mode, :max_query_time, :field_weights
|
352
|
+
].each do |key|
|
353
|
+
value = options[key] || index_options[key]
|
354
|
+
client.send("#{key}=", value) if value
|
355
|
+
end
|
356
|
+
|
357
|
+
# treated non-standard as :select is already used for AR queries
|
358
|
+
client.select = options[:sphinx_select] || '*'
|
359
|
+
|
360
|
+
client.limit = per_page
|
361
|
+
client.offset = offset
|
362
|
+
client.match_mode = match_mode
|
363
|
+
client.filters = filters
|
364
|
+
client.sort_mode = sort_mode
|
365
|
+
client.sort_by = sort_by
|
366
|
+
client.group_by = group_by if group_by
|
367
|
+
client.group_function = group_function if group_function
|
368
|
+
client.index_weights = index_weights
|
369
|
+
client.anchor = anchor
|
370
|
+
|
371
|
+
client
|
372
|
+
end
|
373
|
+
|
374
|
+
def retry_on_stale_index(&block)
|
375
|
+
stale_ids = []
|
376
|
+
retries = stale_retries
|
377
|
+
|
378
|
+
begin
|
379
|
+
options[:raise_on_stale] = retries > 0
|
380
|
+
block.call
|
381
|
+
|
382
|
+
# If ThinkingSphinx::Search#instances_from_matches found records in
|
383
|
+
# Sphinx but not in the DB and the :raise_on_stale option is set, this
|
384
|
+
# exception is raised. We retry a limited number of times, excluding the
|
385
|
+
# stale ids from the search.
|
386
|
+
rescue StaleIdsException => err
|
387
|
+
retries -= 1
|
388
|
+
|
389
|
+
# For logging
|
390
|
+
stale_ids |= err.ids
|
391
|
+
# ID exclusion
|
392
|
+
options[:without_ids] = Array(options[:without_ids]) | err.ids
|
393
|
+
|
394
|
+
log 'Sphinx Stale Ids (%s %s left): %s' % [
|
395
|
+
retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
|
396
|
+
]
|
397
|
+
retry
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def classes
|
402
|
+
@classes ||= options[:classes] || []
|
403
|
+
end
|
404
|
+
|
405
|
+
def one_class
|
406
|
+
@one_class ||= classes.length != 1 ? nil : classes.first
|
407
|
+
end
|
408
|
+
|
409
|
+
def query
|
410
|
+
@query ||= begin
|
411
|
+
q = @args.join(' ') << conditions_as_query
|
412
|
+
(options[:star] ? star_query(q) : q).strip
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def conditions_as_query
|
417
|
+
return '' if @options[:conditions].blank?
|
418
|
+
|
419
|
+
# Soon to be deprecated.
|
420
|
+
keys = @options[:conditions].keys.reject { |key|
|
421
|
+
attributes.include?(key.to_sym)
|
422
|
+
}
|
423
|
+
|
424
|
+
' ' + keys.collect { |key|
|
425
|
+
"@#{key} #{options[:conditions][key]}"
|
426
|
+
}.join(' ')
|
427
|
+
end
|
428
|
+
|
429
|
+
def star_query(query)
|
430
|
+
token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
|
431
|
+
|
432
|
+
query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
|
433
|
+
pre, proper, post = $`, $&, $'
|
434
|
+
# E.g. "@foo", "/2", "~3", but not as part of a token
|
435
|
+
is_operator = pre.match(%r{(\W|^)[@~/]\Z})
|
436
|
+
# E.g. "foo bar", with quotes
|
437
|
+
is_quote = proper.starts_with?('"') && proper.ends_with?('"')
|
438
|
+
has_star = pre.ends_with?("*") || post.starts_with?("*")
|
439
|
+
if is_operator || is_quote || has_star
|
440
|
+
proper
|
441
|
+
else
|
442
|
+
"*#{proper}*"
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
def comment
|
448
|
+
options[:comment] || ''
|
449
|
+
end
|
450
|
+
|
451
|
+
def match_mode
|
452
|
+
options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
|
453
|
+
end
|
454
|
+
|
455
|
+
def sort_mode
|
456
|
+
@sort_mode ||= case options[:sort_mode]
|
457
|
+
when :asc
|
458
|
+
:attr_asc
|
459
|
+
when :desc
|
460
|
+
:attr_desc
|
461
|
+
when nil
|
462
|
+
case options[:order]
|
463
|
+
when String
|
464
|
+
:extended
|
465
|
+
when Symbol
|
466
|
+
:attr_asc
|
467
|
+
else
|
468
|
+
:relevance
|
469
|
+
end
|
470
|
+
else
|
471
|
+
options[:sort_mode]
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def sort_by
|
476
|
+
case @sort_by = (options[:sort_by] || options[:order])
|
477
|
+
when String
|
478
|
+
sorted_fields_to_attributes(@sort_by)
|
479
|
+
when Symbol
|
480
|
+
field_names.include?(@sort_by) ?
|
481
|
+
@sort_by.to_s.concat('_sort') : @sort_by.to_s
|
482
|
+
else
|
483
|
+
''
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def field_names
|
488
|
+
return [] unless one_class
|
489
|
+
|
490
|
+
one_class.sphinx_indexes.collect { |index|
|
491
|
+
index.fields.collect { |field| field.unique_name }
|
492
|
+
}.flatten
|
493
|
+
end
|
494
|
+
|
495
|
+
def sorted_fields_to_attributes(order_string)
|
496
|
+
field_names.each { |field|
|
497
|
+
order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
|
498
|
+
match.gsub field.to_s, field.to_s.concat("_sort")
|
499
|
+
}
|
500
|
+
}
|
501
|
+
|
502
|
+
order_string
|
503
|
+
end
|
504
|
+
|
505
|
+
# Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
|
506
|
+
# { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
|
507
|
+
#
|
508
|
+
def index_weights
|
509
|
+
weights = options[:index_weights] || {}
|
510
|
+
weights.keys.inject({}) do |hash, key|
|
511
|
+
if key.is_a?(Class)
|
512
|
+
name = ThinkingSphinx::Index.name_for(key)
|
513
|
+
hash["#{name}_core"] = weights[key]
|
514
|
+
hash["#{name}_delta"] = weights[key]
|
515
|
+
else
|
516
|
+
hash[key] = weights[key]
|
517
|
+
end
|
518
|
+
|
519
|
+
hash
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
def group_by
|
524
|
+
options[:group] ? options[:group].to_s : nil
|
525
|
+
end
|
526
|
+
|
527
|
+
def group_function
|
528
|
+
options[:group] ? :attr : nil
|
529
|
+
end
|
530
|
+
|
531
|
+
def internal_filters
|
532
|
+
filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
|
533
|
+
|
534
|
+
class_crcs = classes.collect { |klass|
|
535
|
+
klass.to_crc32s
|
536
|
+
}.flatten
|
537
|
+
|
538
|
+
unless class_crcs.empty?
|
539
|
+
filters << Riddle::Client::Filter.new('class_crc', class_crcs)
|
540
|
+
end
|
541
|
+
|
542
|
+
filters << Riddle::Client::Filter.new(
|
543
|
+
'sphinx_internal_id', filter_value(options[:without_ids]), true
|
544
|
+
) if options[:without_ids]
|
545
|
+
|
546
|
+
filters
|
547
|
+
end
|
548
|
+
|
549
|
+
def condition_filters
|
550
|
+
(options[:conditions] || {}).collect { |attrib, value|
|
551
|
+
if attributes.include?(attrib.to_sym)
|
552
|
+
puts <<-MSG
|
553
|
+
Deprecation Warning: filters on attributes should be done using the :with
|
554
|
+
option, not :conditions. For example:
|
555
|
+
:with => {:#{attrib} => #{value.inspect}}
|
556
|
+
MSG
|
557
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
558
|
+
else
|
559
|
+
nil
|
560
|
+
end
|
561
|
+
}.compact
|
562
|
+
end
|
563
|
+
|
564
|
+
def filters
|
565
|
+
internal_filters +
|
566
|
+
condition_filters +
|
567
|
+
(options[:with] || {}).collect { |attrib, value|
|
568
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
569
|
+
} +
|
570
|
+
(options[:without] || {}).collect { |attrib, value|
|
571
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
|
572
|
+
} +
|
573
|
+
(options[:with_all] || {}).collect { |attrib, values|
|
574
|
+
Array(values).collect { |value|
|
575
|
+
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
576
|
+
}
|
577
|
+
}.flatten
|
578
|
+
end
|
579
|
+
|
580
|
+
# When passed a Time instance, returns the integer timestamp.
|
581
|
+
#
|
582
|
+
# If using Rails 2.1+, need to handle timezones to translate them back to
|
583
|
+
# UTC, as that's what datetimes will be stored as by MySQL.
|
584
|
+
#
|
585
|
+
# in_time_zone is a method that was added for the timezone support in
|
586
|
+
# Rails 2.1, which is why it's used for testing. I'm sure there's better
|
587
|
+
# ways, but this does the job.
|
588
|
+
#
|
589
|
+
def filter_value(value)
|
590
|
+
case value
|
591
|
+
when Range
|
592
|
+
filter_value(value.first).first..filter_value(value.last).first
|
593
|
+
when Array
|
594
|
+
value.collect { |v| filter_value(v) }.flatten
|
595
|
+
when Time
|
596
|
+
value.respond_to?(:in_time_zone) ? [value.utc.to_i] : [value.to_i]
|
597
|
+
when NilClass
|
598
|
+
0
|
599
|
+
else
|
600
|
+
Array(value)
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
def anchor
|
605
|
+
return {} unless options[:geo] || (options[:lat] && options[:lng])
|
606
|
+
|
607
|
+
{
|
608
|
+
:latitude => options[:geo] ? options[:geo].first : options[:lat],
|
609
|
+
:longitude => options[:geo] ? options[:geo].last : options[:lng],
|
610
|
+
:latitude_attribute => latitude_attr.to_s,
|
611
|
+
:longitude_attribute => longitude_attr.to_s
|
612
|
+
}
|
613
|
+
end
|
614
|
+
|
615
|
+
def latitude_attr
|
616
|
+
options[:latitude_attr] ||
|
617
|
+
index_option(:latitude_attr) ||
|
618
|
+
attribute(:lat, :latitude)
|
619
|
+
end
|
620
|
+
|
621
|
+
def longitude_attr
|
622
|
+
options[:longitude_attr] ||
|
623
|
+
index_option(:longitude_attr) ||
|
624
|
+
attribute(:lon, :lng, :longitude)
|
625
|
+
end
|
626
|
+
|
627
|
+
def index_option(key)
|
628
|
+
return nil unless one_class
|
629
|
+
|
630
|
+
one_class.sphinx_indexes.collect { |index|
|
631
|
+
index.local_options[key]
|
632
|
+
}.compact.first
|
633
|
+
end
|
634
|
+
|
635
|
+
def attribute(*keys)
|
636
|
+
return nil unless one_class
|
637
|
+
|
638
|
+
keys.detect { |key|
|
639
|
+
attributes.include?(key)
|
640
|
+
}
|
641
|
+
end
|
642
|
+
|
643
|
+
def attributes
|
644
|
+
return [] unless one_class
|
645
|
+
|
646
|
+
attributes = one_class.sphinx_indexes.collect { |index|
|
647
|
+
index.attributes.collect { |attrib| attrib.unique_name }
|
648
|
+
}.flatten
|
649
|
+
end
|
650
|
+
|
651
|
+
def stale_retries
|
652
|
+
case options[:retry_stale]
|
653
|
+
when TrueClass
|
654
|
+
3
|
655
|
+
when nil, FalseClass
|
656
|
+
0
|
657
|
+
else
|
658
|
+
options[:retry_stale].to_i
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
def instances_from_class(klass, matches)
|
663
|
+
index_options = klass.sphinx_index_options
|
664
|
+
|
665
|
+
ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
|
666
|
+
instances = ids.length > 0 ? klass.find(
|
667
|
+
:all,
|
668
|
+
:joins => options[:joins],
|
669
|
+
:conditions => {klass.primary_key_for_sphinx.to_sym => ids},
|
670
|
+
:include => (options[:include] || index_options[:include]),
|
671
|
+
:select => (options[:select] || index_options[:select]),
|
672
|
+
:order => (options[:sql_order] || index_options[:sql_order] || "FIELD(id, #{ids.join(',') })")
|
673
|
+
) : []
|
674
|
+
|
675
|
+
# Raise an exception if we find records in Sphinx but not in the DB, so
|
676
|
+
# the search method can retry without them. See
|
677
|
+
# ThinkingSphinx::Search.retry_search_on_stale_index.
|
678
|
+
if options[:raise_on_stale] && instances.length < ids.length
|
679
|
+
stale_ids = ids - instances.map { |i| i.id }
|
680
|
+
raise StaleIdsException, stale_ids
|
681
|
+
end
|
682
|
+
|
683
|
+
# if the user has specified an SQL order, return the collection
|
684
|
+
# without rearranging it into the Sphinx order
|
685
|
+
return instances
|
686
|
+
end
|
687
|
+
|
688
|
+
# Group results by class and call #find(:all) once for each group to reduce
|
689
|
+
# the number of #find's in multi-model searches.
|
690
|
+
#
|
691
|
+
def instances_from_matches
|
692
|
+
return single_class_results if one_class
|
693
|
+
|
694
|
+
groups = results[:matches].group_by { |match|
|
695
|
+
match[:attributes]["class_crc"]
|
696
|
+
}
|
697
|
+
groups.each do |crc, group|
|
698
|
+
group.replace(
|
699
|
+
instances_from_class(class_from_crc(crc), group)
|
700
|
+
)
|
701
|
+
end
|
702
|
+
|
703
|
+
results[:matches].collect do |match|
|
704
|
+
groups.detect { |crc, group|
|
705
|
+
crc == match[:attributes]["class_crc"]
|
706
|
+
}[1].compact.detect { |obj|
|
707
|
+
obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
|
708
|
+
}
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
def single_class_results
|
713
|
+
instances_from_class one_class, results[:matches]
|
714
|
+
end
|
715
|
+
|
716
|
+
def class_from_crc(crc)
|
717
|
+
config.models_by_crc[crc].constantize
|
718
|
+
end
|
719
|
+
|
720
|
+
def each_with_attribute(attribute, &block)
|
721
|
+
populate
|
722
|
+
results[:matches].each_with_index do |match, index|
|
723
|
+
yield self[index],
|
724
|
+
(match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
def is_scope?(method)
|
729
|
+
one_class && one_class.sphinx_scopes.include?(method)
|
730
|
+
end
|
731
|
+
|
732
|
+
# Adds the default_sphinx_scope if set.
|
733
|
+
def add_default_scope
|
734
|
+
add_scope(one_class.get_default_sphinx_scope) if one_class && one_class.has_default_sphinx_scope?
|
735
|
+
end
|
736
|
+
|
737
|
+
def add_scope(method, *args, &block)
|
738
|
+
merge_search one_class.send(method, *args, &block)
|
739
|
+
end
|
740
|
+
|
741
|
+
def merge_search(search)
|
742
|
+
search.args.each { |arg| args << arg }
|
743
|
+
|
744
|
+
search.options.keys.each do |key|
|
745
|
+
if HashOptions.include?(key)
|
746
|
+
options[key] ||= {}
|
747
|
+
options[key].merge! search.options[key]
|
748
|
+
elsif ArrayOptions.include?(key)
|
749
|
+
options[key] ||= []
|
750
|
+
options[key] += search.options[key]
|
751
|
+
options[key].uniq!
|
752
|
+
else
|
753
|
+
options[key] = search.options[key]
|
754
|
+
end
|
755
|
+
end
|
756
|
+
end
|
757
|
+
|
758
|
+
def scoped_count
|
759
|
+
return self.total_entries if @options[:ids_only]
|
760
|
+
|
761
|
+
@options[:ids_only] = true
|
762
|
+
results_count = self.total_entries
|
763
|
+
@options[:ids_only] = false
|
764
|
+
@populated = false
|
765
|
+
|
766
|
+
results_count
|
767
|
+
end
|
768
|
+
end
|
769
|
+
end
|