thinking-sphinx 2.0.5 → 2.0.6

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.
Files changed (70) hide show
  1. data/README.textile +7 -1
  2. data/features/searching_by_model.feature +24 -30
  3. data/features/step_definitions/common_steps.rb +5 -5
  4. data/features/thinking_sphinx/db/.gitignore +1 -0
  5. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +1 -0
  6. data/spec/fixtures/data.sql +32 -0
  7. data/spec/fixtures/database.yml.default +3 -0
  8. data/spec/fixtures/models.rb +161 -0
  9. data/spec/fixtures/structure.sql +146 -0
  10. data/spec/spec_helper.rb +62 -0
  11. data/spec/sphinx_helper.rb +61 -0
  12. data/spec/support/rails.rb +18 -0
  13. data/spec/thinking_sphinx/active_record/delta_spec.rb +24 -24
  14. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +27 -0
  15. data/spec/thinking_sphinx/active_record/scopes_spec.rb +25 -25
  16. data/spec/thinking_sphinx/active_record_spec.rb +108 -107
  17. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +38 -38
  18. data/spec/thinking_sphinx/association_spec.rb +69 -35
  19. data/spec/thinking_sphinx/context_spec.rb +61 -64
  20. data/spec/thinking_sphinx/search_spec.rb +7 -0
  21. data/spec/thinking_sphinx_spec.rb +47 -46
  22. metadata +49 -141
  23. data/VERSION +0 -1
  24. data/lib/cucumber/thinking_sphinx/external_world.rb +0 -12
  25. data/lib/cucumber/thinking_sphinx/internal_world.rb +0 -127
  26. data/lib/cucumber/thinking_sphinx/sql_logger.rb +0 -20
  27. data/lib/thinking-sphinx.rb +0 -1
  28. data/lib/thinking_sphinx.rb +0 -301
  29. data/lib/thinking_sphinx/action_controller.rb +0 -31
  30. data/lib/thinking_sphinx/active_record.rb +0 -384
  31. data/lib/thinking_sphinx/active_record/attribute_updates.rb +0 -52
  32. data/lib/thinking_sphinx/active_record/delta.rb +0 -65
  33. data/lib/thinking_sphinx/active_record/has_many_association.rb +0 -36
  34. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +0 -21
  35. data/lib/thinking_sphinx/active_record/log_subscriber.rb +0 -61
  36. data/lib/thinking_sphinx/active_record/scopes.rb +0 -93
  37. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +0 -87
  38. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +0 -62
  39. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +0 -157
  40. data/lib/thinking_sphinx/association.rb +0 -219
  41. data/lib/thinking_sphinx/attribute.rb +0 -396
  42. data/lib/thinking_sphinx/auto_version.rb +0 -38
  43. data/lib/thinking_sphinx/bundled_search.rb +0 -44
  44. data/lib/thinking_sphinx/class_facet.rb +0 -20
  45. data/lib/thinking_sphinx/configuration.rb +0 -339
  46. data/lib/thinking_sphinx/context.rb +0 -76
  47. data/lib/thinking_sphinx/core/string.rb +0 -15
  48. data/lib/thinking_sphinx/deltas.rb +0 -28
  49. data/lib/thinking_sphinx/deltas/default_delta.rb +0 -62
  50. data/lib/thinking_sphinx/deploy/capistrano.rb +0 -101
  51. data/lib/thinking_sphinx/excerpter.rb +0 -23
  52. data/lib/thinking_sphinx/facet.rb +0 -128
  53. data/lib/thinking_sphinx/facet_search.rb +0 -170
  54. data/lib/thinking_sphinx/field.rb +0 -98
  55. data/lib/thinking_sphinx/index.rb +0 -157
  56. data/lib/thinking_sphinx/index/builder.rb +0 -312
  57. data/lib/thinking_sphinx/index/faux_column.rb +0 -118
  58. data/lib/thinking_sphinx/join.rb +0 -37
  59. data/lib/thinking_sphinx/property.rb +0 -185
  60. data/lib/thinking_sphinx/railtie.rb +0 -46
  61. data/lib/thinking_sphinx/search.rb +0 -972
  62. data/lib/thinking_sphinx/search_methods.rb +0 -439
  63. data/lib/thinking_sphinx/sinatra.rb +0 -7
  64. data/lib/thinking_sphinx/source.rb +0 -194
  65. data/lib/thinking_sphinx/source/internal_properties.rb +0 -51
  66. data/lib/thinking_sphinx/source/sql.rb +0 -157
  67. data/lib/thinking_sphinx/tasks.rb +0 -130
  68. data/lib/thinking_sphinx/test.rb +0 -55
  69. data/tasks/distribution.rb +0 -33
  70. data/tasks/testing.rb +0 -80
