thinking-sphinx 2.0.6 → 2.0.7

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 (50) hide show
  1. data/HISTORY +157 -0
  2. data/lib/cucumber/thinking_sphinx/external_world.rb +12 -0
  3. data/lib/cucumber/thinking_sphinx/internal_world.rb +127 -0
  4. data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
  5. data/lib/thinking-sphinx.rb +1 -0
  6. data/lib/thinking_sphinx/action_controller.rb +31 -0
  7. data/lib/thinking_sphinx/active_record/attribute_updates.rb +53 -0
  8. data/lib/thinking_sphinx/active_record/collection_proxy.rb +40 -0
  9. data/lib/thinking_sphinx/active_record/collection_proxy_with_scopes.rb +27 -0
  10. data/lib/thinking_sphinx/active_record/delta.rb +65 -0
  11. data/lib/thinking_sphinx/active_record/has_many_association.rb +37 -0
  12. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +21 -0
  13. data/lib/thinking_sphinx/active_record/log_subscriber.rb +61 -0
  14. data/lib/thinking_sphinx/active_record/scopes.rb +110 -0
  15. data/lib/thinking_sphinx/active_record.rb +383 -0
  16. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +87 -0
  17. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +62 -0
  18. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +171 -0
  19. data/lib/thinking_sphinx/association.rb +229 -0
  20. data/lib/thinking_sphinx/attribute.rb +407 -0
  21. data/lib/thinking_sphinx/auto_version.rb +38 -0
  22. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  23. data/lib/thinking_sphinx/class_facet.rb +20 -0
  24. data/lib/thinking_sphinx/configuration.rb +335 -0
  25. data/lib/thinking_sphinx/context.rb +77 -0
  26. data/lib/thinking_sphinx/core/string.rb +15 -0
  27. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  28. data/lib/thinking_sphinx/deltas.rb +28 -0
  29. data/lib/thinking_sphinx/deploy/capistrano.rb +99 -0
  30. data/lib/thinking_sphinx/excerpter.rb +23 -0
  31. data/lib/thinking_sphinx/facet.rb +128 -0
  32. data/lib/thinking_sphinx/facet_search.rb +170 -0
  33. data/lib/thinking_sphinx/field.rb +98 -0
  34. data/lib/thinking_sphinx/index/builder.rb +312 -0
  35. data/lib/thinking_sphinx/index/faux_column.rb +118 -0
  36. data/lib/thinking_sphinx/index.rb +157 -0
  37. data/lib/thinking_sphinx/join.rb +37 -0
  38. data/lib/thinking_sphinx/property.rb +185 -0
  39. data/lib/thinking_sphinx/railtie.rb +46 -0
  40. data/lib/thinking_sphinx/search.rb +995 -0
  41. data/lib/thinking_sphinx/search_methods.rb +439 -0
  42. data/lib/thinking_sphinx/sinatra.rb +7 -0
  43. data/lib/thinking_sphinx/source/internal_properties.rb +51 -0
  44. data/lib/thinking_sphinx/source/sql.rb +157 -0
  45. data/lib/thinking_sphinx/source.rb +194 -0
  46. data/lib/thinking_sphinx/tasks.rb +132 -0
  47. data/lib/thinking_sphinx/test.rb +55 -0
  48. data/lib/thinking_sphinx/version.rb +3 -0
  49. data/lib/thinking_sphinx.rb +296 -0
  50. metadata +53 -4