@@ -1,118 +0,0 @@
1
- module ThinkingSphinx
2
- class Index
3
- # Instances of this class represent database columns and the stack of
4
- # associations that lead from the base model to them.
5
- #
6
- # The name and stack are accessible through methods starting with __ to
7
- # avoid conflicting with the method_missing calls that build the stack.
8
- #
9
- class FauxColumn
10
- # Create a new column with a pre-defined stack. The top element in the
11
- # stack will get shifted to be the name value.
12
- #
13
- def initialize(*stack)
14
- @name = stack.pop
15
- @stack = stack
16
- end
17
-
18
- def self.coerce(columns)
19
- case columns
20
- when Symbol, String
21
- FauxColumn.new(columns)
22
- when Array
23
- columns.collect { |col| FauxColumn.coerce(col) }
24
- when FauxColumn
25
- columns
26
- else
27
- nil
28
- end
29
- end
30
-
31
- # Can't use normal method name, as that could be an association or
32
- # column name.
33
- #
34
- def __name
35
- @name
36
- end
37
-
38
- # Can't use normal method name, as that could be an association or
39
- # column name.
40
- #
41
- def __stack
42
- @stack
43
- end
44
-
45
- def __path
46
- @stack + [@name]
47
- end
48
-
49
- # Returns true if the stack is empty *and* if the name is a string -
50
- # which is an indication that of raw SQL, as opposed to a value from a
51
- # table's column.
52
- #
53
- def is_string?
54
- @name.is_a?(String) && @stack.empty?
55
- end
56
-
57
- def to_ary
58
- [self]
59
- end
60
-
61
- # This handles any 'invalid' method calls and sets them as the name,
62
- # and pushing the previous name into the stack. The object returns
63
- # itself.
64
- #
65
- # If there's a single argument, it becomes the name, and the method
66
- # symbol goes into the stack as well. Multiple arguments means new
67
- # columns with the original stack and new names (from each argument) gets
68
- # returned.
69
- #
70
- # Easier to explain with examples:
71
- #
72
- # col = FauxColumn.new :a, :b, :c
73
- # col.__name #=> :c
74
- # col.__stack #=> [:a, :b]
75
- #
76
- # col.whatever #=> col
77
- # col.__name #=> :whatever
78
- # col.__stack #=> [:a, :b, :c]
79
- #
80
- # col.something(:id) #=> col
81
- # col.__name #=> :id
82
- # col.__stack #=> [:a, :b, :c, :whatever, :something]
83
- #
84
- # cols = col.short(:x, :y, :z)
85
- # cols[0].__name #=> :x
86
- # cols[0].__stack #=> [:a, :b, :c, :whatever, :something, :short]
87
- # cols[1].__name #=> :y
88
- # cols[1].__stack #=> [:a, :b, :c, :whatever, :something, :short]
89
- # cols[2].__name #=> :z
90
- # cols[2].__stack #=> [:a, :b, :c, :whatever, :something, :short]
91
- #
92
- # Also, this allows method chaining to build up a relevant stack:
93
- #
94
- # col = FauxColumn.new :a, :b
95
- # col.__name #=> :b
96
- # col.__stack #=> [:a]
97
- #
98
- # col.one.two.three #=> col
99
- # col.__name #=> :three
100
- # col.__stack #=> [:a, :b, :one, :two]
101
- #
102
- def method_missing(method, *args)
103
- @stack << @name
104
- @name = method
105
-
106
- if (args.empty?)
107
- self
108
- elsif (args.length == 1)
109
- method_missing(args.first)
110
- else
111
- args.collect { |arg|
112
- FauxColumn.new(@stack + [@name, arg])
113
- }
114
- end
115
- end
116
- end
117
- end
118
- end
@@ -1,37 +0,0 @@
1
- module ThinkingSphinx
2
- class Join
3
- attr_accessor :source, :column, :associations
4
-
5
- def initialize(source, column)
6
- @source = source
7
- @column = column
8
-
9
- @associations = association_stack(column.__path.clone).each { |assoc|
10
- assoc.join_to(source.base)
11
- }
12
-
13
- source.joins << self
14
- end
15
-
16
- private
17
-
18
- # Gets a stack of associations for a specific path.
19
- #
20
- def association_stack(path, parent = nil)
21
- assocs = []
22
-
23
- if parent.nil?
24
- assocs = @source.association(path.shift)
25
- else
26
- assocs = parent.children(path.shift)
27
- end
28
-
29
- until path.empty?
30
- point = path.shift
31
- assocs = assocs.collect { |assoc| assoc.children(point) }.flatten
32
- end
33
-
34
- assocs
35
- end
36
- end
37
- end
@@ -1,185 +0,0 @@
1
- module ThinkingSphinx
2
- class Property
3
- attr_accessor :alias, :columns, :associations, :model, :faceted, :admin
4
-
5
- def initialize(source, columns, options = {})
6
- @source = source
7
- @model = source.model
8
- @columns = Array(columns)
9
- @associations = {}
10
-
11
- raise "Cannot define a field or attribute in #{source.model.name} with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) }
12
-
13
- @alias = options[:as]
14
- @faceted = options[:facet]
15
- @admin = options[:admin]
16
- @sortable = options[:sortable] || false
17
- @value_source = options[:value]
18
-
19
- @alias = @alias.to_sym unless @alias.blank?
20
-
21
- @columns.each { |col|
22
- @associations[col] = association_stack(col.__stack.clone).each { |assoc|
23
- assoc.join_to(source.base)
24
- }
25
- }
26
- end
27
-
28
- # Returns the unique name of the attribute - which is either the alias of
29
- # the attribute, or the name of the only column - if there is only one. If
30
- # there isn't, there should be an alias. Else things probably won't work.
31
- # Consider yourself warned.
32
- #
33
- def unique_name
34
- if @columns.length == 1
35
- @alias || @columns.first.__name
36
- else
37
- @alias
38
- end
39
- end
40
-
41
- def to_facet
42
- return nil unless @faceted
43
-
44
- ThinkingSphinx::Facet.new(self, @value_source)
45
- end
46
-
47
- # Get the part of the GROUP BY clause related to this attribute - if one is
48
- # needed. If not, all you'll get back is nil. The latter will happen if
49
- # there isn't actually a real column to get data from, or if there's
50
- # multiple data values (read: a has_many or has_and_belongs_to_many
51
- # association).
52
- #
53
- def to_group_sql
54
- case
55
- when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
56
- nil
57
- else
58
- @columns.collect { |column|
59
- column_with_prefix(column)
60
- }
61
- end
62
- end
63
-
64
- def changed?(instance)
65
- return true if is_string? || @columns.any? { |col| !col.__stack.empty? }
66
-
67
- @columns.any? { |col|
68
- instance.send("#{col.__name.to_s}_changed?")
69
- }
70
- end
71
-
72
- def admin?
73
- admin
74
- end
75
-
76
- def public?
77
- !admin
78
- end
79
-
80
- def available?
81
- columns.any? { |column| column_available?(column) }
82
- end
83
-
84
- private
85
-
86
- # Could there be more than one value related to the parent record? If so,
87
- # then this will return true. If not, false. It's that simple.
88
- #
89
- def is_many?
90
- associations.values.flatten.any? { |assoc| assoc.is_many? }
91
- end
92
-
93
- # Returns true if any of the columns are string values, instead of database
94
- # column references.
95
- def is_string?
96
- columns.all? { |col| col.is_string? }
97
- end
98
-
99
- def adapter
100
- @adapter ||= @model.sphinx_database_adapter
101
- end
102
-
103
- def quote_with_table(table, column)
104
- "#{quote_table_name(table)}.#{quote_column(column)}"
105
- end
106
-
107
- def quote_column(column)
108
- @model.connection.quote_column_name(column)
109
- end
110
-
111
- def quote_table_name(table_name)
112
- @model.connection.quote_table_name(table_name)
113
- end
114
-
115
- # Indication of whether the columns should be concatenated with a space
116
- # between each value. True if there's either multiple sources or multiple
117
- # associations.
118
- #
119
- def concat_ws?
120
- multiple_associations? || @columns.length > 1
121
- end
122
-
123
- # Checks whether any column requires multiple associations (which only
124
- # happens for polymorphic situations).
125
- #
126
- def multiple_associations?
127
- associations.any? { |col,assocs| assocs.length > 1 }
128
- end
129
-
130
- # Builds a column reference tied to the appropriate associations. This
131
- # dives into the associations hash and their corresponding joins to
132
- # figure out how to correctly reference a column in SQL.
133
- #
134
- def column_with_prefix(column)
135
- return nil unless column_available?(column)
136
-
137
- if column.is_string?
138
- column.__name
139
- elsif column.__stack.empty?
140
- "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
141
- else
142
- associations[column].collect { |assoc|
143
- assoc.has_column?(column.__name) ?
144
- "#{quote_with_table(assoc.join.aliased_table_name, column.__name)}" :
145
- nil
146
- }.compact
147
- end
148
- end
149
-
150
- def columns_with_prefixes
151
- @columns.collect { |column|
152
- column_with_prefix column
153
- }.flatten.compact
154
- end
155
-
156
- def column_available?(column)
157
- if column.is_string?
158
- true
159
- elsif column.__stack.empty?
160
- @model.column_names.include?(column.__name.to_s)
161
- else
162
- associations[column].any? { |assoc| assoc.has_column?(column.__name) }
163
- end
164
- end
165
-
166
- # Gets a stack of associations for a specific path.
167
- #
168
- def association_stack(path, parent = nil)
169
- assocs = []
170
-
171
- if parent.nil?
172
- assocs = @source.association(path.shift)
173
- else
174
- assocs = parent.children(path.shift)
175
- end
176
-
177
- until path.empty?
178
- point = path.shift
179
- assocs = assocs.collect { |assoc| assoc.children(point) }.flatten
180
- end
181
-
182
- assocs
183
- end
184
- end
185
- end
@@ -1,46 +0,0 @@
1
- require 'thinking_sphinx'
2
- require 'rails'
3
-
4
- module ThinkingSphinx
5
- class Railtie < Rails::Railtie
6
-
7
- initializer 'thinking_sphinx.sphinx' do
8
- ThinkingSphinx::AutoVersion.detect
9
- end
10
-
11
- initializer "thinking_sphinx.active_record" do
12
- ActiveSupport.on_load :active_record do
13
- include ThinkingSphinx::ActiveRecord
14
- end
15
- end
16
-
17
- initializer "thinking_sphinx.action_controller" do
18
- ActiveSupport.on_load :action_controller do
19
- require 'thinking_sphinx/action_controller'
20
- include ThinkingSphinx::ActionController
21
- end
22
- end
23
-
24
- initializer "thinking_sphinx.set_app_root" do |app|
25
- ThinkingSphinx::Configuration.instance.reset # Rails has setup app now
26
- end
27
-
28
- config.to_prepare do
29
- I18n.backend.reload!
30
- I18n.backend.available_locales
31
-
32
- # ActiveRecord::Base.to_crc32s is dependant on the subclasses being loaded
33
- # consistently. When the environment is reset, subclasses/descendants will
34
- # be lost but our context will not reload them for us.
35
- #
36
- # We reset the context which causes the subclasses/descendants to be
37
- # reloaded next time the context is called.
38
- #
39
- ThinkingSphinx.reset_context!
40
- end
41
-
42
- rake_tasks do
43
- load File.expand_path('../tasks.rb', __FILE__)
44
- end
45
- end
46
- end
@@ -1,972 +0,0 @@
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 < Array
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?
15
- respond_to_missing? send should 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, :without_any]
26
- ArrayOptions = [:classes, :without_ids]
27
-
28
- attr_reader :args, :options
29
-
30
- # Deprecated. Use ThinkingSphinx.search
31
- def self.search(*args)
32
- warn '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
- warn '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
- warn '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
- warn '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
- warn 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
57
- ThinkingSphinx.facets(*args)
58
- end
59
-
60
- def self.warn(message)
61
- ::ActiveSupport::Deprecation.warn message
62
- end
63
-
64
- def self.bundle_searches(enum = nil)
65
- bundle = ThinkingSphinx::BundledSearch.new
66
-
67
- if enum.nil?
68
- yield bundle
69
- else
70
- enum.each { |item| yield bundle, item }
71
- end
72
-
73
- bundle.searches
74
- end
75
-
76
- def self.matching_fields(fields, bitmask)
77
- matches = []
78
- bitstring = bitmask.to_s(2).rjust(32, '0').reverse
79
-
80
- fields.each_with_index do |field, index|
81
- matches << field if bitstring[index, 1] == '1'
82
- end
83
- matches
84
- end
85
-
86
- def initialize(*args)
87
- ThinkingSphinx.context.define_indexes
88
-
89
- @array = []
90
- @options = args.extract_options!
91
- @args = args
92
-
93
- add_default_scope unless options[:ignore_default]
94
-
95
- populate if @options[:populate]
96
- end
97
-
98
- def to_a
99
- populate
100
- @array
101
- end
102
-
103
- def freeze
104
- populate
105
- @array.freeze
106
- self
107
- end
108
-
109
- def as_json(*args)
110
- populate
111
- @array.as_json(*args)
112
- end
113
-
114
- # Indication of whether the request has been made to Sphinx for the search
115
- # query.
116
- #
117
- # @return [Boolean] true if the results have been requested.
118
- #
119
- def populated?
120
- !!@populated
121
- end
122
-
123
- # Indication of whether the request resulted in an error from Sphinx.
124
- #
125
- # @return [Boolean] true if Sphinx reports query error
126
- #
127
- def error?
128
- !!error
129
- end
130
-
131
- # The Sphinx-reported error, if any.
132
- #
133
- # @return [String, nil]
134
- #
135
- def error
136
- populate
137
- @results[:error]
138
- end
139
-
140
- # Indication of whether the request resulted in a warning from Sphinx.
141
- #
142
- # @return [Boolean] true if Sphinx reports query warning
143
- #
144
- def warning?
145
- !!warning
146
- end
147
-
148
- # The Sphinx-reported warning, if any.
149
- #
150
- # @return [String, nil]
151
- #
152
- def warning
153
- populate
154
- @results[:warning]
155
- end
156
-
157
- # The query result hash from Riddle.
158
- #
159
- # @return [Hash] Raw Sphinx results
160
- #
161
- def results
162
- populate
163
- @results
164
- end
165
-
166
- def method_missing(method, *args, &block)
167
- if is_scope?(method)
168
- add_scope(method, *args, &block)
169
- return self
170
- elsif method == :search_count
171
- merge_search one_class.search(*args), self.args, options
172
- return scoped_count
173
- elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
174
- super
175
- elsif !SafeMethods.include?(method.to_s)
176
- populate
177
- end
178
-
179
- if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
180
- each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
181
- else
182
- @array.send(method, *args, &block)
183
- end
184
- end
185
-
186
- # Returns true if the Search object or the underlying Array object respond
187
- # to the requested method.
188
- #
189
- # @param [Symbol] method The method name
190
- # @return [Boolean] true if either Search or Array responds to the method.
191
- #
192
- def respond_to?(method, include_private = false)
193
- super || @array.respond_to?(method, include_private)
194
- end
195
-
196
- # The current page number of the result set. Defaults to 1 if no page was
197
- # explicitly requested.
198
- #
199
- # @return [Integer]
200
- #
201
- def current_page
202
- @options[:page].blank? ? 1 : @options[:page].to_i
203
- end
204
-
205
- # Kaminari support
206
- def page(page_number)
207
- @options[:page] = page_number
208
- self
209
- end
210
-
211
- # The next page number of the result set. If there are no more pages
212
- # available, nil is returned.
213
- #
214
- # @return [Integer, nil]
215
- #
216
- def next_page
217
- current_page >= total_pages ? nil : current_page + 1
218
- end
219
-
220
- # The previous page number of the result set. If this is the first page,
221
- # then nil is returned.
222
- #
223
- # @return [Integer, nil]
224
- #
225
- def previous_page
226
- current_page == 1 ? nil : current_page - 1
227
- end
228
-
229
- # The amount of records per set of paged results. Defaults to 20 unless a
230
- # specific page size is requested.
231
- #
232
- # @return [Integer]
233
- #
234
- def per_page
235
- @options[:limit] ||= @options[:per_page]
236
- @options[:limit] ||= 20
237
- @options[:limit].to_i
238
- end
239
- # Kaminari support
240
- alias_method :limit_value, :per_page
241
-
242
- # Kaminari support
243
- def per(limit)
244
- @options[:limit] = limit
245
- self
246
- end
247
-
248
- # The total number of pages available if the results are paginated.
249
- #
250
- # @return [Integer]
251
- #
252
- def total_pages
253
- populate
254
- return 0 if @results[:total].nil?
255
-
256
- @total_pages ||= (@results[:total] / per_page.to_f).ceil
257
- end
258
- # Compatibility with kaminari and older versions of will_paginate
259
- alias_method :page_count, :total_pages
260
- alias_method :num_pages, :total_pages
261
-
262
- # Query time taken
263
- #
264
- # @return [Integer]
265
- #
266
- def query_time
267
- populate
268
- return 0 if @results[:time].nil?
269
-
270
- @query_time ||= @results[:time]
271
- end
272
-
273
- # The total number of search results available.
274
- #
275
- # @return [Integer]
276
- #
277
- def total_entries
278
- populate
279
- return 0 if @results[:total_found].nil?
280
-
281
- @total_entries ||= @results[:total_found]
282
- end
283
-
284
- # The current page's offset, based on the number of records per page.
285
- # Or explicit :offset if given.
286
- #
287
- # @return [Integer]
288
- #
289
- def offset
290
- @options[:offset] || ((current_page - 1) * per_page)
291
- end
292
-
293
- def indexes
294
- return options[:index] if options[:index]
295
- return '*' if classes.empty?
296
-
297
- classes.collect { |klass|
298
- klass.sphinx_index_names
299
- }.flatten.uniq.join(',')
300
- end
301
-
302
- def each_with_groupby_and_count(&block)
303
- populate
304
- results[:matches].each_with_index do |match, index|
305
- yield self[index],
306
- match[:attributes]["@groupby"],
307
- match[:attributes]["@count"]
308
- end
309
- end
310
- alias_method :each_with_group_and_count, :each_with_groupby_and_count
311
-
312
- def each_with_weighting(&block)
313
- populate
314
- results[:matches].each_with_index do |match, index|
315
- yield self[index], match[:weight]
316
- end
317
- end
318
-
319
- def each_with_match(&block)
320
- populate
321
- results[:matches].each_with_index do |match, index|
322
- yield self[index], match
323
- end
324
- end
325
-
326
- def excerpt_for(string, model = nil)
327
- if model.nil? && one_class
328
- model ||= one_class
329
- end
330
-
331
- populate
332
- client.excerpts(
333
- {
334
- :docs => [string.to_s],
335
- :words => results[:words].keys.join(' '),
336
- :index => options[:index] || "#{model.core_index_names.first}"
337
- }.merge(options[:excerpt_options] || {})
338
- ).first
339
- end
340
-
341
- def search(*args)
342
- args << args.extract_options!.merge(:ignore_default => true)
343
- merge_search ThinkingSphinx::Search.new(*args), self.args, options
344
- self
345
- end
346
-
347
- def search_for_ids(*args)
348
- args << args.extract_options!.merge(
349
- :ignore_default => true,
350
- :ids_only => true
351
- )
352
- merge_search ThinkingSphinx::Search.new(*args), self.args, options
353
- self
354
- end
355
-
356
- def facets(*args)
357
- options = args.extract_options!
358
- merge_search self, args, options
359
- args << options
360
-
361
- ThinkingSphinx::FacetSearch.new(*args)
362
- end
363
-
364
- def client
365
- client = options[:client] || config.client
366
-
367
- prepare client
368
- end
369
-
370
- def append_to(client)
371
- prepare client
372
- client.append_query query, indexes, comment
373
- client.reset
374
- end
375
-
376
- def populate_from_queue(results)
377
- return if @populated
378
- @populated = true
379
- @results = results
380
-
381
- compose_results
382
- end
383
-
384
- private
385
-
386
- def config
387
- ThinkingSphinx::Configuration.instance
388
- end
389
-
390
- def populate
391
- return if @populated
392
- @populated = true
393
-
394
- retry_on_stale_index do
395
- begin
396
- log query do
397
- @results = client.query query, indexes, comment
398
- end
399
- total = @results[:total_found].to_i
400
- log "Found #{total} result#{'s' unless total == 1}"
401
-
402
- log "Sphinx Daemon returned warning: #{warning}" if warning?
403
-
404
- if error?
405
- log "Sphinx Daemon returned error: #{error}"
406
- raise SphinxError.new(error, @results) unless options[:ignore_errors]
407
- end
408
- rescue Errno::ECONNREFUSED => err
409
- raise ThinkingSphinx::ConnectionError,
410
- 'Connection to Sphinx Daemon (searchd) failed.'
411
- end
412
-
413
- compose_results
414
- end
415
- end
416
-
417
- def compose_results
418
- if options[:ids_only]
419
- compose_ids_results
420
- elsif options[:only]
421
- compose_only_results
422
- else
423
- replace instances_from_matches
424
- add_excerpter
425
- add_sphinx_attributes
426
- add_matching_fields if client.rank_mode == :fieldmask
427
- end
428
- end
429
-
430
- def compose_ids_results
431
- replace @results[:matches].collect { |match|
432
- match[:attributes]['sphinx_internal_id']
433
- }
434
- end
435
-
436
- def compose_only_results
437
- replace @results[:matches].collect { |match|
438
- case only = options[:only]
439
- when String, Symbol
440
- match[:attributes][only.to_s]
441
- when Array
442
- only.inject({}) do |hash, attribute|
443
- hash[attribute.to_sym] = match[:attributes][attribute.to_s]
444
- hash
445
- end
446
- else
447
- raise "Unexpected object for :only argument. String or Array is expected, #{only.class} was received."
448
- end
449
- }
450
- end
451
-
452
- def add_excerpter
453
- each do |object|
454
- next if object.nil?
455
-
456
- object.excerpts = ThinkingSphinx::Excerpter.new self, object
457
- end
458
- end
459
-
460
- def add_sphinx_attributes
461
- each do |object|
462
- next if object.nil?
463
-
464
- match = match_hash object
465
- next if match.nil?
466
-
467
- object.sphinx_attributes = match[:attributes]
468
- end
469
- end
470
-
471
- def add_matching_fields
472
- each do |object|
473
- next if object.nil?
474
-
475
- match = match_hash object
476
- next if match.nil?
477
- object.matching_fields = ThinkingSphinx::Search.matching_fields(
478
- @results[:fields], match[:weight]
479
- )
480
- end
481
- end
482
-
483
- def match_hash(object)
484
- @results[:matches].detect { |match|
485
- class_crc = object.class.name
486
- class_crc = object.class.to_crc32 if Riddle.loaded_version.to_i < 2
487
-
488
- match[:attributes]['sphinx_internal_id'] == object.
489
- primary_key_for_sphinx &&
490
- match[:attributes][crc_attribute] == class_crc
491
- }
492
- end
493
-
494
- def self.log(message, &block)
495
- if ThinkingSphinx::ActiveRecord::LogSubscriber.logger.nil?
496
- yield if block_given?
497
- return
498
- end
499
-
500
- if block_given?
501
- ::ActiveSupport::Notifications.
502
- instrument('query.thinking_sphinx', :query => message, &block)
503
- else
504
- ::ActiveSupport::Notifications.
505
- instrument('message.thinking_sphinx', :message => message)
506
- end
507
- end
508
-
509
- def log(query, &block)
510
- self.class.log(query, &block)
511
- end
512
-
513
- def prepare(client)
514
- index_options = {}
515
- if one_class && one_class.sphinx_indexes && one_class.sphinx_indexes.first
516
- index_options = one_class.sphinx_indexes.first.local_options
517
- end
518
-
519
- [
520
- :max_matches, :group_by, :group_function, :group_clause,
521
- :group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
522
- :rank_mode, :max_query_time, :field_weights
523
- ].each do |key|
524
- value = options[key] || index_options[key]
525
- client.send("#{key}=", value) if value
526
- end
527
-
528
- # treated non-standard as :select is already used for AR queries
529
- client.select = options[:sphinx_select] || '*'
530
-
531
- client.limit = per_page
532
- client.offset = offset
533
- client.match_mode = match_mode
534
- client.filters = filters
535
- client.sort_mode = sort_mode
536
- client.sort_by = sort_by
537
- client.group_by = group_by if group_by
538
- client.group_function = group_function if group_function
539
- client.index_weights = index_weights
540
- client.anchor = anchor
541
-
542
- client
543
- end
544
-
545
- def retry_on_stale_index(&block)
546
- stale_ids = []
547
- retries = stale_retries
548
-
549
- begin
550
- options[:raise_on_stale] = retries > 0
551
- block.call
552
-
553
- # If ThinkingSphinx::Search#instances_from_matches found records in
554
- # Sphinx but not in the DB and the :raise_on_stale option is set, this
555
- # exception is raised. We retry a limited number of times, excluding the
556
- # stale ids from the search.
557
- rescue StaleIdsException => err
558
- retries -= 1
559
-
560
- # For logging
561
- stale_ids |= err.ids
562
- # ID exclusion
563
- options[:without_ids] = Array(options[:without_ids]) | err.ids
564
-
565
- log 'Stale Ids (%s %s left): %s' % [
566
- retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
567
- ]
568
- retry
569
- end
570
- end
571
-
572
- def classes
573
- @classes ||= options[:classes] || []
574
- end
575
-
576
- def one_class
577
- @one_class ||= classes.length != 1 ? nil : classes.first
578
- end
579
-
580
- def query
581
- @query ||= begin
582
- q = @args.join(' ') << conditions_as_query
583
- (options[:star] ? star_query(q) : q).strip
584
- end
585
- end
586
-
587
- def conditions_as_query
588
- return '' if @options[:conditions].blank?
589
-
590
- ' ' + @options[:conditions].keys.collect { |key|
591
- "@#{key} #{options[:conditions][key]}"
592
- }.join(' ')
593
- end
594
-
595
- def star_query(query)
596
- token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
597
-
598
- query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
599
- pre, proper, post = $`, $&, $'
600
- # E.g. "@foo", "/2", "~3", but not as part of a token
601
- is_operator = pre.match(%r{(\W|^)[@~/]\Z}) ||
602
- pre.match(%r{(\W|^)@\([^\)]*$})
603
- # E.g. "foo bar", with quotes
604
- is_quote = proper.starts_with?('"') && proper.ends_with?('"')
605
- has_star = pre.ends_with?("*") || post.starts_with?("*")
606
- if is_operator || is_quote || has_star
607
- proper
608
- else
609
- "*#{proper}*"
610
- end
611
- end
612
- end
613
-
614
- def comment
615
- options[:comment] || ''
616
- end
617
-
618
- def match_mode
619
- options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
620
- end
621
-
622
- def sort_mode
623
- @sort_mode ||= case options[:sort_mode]
624
- when :asc
625
- :attr_asc
626
- when :desc
627
- :attr_desc
628
- when nil
629
- case options[:order]
630
- when String
631
- :extended
632
- when Symbol
633
- :attr_asc
634
- else
635
- :relevance
636
- end
637
- else
638
- options[:sort_mode]
639
- end
640
- end
641
-
642
- def sort_by
643
- case @sort_by = (options[:sort_by] || options[:order])
644
- when String
645
- sorted_fields_to_attributes(@sort_by.clone)
646
- when Symbol
647
- field_names.include?(@sort_by) ?
648
- @sort_by.to_s.concat('_sort') : @sort_by.to_s
649
- else
650
- ''
651
- end
652
- end
653
-
654
- def field_names
655
- return [] unless one_class
656
-
657
- one_class.sphinx_indexes.collect { |index|
658
- index.fields.collect { |field| field.unique_name }
659
- }.flatten
660
- end
661
-
662
- def sorted_fields_to_attributes(order_string)
663
- field_names.each { |field|
664
- order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
665
- match.gsub field.to_s, field.to_s.concat("_sort")
666
- }
667
- }
668
-
669
- order_string
670
- end
671
-
672
- # Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
673
- # { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
674
- #
675
- def index_weights
676
- weights = options[:index_weights] || {}
677
- weights.keys.inject({}) do |hash, key|
678
- if key.is_a?(Class)
679
- name = ThinkingSphinx::Index.name_for(key)
680
- hash["#{name}_core"] = weights[key]
681
- hash["#{name}_delta"] = weights[key]
682
- else
683
- hash[key] = weights[key]
684
- end
685
-
686
- hash
687
- end
688
- end
689
-
690
- def group_by
691
- options[:group] ? options[:group].to_s : nil
692
- end
693
-
694
- def group_function
695
- options[:group] ? :attr : nil
696
- end
697
-
698
- def internal_filters
699
- filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
700
-
701
- class_crcs = classes.collect { |klass|
702
- klass.to_crc32s
703
- }.flatten
704
-
705
- unless class_crcs.empty?
706
- filters << Riddle::Client::Filter.new('class_crc', class_crcs)
707
- end
708
-
709
- filters << Riddle::Client::Filter.new(
710
- 'sphinx_internal_id', filter_value(options[:without_ids]), true
711
- ) if options[:without_ids]
712
-
713
- filters
714
- end
715
-
716
- def filters
717
- internal_filters +
718
- (options[:with] || {}).collect { |attrib, value|
719
- Riddle::Client::Filter.new attrib.to_s, filter_value(value)
720
- } +
721
- (options[:without] || {}).collect { |attrib, value|
722
- Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
723
- } +
724
- (options[:with_all] || {}).collect { |attrib, values|
725
- Array(values).collect { |value|
726
- Riddle::Client::Filter.new attrib.to_s, filter_value(value)
727
- }
728
- }.flatten +
729
- (options[:without_any] || {}).collect { |attrib, values|
730
- Array(values).collect { |value|
731
- Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
732
- }
733
- }.flatten
734
- end
735
-
736
- # When passed a Time instance, returns the integer timestamp.
737
- def filter_value(value)
738
- case value
739
- when Range
740
- filter_value(value.first).first..filter_value(value.last).first
741
- when Array
742
- value.collect { |v| filter_value(v) }.flatten
743
- when Time
744
- [value.to_i]
745
- when NilClass
746
- 0
747
- else
748
- Array(value)
749
- end
750
- end
751
-
752
- def anchor
753
- return {} unless options[:geo] || (options[:lat] && options[:lng])
754
-
755
- {
756
- :latitude => options[:geo] ? options[:geo].first : options[:lat],
757
- :longitude => options[:geo] ? options[:geo].last : options[:lng],
758
- :latitude_attribute => latitude_attr.to_s,
759
- :longitude_attribute => longitude_attr.to_s
760
- }
761
- end
762
-
763
- def latitude_attr
764
- options[:latitude_attr] ||
765
- index_option(:latitude_attr) ||
766
- attribute(:lat, :latitude)
767
- end
768
-
769
- def longitude_attr
770
- options[:longitude_attr] ||
771
- index_option(:longitude_attr) ||
772
- attribute(:lon, :lng, :longitude)
773
- end
774
-
775
- def index_option(key)
776
- return nil unless one_class
777
-
778
- one_class.sphinx_indexes.collect { |index|
779
- index.local_options[key]
780
- }.compact.first
781
- end
782
-
783
- def attribute(*keys)
784
- return nil unless one_class
785
-
786
- keys.detect { |key|
787
- attributes.include?(key)
788
- }
789
- end
790
-
791
- def attributes
792
- return [] unless one_class
793
-
794
- attributes = one_class.sphinx_indexes.collect { |index|
795
- index.attributes.collect { |attrib| attrib.unique_name }
796
- }.flatten
797
- end
798
-
799
- def stale_retries
800
- case options[:retry_stale]
801
- when TrueClass
802
- 3
803
- when nil, FalseClass
804
- 0
805
- else
806
- options[:retry_stale].to_i
807
- end
808
- end
809
-
810
- def include_for_class(klass)
811
- includes = options[:include] || klass.sphinx_index_options[:include]
812
-
813
- case includes
814
- when NilClass
815
- nil
816
- when Array
817
- include_from_array includes, klass
818
- when Symbol
819
- klass.reflections[includes].nil? ? nil : includes
820
- when Hash
821
- include_from_hash includes, klass
822
- else
823
- includes
824
- end
825
- end
826
-
827
- def include_from_array(array, klass)
828
- scoped_array = []
829
- array.each do |value|
830
- case value
831
- when Hash
832
- scoped_hash = include_from_hash(value, klass)
833
- scoped_array << scoped_hash unless scoped_hash.nil?
834
- else
835
- scoped_array << value unless klass.reflections[value].nil?
836
- end
837
- end
838
- scoped_array.empty? ? nil : scoped_array
839
- end
840
-
841
- def include_from_hash(hash, klass)
842
- scoped_hash = {}
843
- hash.keys.each do |key|
844
- scoped_hash[key] = hash[key] unless klass.reflections[key].nil?
845
- end
846
- scoped_hash.empty? ? nil : scoped_hash
847
- end
848
-
849
- def instances_from_class(klass, matches)
850
- index_options = klass.sphinx_index_options
851
-
852
- ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
853
- instances = ids.length > 0 ? klass.find(
854
- :all,
855
- :joins => options[:joins],
856
- :conditions => {klass.primary_key_for_sphinx.to_sym => ids},
857
- :include => include_for_class(klass),
858
- :select => (options[:select] || index_options[:select]),
859
- :order => (options[:sql_order] || index_options[:sql_order])
860
- ) : []
861
-
862
- # Raise an exception if we find records in Sphinx but not in the DB, so
863
- # the search method can retry without them. See
864
- # ThinkingSphinx::Search.retry_search_on_stale_index.
865
- if options[:raise_on_stale] && instances.length < ids.length
866
- stale_ids = ids - instances.map { |i| i.id }
867
- raise StaleIdsException, stale_ids
868
- end
869
-
870
- # if the user has specified an SQL order, return the collection
871
- # without rearranging it into the Sphinx order
872
- return instances if (options[:sql_order] || index_options[:sql_order])
873
-
874
- ids.collect { |obj_id|
875
- instances.detect do |obj|
876
- obj.primary_key_for_sphinx == obj_id
877
- end
878
- }
879
- end
880
-
881
- # Group results by class and call #find(:all) once for each group to reduce
882
- # the number of #find's in multi-model searches.
883
- #
884
- def instances_from_matches
885
- return single_class_results if one_class
886
-
887
- groups = results[:matches].group_by { |match|
888
- match[:attributes][crc_attribute]
889
- }
890
- groups.each do |crc, group|
891
- group.replace(
892
- instances_from_class(class_from_crc(crc), group)
893
- )
894
- end
895
-
896
- results[:matches].collect do |match|
897
- groups.detect { |crc, group|
898
- crc == match[:attributes][crc_attribute]
899
- }[1].compact.detect { |obj|
900
- obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
901
- }
902
- end
903
- end
904
-
905
- def single_class_results
906
- instances_from_class one_class, results[:matches]
907
- end
908
-
909
- def class_from_crc(crc)
910
- if Riddle.loaded_version.to_i < 2
911
- config.models_by_crc[crc].constantize
912
- else
913
- crc.constantize
914
- end
915
- end
916
-
917
- def each_with_attribute(attribute, &block)
918
- populate
919
- results[:matches].each_with_index do |match, index|
920
- yield self[index],
921
- (match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
922
- end
923
- end
924
-
925
- def is_scope?(method)
926
- one_class && one_class.sphinx_scopes.include?(method)
927
- end
928
-
929
- # Adds the default_sphinx_scope if set.
930
- def add_default_scope
931
- return unless one_class && one_class.has_default_sphinx_scope?
932
- add_scope(one_class.get_default_sphinx_scope.to_sym)
933
- end
934
-
935
- def add_scope(method, *args, &block)
936
- method = "#{method}_without_default".to_sym
937
- merge_search one_class.send(method, *args, &block), self.args, options
938
- end
939
-
940
- def merge_search(search, args, options)
941
- search.args.each { |arg| args << arg }
942
-
943
- search.options.keys.each do |key|
944
- if HashOptions.include?(key)
945
- options[key] ||= {}
946
- options[key].merge! search.options[key]
947
- elsif ArrayOptions.include?(key)
948
- options[key] ||= []
949
- options[key] += search.options[key]
950
- options[key].uniq!
951
- else
952
- options[key] = search.options[key]
953
- end
954
- end
955
- end
956
-
957
- def scoped_count
958
- return self.total_entries if(@options[:ids_only] || @options[:only])
959
-
960
- @options[:ids_only] = true
961
- results_count = self.total_entries
962
- @options[:ids_only] = false
963
- @populated = false
964
-
965
- results_count
966
- end
967
-
968
- def crc_attribute
969
- Riddle.loaded_version.to_i < 2 ? 'class_crc' : 'sphinx_internal_class'
970
- end
971
- end
972
- end