@@ -0,0 +1,118 @@
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
@@ -0,0 +1,157 @@
1
+ require 'thinking_sphinx/index/builder'
2
+ require 'thinking_sphinx/index/faux_column'
3
+
4
+ module ThinkingSphinx
5
+ class Index
6
+ attr_accessor :name, :model, :sources, :delta_object
7
+
8
+ # Create a new index instance by passing in the model it is tied to, and
9
+ # a block to build it with (optional but recommended). For documentation
10
+ # on the syntax for inside the block, the Builder class is what you want.
11
+ #
12
+ # Quick Example:
13
+ #
14
+ # Index.new(User) do
15
+ # indexes login, email
16
+ #
17
+ # has created_at
18
+ #
19
+ # set_property :delta => true
20
+ # end
21
+ #
22
+ def initialize(model, &block)
23
+ @name = self.class.name_for model
24
+ @model = model
25
+ @sources = []
26
+ @options = {}
27
+ @delta_object = nil
28
+ end
29
+
30
+ def fields
31
+ @sources.collect { |source| source.fields }.flatten
32
+ end
33
+
34
+ def attributes
35
+ @sources.collect { |source| source.attributes }.flatten
36
+ end
37
+
38
+ def core_name
39
+ "#{name}_core"
40
+ end
41
+
42
+ def delta_name
43
+ "#{name}_delta"
44
+ end
45
+
46
+ def all_names
47
+ names = [core_name]
48
+ names << delta_name if delta?
49
+
50
+ names
51
+ end
52
+
53
+ def self.name_for(model)
54
+ model.name.underscore.tr(':/\\', '_')
55
+ end
56
+
57
+ def prefix_fields
58
+ fields.select { |field| field.prefixes }
59
+ end
60
+
61
+ def infix_fields
62
+ fields.select { |field| field.infixes }
63
+ end
64
+
65
+ def local_options
66
+ @options
67
+ end
68
+
69
+ def options
70
+ all_index_options = config.index_options.clone
71
+ @options.keys.select { |key|
72
+ ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s) ||
73
+ ThinkingSphinx::Configuration::CustomOptions.include?(key.to_s)
74
+ }.each { |key| all_index_options[key.to_sym] = @options[key] }
75
+ all_index_options
76
+ end
77
+
78
+ def delta?
79
+ !@delta_object.nil?
80
+ end
81
+
82
+ def to_riddle(offset)
83
+ indexes = [to_riddle_for_core(offset)]
84
+ indexes << to_riddle_for_delta(offset) if delta?
85
+ indexes << to_riddle_for_distributed
86
+ end
87
+
88
+ private
89
+
90
+ def adapter
91
+ @adapter ||= @model.sphinx_database_adapter
92
+ end
93
+
94
+ def utf8?
95
+ options[:charset_type] == "utf-8"
96
+ end
97
+
98
+ def sql_query_pre_for_delta
99
+ [""]
100
+ end
101
+
102
+ def config
103
+ @config ||= ThinkingSphinx::Configuration.instance
104
+ end
105
+
106
+ def to_riddle_for_core(offset)
107
+ index = Riddle::Configuration::Index.new core_name
108
+ index.path = File.join config.searchd_file_path, index.name
109
+
110
+ set_configuration_options_for_indexes index
111
+ set_field_settings_for_indexes index
112
+
113
+ sources.each_with_index do |source, i|
114
+ index.sources << source.to_riddle_for_core(offset, i)
115
+ end
116
+
117
+ index
118
+ end
119
+
120
+ def to_riddle_for_delta(offset)
121
+ index = Riddle::Configuration::Index.new delta_name
122
+ index.parent = core_name
123
+ index.path = File.join config.searchd_file_path, index.name
124
+
125
+ sources.each_with_index do |source, i|
126
+ index.sources << source.to_riddle_for_delta(offset, i)
127
+ end
128
+
129
+ index
130
+ end
131
+
132
+ def to_riddle_for_distributed
133
+ index = Riddle::Configuration::DistributedIndex.new name
134
+ index.local_indexes << core_name
135
+ index.local_indexes.unshift delta_name if delta?
136
+ index
137
+ end
138
+
139
+ def set_configuration_options_for_indexes(index)
140
+ config.index_options.each do |key, value|
141
+ method = "#{key}=".to_sym
142
+ index.send(method, value) if index.respond_to?(method)
143
+ end
144
+
145
+ options.each do |key, value|
146
+ index.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s) && !value.nil?
147
+ end
148
+ end
149
+
150
+ def set_field_settings_for_indexes(index)
151
+ field_names = lambda { |field| field.unique_name.to_s }
152
+
153
+ index.prefix_field_names += prefix_fields.collect(&field_names)
154
+ index.infix_field_names += infix_fields.collect(&field_names)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,185 @@
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
@@ -0,0 +1,46 @@
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