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.
Files changed (199) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +201 -0
  3. data/Rakefile +3 -0
  4. data/VERSION +1 -0
  5. data/contribute.rb +385 -0
  6. data/cucumber.yml +1 -0
  7. data/features/abstract_inheritance.feature +10 -0
  8. data/features/alternate_primary_key.feature +27 -0
  9. data/features/attribute_transformation.feature +22 -0
  10. data/features/attribute_updates.feature +51 -0
  11. data/features/deleting_instances.feature +67 -0
  12. data/features/direct_attributes.feature +11 -0
  13. data/features/excerpts.feature +13 -0
  14. data/features/extensible_delta_indexing.feature +9 -0
  15. data/features/facets.feature +82 -0
  16. data/features/facets_across_model.feature +29 -0
  17. data/features/handling_edits.feature +92 -0
  18. data/features/retry_stale_indexes.feature +24 -0
  19. data/features/searching_across_models.feature +20 -0
  20. data/features/searching_by_index.feature +40 -0
  21. data/features/searching_by_model.feature +175 -0
  22. data/features/searching_with_find_arguments.feature +56 -0
  23. data/features/sphinx_detection.feature +25 -0
  24. data/features/sphinx_scopes.feature +42 -0
  25. data/features/step_definitions/alpha_steps.rb +16 -0
  26. data/features/step_definitions/beta_steps.rb +7 -0
  27. data/features/step_definitions/common_steps.rb +188 -0
  28. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  29. data/features/step_definitions/facet_steps.rb +96 -0
  30. data/features/step_definitions/find_arguments_steps.rb +36 -0
  31. data/features/step_definitions/gamma_steps.rb +15 -0
  32. data/features/step_definitions/scope_steps.rb +15 -0
  33. data/features/step_definitions/search_steps.rb +89 -0
  34. data/features/step_definitions/sphinx_steps.rb +35 -0
  35. data/features/sti_searching.feature +19 -0
  36. data/features/support/database.example.yml +3 -0
  37. data/features/support/db/.gitignore +1 -0
  38. data/features/support/db/fixtures/alphas.rb +10 -0
  39. data/features/support/db/fixtures/authors.rb +1 -0
  40. data/features/support/db/fixtures/betas.rb +10 -0
  41. data/features/support/db/fixtures/boxes.rb +9 -0
  42. data/features/support/db/fixtures/categories.rb +1 -0
  43. data/features/support/db/fixtures/cats.rb +3 -0
  44. data/features/support/db/fixtures/comments.rb +24 -0
  45. data/features/support/db/fixtures/developers.rb +29 -0
  46. data/features/support/db/fixtures/dogs.rb +3 -0
  47. data/features/support/db/fixtures/extensible_betas.rb +10 -0
  48. data/features/support/db/fixtures/foxes.rb +3 -0
  49. data/features/support/db/fixtures/gammas.rb +10 -0
  50. data/features/support/db/fixtures/music.rb +4 -0
  51. data/features/support/db/fixtures/people.rb +1001 -0
  52. data/features/support/db/fixtures/posts.rb +6 -0
  53. data/features/support/db/fixtures/robots.rb +14 -0
  54. data/features/support/db/fixtures/tags.rb +27 -0
  55. data/features/support/db/migrations/create_alphas.rb +8 -0
  56. data/features/support/db/migrations/create_animals.rb +5 -0
  57. data/features/support/db/migrations/create_authors.rb +3 -0
  58. data/features/support/db/migrations/create_authors_posts.rb +6 -0
  59. data/features/support/db/migrations/create_betas.rb +5 -0
  60. data/features/support/db/migrations/create_boxes.rb +5 -0
  61. data/features/support/db/migrations/create_categories.rb +3 -0
  62. data/features/support/db/migrations/create_comments.rb +10 -0
  63. data/features/support/db/migrations/create_developers.rb +9 -0
  64. data/features/support/db/migrations/create_extensible_betas.rb +5 -0
  65. data/features/support/db/migrations/create_gammas.rb +3 -0
  66. data/features/support/db/migrations/create_genres.rb +3 -0
  67. data/features/support/db/migrations/create_music.rb +6 -0
  68. data/features/support/db/migrations/create_people.rb +13 -0
  69. data/features/support/db/migrations/create_posts.rb +5 -0
  70. data/features/support/db/migrations/create_robots.rb +4 -0
  71. data/features/support/db/migrations/create_taggings.rb +5 -0
  72. data/features/support/db/migrations/create_tags.rb +4 -0
  73. data/features/support/env.rb +21 -0
  74. data/features/support/lib/generic_delta_handler.rb +8 -0
  75. data/features/support/models/alpha.rb +22 -0
  76. data/features/support/models/animal.rb +5 -0
  77. data/features/support/models/author.rb +3 -0
  78. data/features/support/models/beta.rb +8 -0
  79. data/features/support/models/box.rb +8 -0
  80. data/features/support/models/cat.rb +3 -0
  81. data/features/support/models/category.rb +4 -0
  82. data/features/support/models/comment.rb +10 -0
  83. data/features/support/models/developer.rb +16 -0
  84. data/features/support/models/dog.rb +3 -0
  85. data/features/support/models/extensible_beta.rb +9 -0
  86. data/features/support/models/fox.rb +5 -0
  87. data/features/support/models/gamma.rb +5 -0
  88. data/features/support/models/genre.rb +3 -0
  89. data/features/support/models/medium.rb +5 -0
  90. data/features/support/models/music.rb +8 -0
  91. data/features/support/models/person.rb +23 -0
  92. data/features/support/models/post.rb +21 -0
  93. data/features/support/models/robot.rb +12 -0
  94. data/features/support/models/tag.rb +3 -0
  95. data/features/support/models/tagging.rb +4 -0
  96. data/ginger_scenarios.rb +28 -0
  97. data/init.rb +5 -0
  98. data/install.rb +5 -0
  99. data/lib/cucumber/thinking_sphinx/external_world.rb +8 -0
  100. data/lib/cucumber/thinking_sphinx/internal_world.rb +126 -0
  101. data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
  102. data/lib/thinking_sphinx/active_record/attribute_updates.rb +19 -0
  103. data/lib/thinking_sphinx/active_record/delta.rb +47 -0
  104. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  105. data/lib/thinking_sphinx/active_record/scopes.rb +75 -0
  106. data/lib/thinking_sphinx/active_record.rb +348 -0
  107. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  108. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  109. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +143 -0
  110. data/lib/thinking_sphinx/association.rb +164 -0
  111. data/lib/thinking_sphinx/attribute.rb +362 -0
  112. data/lib/thinking_sphinx/auto_version.rb +22 -0
  113. data/lib/thinking_sphinx/class_facet.rb +15 -0
  114. data/lib/thinking_sphinx/configuration.rb +300 -0
  115. data/lib/thinking_sphinx/context.rb +68 -0
  116. data/lib/thinking_sphinx/core/array.rb +7 -0
  117. data/lib/thinking_sphinx/core/string.rb +15 -0
  118. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  119. data/lib/thinking_sphinx/deltas.rb +28 -0
  120. data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
  121. data/lib/thinking_sphinx/excerpter.rb +22 -0
  122. data/lib/thinking_sphinx/facet.rb +125 -0
  123. data/lib/thinking_sphinx/facet_search.rb +136 -0
  124. data/lib/thinking_sphinx/field.rb +82 -0
  125. data/lib/thinking_sphinx/index/builder.rb +296 -0
  126. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  127. data/lib/thinking_sphinx/index.rb +157 -0
  128. data/lib/thinking_sphinx/property.rb +162 -0
  129. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  130. data/lib/thinking_sphinx/search.rb +769 -0
  131. data/lib/thinking_sphinx/search_methods.rb +439 -0
  132. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  133. data/lib/thinking_sphinx/source/sql.rb +130 -0
  134. data/lib/thinking_sphinx/source.rb +153 -0
  135. data/lib/thinking_sphinx/tasks.rb +131 -0
  136. data/lib/thinking_sphinx/test.rb +52 -0
  137. data/lib/thinking_sphinx.rb +225 -0
  138. data/rails/init.rb +16 -0
  139. data/recipes/thinking_sphinx.rb +3 -0
  140. data/spec/fixtures/data.sql +32 -0
  141. data/spec/fixtures/database.yml.default +3 -0
  142. data/spec/fixtures/models.rb +145 -0
  143. data/spec/fixtures/structure.sql +125 -0
  144. data/spec/spec_helper.rb +60 -0
  145. data/spec/sphinx_helper.rb +81 -0
  146. data/spec/thinking_sphinx/active_record/delta_spec.rb +128 -0
  147. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +55 -0
  148. data/spec/thinking_sphinx/active_record/scopes_spec.rb +177 -0
  149. data/spec/thinking_sphinx/active_record_spec.rb +622 -0
  150. data/spec/thinking_sphinx/association_spec.rb +239 -0
  151. data/spec/thinking_sphinx/attribute_spec.rb +570 -0
  152. data/spec/thinking_sphinx/auto_version_spec.rb +39 -0
  153. data/spec/thinking_sphinx/configuration_spec.rb +234 -0
  154. data/spec/thinking_sphinx/context_spec.rb +119 -0
  155. data/spec/thinking_sphinx/core/array_spec.rb +9 -0
  156. data/spec/thinking_sphinx/core/string_spec.rb +9 -0
  157. data/spec/thinking_sphinx/excerpter_spec.rb +57 -0
  158. data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
  159. data/spec/thinking_sphinx/facet_spec.rb +333 -0
  160. data/spec/thinking_sphinx/field_spec.rb +154 -0
  161. data/spec/thinking_sphinx/index/builder_spec.rb +479 -0
  162. data/spec/thinking_sphinx/index/faux_column_spec.rb +30 -0
  163. data/spec/thinking_sphinx/index_spec.rb +183 -0
  164. data/spec/thinking_sphinx/rails_additions_spec.rb +203 -0
  165. data/spec/thinking_sphinx/search_methods_spec.rb +152 -0
  166. data/spec/thinking_sphinx/search_spec.rb +1181 -0
  167. data/spec/thinking_sphinx/source_spec.rb +235 -0
  168. data/spec/thinking_sphinx_spec.rb +204 -0
  169. data/tasks/distribution.rb +41 -0
  170. data/tasks/rails.rake +1 -0
  171. data/tasks/testing.rb +72 -0
  172. data/vendor/after_commit/.gitignore +1 -0
  173. data/vendor/after_commit/lib/after_commit/active_record.rb +122 -0
  174. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +168 -0
  175. data/vendor/after_commit/lib/after_commit/test_bypass.rb +30 -0
  176. data/vendor/after_commit/lib/after_commit.rb +70 -0
  177. data/vendor/riddle/lib/riddle/0.9.8.rb +1 -0
  178. data/vendor/riddle/lib/riddle/0.9.9/client/filter.rb +22 -0
  179. data/vendor/riddle/lib/riddle/0.9.9/client.rb +49 -0
  180. data/vendor/riddle/lib/riddle/0.9.9/configuration/searchd.rb +28 -0
  181. data/vendor/riddle/lib/riddle/0.9.9.rb +7 -0
  182. data/vendor/riddle/lib/riddle/auto_version.rb +11 -0
  183. data/vendor/riddle/lib/riddle/client/filter.rb +62 -0
  184. data/vendor/riddle/lib/riddle/client/message.rb +70 -0
  185. data/vendor/riddle/lib/riddle/client/response.rb +94 -0
  186. data/vendor/riddle/lib/riddle/client.rb +745 -0
  187. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +49 -0
  188. data/vendor/riddle/lib/riddle/configuration/index.rb +149 -0
  189. data/vendor/riddle/lib/riddle/configuration/indexer.rb +20 -0
  190. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  191. data/vendor/riddle/lib/riddle/configuration/searchd.rb +28 -0
  192. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  193. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  194. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +53 -0
  195. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +29 -0
  196. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  197. data/vendor/riddle/lib/riddle/controller.rb +78 -0
  198. data/vendor/riddle/lib/riddle.rb +51 -0
  199. metadata +312 -0
@@ -0,0 +1,143 @@
1
+ module ThinkingSphinx
2
+ class PostgreSQLAdapter < AbstractAdapter
3
+ def setup
4
+ create_array_accum_function
5
+ create_crc32_function
6
+ end
7
+
8
+ def sphinx_identifier
9
+ "pgsql"
10
+ end
11
+
12
+ def concatenate(clause, separator = ' ')
13
+ if clause[/^COALESCE/]
14
+ clause.split('), ').join(") || '#{separator}' || ")
15
+ else
16
+ clause.split(', ').collect { |field|
17
+ "CAST(COALESCE(#{field}, '') as varchar)"
18
+ }.join(" || '#{separator}' || ")
19
+ end
20
+ end
21
+
22
+ def group_concatenate(clause, separator = ' ')
23
+ "array_to_string(array_accum(COALESCE(#{clause}, '0')), '#{separator}')"
24
+ end
25
+
26
+ def cast_to_string(clause)
27
+ clause
28
+ end
29
+
30
+ def cast_to_datetime(clause)
31
+ "cast(extract(epoch from #{clause}) as int)"
32
+ end
33
+
34
+ def cast_to_unsigned(clause)
35
+ clause
36
+ end
37
+
38
+ def convert_nulls(clause, default = '')
39
+ default = case default
40
+ when String
41
+ "'#{default}'"
42
+ when NilClass
43
+ 'NULL'
44
+ when Fixnum
45
+ "#{default}::bigint"
46
+ else
47
+ default
48
+ end
49
+
50
+ "COALESCE(#{clause}, #{default})"
51
+ end
52
+
53
+ def boolean(value)
54
+ value ? 'TRUE' : 'FALSE'
55
+ end
56
+
57
+ def crc(clause, blank_to_null = false)
58
+ clause = "NULLIF(#{clause},'')" if blank_to_null
59
+ "crc32(#{clause})"
60
+ end
61
+
62
+ def utf8_query_pre
63
+ nil
64
+ end
65
+
66
+ def time_difference(diff)
67
+ "current_timestamp - interval '#{diff} seconds'"
68
+ end
69
+
70
+ private
71
+
72
+ def execute(command, output_error = false)
73
+ connection.execute "begin"
74
+ connection.execute "savepoint ts"
75
+ begin
76
+ connection.execute command
77
+ rescue StandardError => err
78
+ puts err if output_error
79
+ connection.execute "rollback to savepoint ts"
80
+ end
81
+ connection.execute "release savepoint ts"
82
+ connection.execute "commit"
83
+ end
84
+
85
+ def create_array_accum_function
86
+ if connection.raw_connection.respond_to?(:server_version) && connection.raw_connection.server_version > 80200
87
+ execute <<-SQL
88
+ CREATE AGGREGATE array_accum (anyelement)
89
+ (
90
+ sfunc = array_append,
91
+ stype = anyarray,
92
+ initcond = '{}'
93
+ );
94
+ SQL
95
+ else
96
+ execute <<-SQL
97
+ CREATE AGGREGATE array_accum
98
+ (
99
+ basetype = anyelement,
100
+ sfunc = array_append,
101
+ stype = anyarray,
102
+ initcond = '{}'
103
+ );
104
+ SQL
105
+ end
106
+ end
107
+
108
+ def create_crc32_function
109
+ execute "CREATE LANGUAGE 'plpgsql';"
110
+ function = <<-SQL
111
+ CREATE OR REPLACE FUNCTION crc32(word text)
112
+ RETURNS bigint AS $$
113
+ DECLARE tmp bigint;
114
+ DECLARE i int;
115
+ DECLARE j int;
116
+ DECLARE word_array bytea;
117
+ BEGIN
118
+ i = 0;
119
+ tmp = 4294967295;
120
+ word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
121
+ LOOP
122
+ tmp = (tmp # get_byte(word_array, i))::bigint;
123
+ i = i + 1;
124
+ j = 0;
125
+ LOOP
126
+ tmp = ((tmp >> 1) # (3988292384 * (tmp & 1)))::bigint;
127
+ j = j + 1;
128
+ IF j >= 8 THEN
129
+ EXIT;
130
+ END IF;
131
+ END LOOP;
132
+ IF i >= char_length(word) THEN
133
+ EXIT;
134
+ END IF;
135
+ END LOOP;
136
+ return (tmp # 4294967295);
137
+ END
138
+ $$ IMMUTABLE STRICT LANGUAGE plpgsql;
139
+ SQL
140
+ execute function, true
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,164 @@
1
+ module ThinkingSphinx
2
+ # Association tracks a specific reflection and join to reference data that
3
+ # isn't in the base model. Very much an internal class for Thinking Sphinx -
4
+ # perhaps because I feel it's not as strong (or simple) as most of the rest.
5
+ #
6
+ class Association
7
+ attr_accessor :parent, :reflection, :join
8
+
9
+ # Create a new association by passing in the parent association, and the
10
+ # corresponding reflection instance. If there is no parent, pass in nil.
11
+ #
12
+ # top = Association.new nil, top_reflection
13
+ # child = Association.new top, child_reflection
14
+ #
15
+ def initialize(parent, reflection)
16
+ @parent, @reflection = parent, reflection
17
+ @children = {}
18
+ end
19
+
20
+ # Get the children associations for a given association name. The only time
21
+ # that there'll actually be more than one association is when the
22
+ # relationship is polymorphic. To keep things simple though, it will always
23
+ # be an Array that gets returned (an empty one if no matches).
24
+ #
25
+ # # where pages is an association on the class tied to the reflection.
26
+ # association.children(:pages)
27
+ #
28
+ def children(assoc)
29
+ @children[assoc] ||= Association.children(@reflection.klass, assoc, self)
30
+ end
31
+
32
+ # Get the children associations for a given class, association name and
33
+ # parent association. Much like the instance method of the same name, it
34
+ # will return an empty array if no associations have the name, and only
35
+ # have multiple association instances if the underlying relationship is
36
+ # polymorphic.
37
+ #
38
+ # Association.children(User, :pages, user_association)
39
+ #
40
+ def self.children(klass, assoc, parent=nil)
41
+ ref = klass.reflect_on_association(assoc)
42
+
43
+ return [] if ref.nil?
44
+ return [Association.new(parent, ref)] unless ref.options[:polymorphic]
45
+
46
+ # association is polymorphic - create associations for each
47
+ # non-polymorphic reflection.
48
+ polymorphic_classes(ref).collect { |klass|
49
+ Association.new parent, ::ActiveRecord::Reflection::AssociationReflection.new(
50
+ ref.macro,
51
+ "#{ref.name}_#{klass.name}".to_sym,
52
+ casted_options(klass, ref),
53
+ ref.active_record
54
+ )
55
+ }
56
+ end
57
+
58
+ # Link up the join for this model from a base join - and set parent
59
+ # associations' joins recursively.
60
+ #
61
+ def join_to(base_join)
62
+ parent.join_to(base_join) if parent && parent.join.nil?
63
+
64
+ @join ||= ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.new(
65
+ @reflection, base_join, parent ? parent.join : base_join.joins.first
66
+ )
67
+ end
68
+
69
+ # Returns the association's join SQL statements - and it replaces
70
+ # ::ts_join_alias:: with the aliased table name so the generated reflection
71
+ # join conditions avoid column name collisions.
72
+ #
73
+ def to_sql
74
+ @join.association_join.gsub(/::ts_join_alias::/,
75
+ "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
76
+ )
77
+ end
78
+
79
+ # Returns true if the association - or a parent - is a has_many or
80
+ # has_and_belongs_to_many.
81
+ #
82
+ def is_many?
83
+ case @reflection.macro
84
+ when :has_many, :has_and_belongs_to_many
85
+ true
86
+ else
87
+ @parent ? @parent.is_many? : false
88
+ end
89
+ end
90
+
91
+ # Returns an array of all the associations that lead to this one - starting
92
+ # with the top level all the way to the current association object.
93
+ #
94
+ def ancestors
95
+ (parent ? parent.ancestors : []) << self
96
+ end
97
+
98
+ def has_column?(column)
99
+ @reflection.klass.column_names.include?(column.to_s)
100
+ end
101
+
102
+ def primary_key_from_reflection
103
+ if @reflection.options[:through]
104
+ @reflection.source_reflection.options[:foreign_key] ||
105
+ @reflection.source_reflection.primary_key_name
106
+ elsif @reflection.macro == :has_and_belongs_to_many
107
+ @reflection.association_foreign_key
108
+ else
109
+ nil
110
+ end
111
+ end
112
+
113
+ def table
114
+ if @reflection.options[:through] ||
115
+ @reflection.macro == :has_and_belongs_to_many
116
+ @join.aliased_join_table_name
117
+ else
118
+ @join.aliased_table_name
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ # Returns all the objects that could be currently instantiated from a
125
+ # polymorphic association. This is pretty damn fast if there's an index on
126
+ # the foreign type column - but if there isn't, it can take a while if you
127
+ # have a lot of data.
128
+ #
129
+ def self.polymorphic_classes(ref)
130
+ ref.active_record.connection.select_all(
131
+ "SELECT DISTINCT #{ref.options[:foreign_type]} " +
132
+ "FROM #{ref.active_record.table_name} " +
133
+ "WHERE #{ref.options[:foreign_type]} IS NOT NULL"
134
+ ).collect { |row|
135
+ row[ref.options[:foreign_type]].constantize
136
+ }
137
+ end
138
+
139
+ # Returns a new set of options for an association that mimics an existing
140
+ # polymorphic relationship for a specific class. It adds a condition to
141
+ # filter by the appropriate object.
142
+ #
143
+ def self.casted_options(klass, ref)
144
+ options = ref.options.clone
145
+ options[:polymorphic] = nil
146
+ options[:class_name] = klass.name
147
+ options[:foreign_key] ||= "#{ref.name}_id"
148
+
149
+ quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
150
+ case options[:conditions]
151
+ when nil
152
+ options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
153
+ when Array
154
+ options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
155
+ when Hash
156
+ options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
157
+ else
158
+ options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
159
+ end
160
+
161
+ options
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,362 @@
1
+ module ThinkingSphinx
2
+ # Attributes - eternally useful when it comes to filtering, sorting or
3
+ # grouping. This class isn't really useful to you unless you're hacking
4
+ # around with the internals of Thinking Sphinx - but hey, don't let that
5
+ # stop you.
6
+ #
7
+ # One key thing to remember - if you're using the attribute manually to
8
+ # generate SQL statements, you'll need to set the base model, and all the
9
+ # associations. Which can get messy. Use Index.link!, it really helps.
10
+ #
11
+ class Attribute < ThinkingSphinx::Property
12
+ attr_accessor :query_source
13
+
14
+ # To create a new attribute, you'll need to pass in either a single Column
15
+ # or an array of them, and some (optional) options.
16
+ #
17
+ # Valid options are:
18
+ # - :as => :alias_name
19
+ # - :type => :attribute_type
20
+ # - :source => :field, :query, :ranged_query
21
+ #
22
+ # Alias is only required in three circumstances: when there's
23
+ # another attribute or field with the same name, when the column name is
24
+ # 'id', or when there's more than one column.
25
+ #
26
+ # Type is not required, unless you want to force a column to be a certain
27
+ # type (but keep in mind the value will not be CASTed in the SQL
28
+ # statements). The only time you really need to use this is when the type
29
+ # can't be figured out by the column - ie: when not actually using a
30
+ # database column as your source.
31
+ #
32
+ # Source is only used for multi-value attributes (MVA). By default this will
33
+ # use a left-join and a group_concat to obtain the values. For better performance
34
+ # during indexing it can be beneficial to let Sphinx use a separate query to retrieve
35
+ # all document,value-pairs.
36
+ # Either :query or :ranged_query will enable this feature, where :ranged_query will cause
37
+ # the query to be executed incremental.
38
+ #
39
+ # Example usage:
40
+ #
41
+ # Attribute.new(
42
+ # Column.new(:created_at)
43
+ # )
44
+ #
45
+ # Attribute.new(
46
+ # Column.new(:posts, :id),
47
+ # :as => :post_ids
48
+ # )
49
+ #
50
+ # Attribute.new(
51
+ # Column.new(:posts, :id),
52
+ # :as => :post_ids,
53
+ # :source => :ranged_query
54
+ # )
55
+ #
56
+ # Attribute.new(
57
+ # [Column.new(:pages, :id), Column.new(:articles, :id)],
58
+ # :as => :content_ids
59
+ # )
60
+ #
61
+ # Attribute.new(
62
+ # Column.new("NOW()"),
63
+ # :as => :indexed_at,
64
+ # :type => :datetime
65
+ # )
66
+ #
67
+ # If you're creating attributes for latitude and longitude, don't forget
68
+ # that Sphinx expects these values to be in radians.
69
+ #
70
+ def initialize(source, columns, options = {})
71
+ super
72
+
73
+ @type = options[:type]
74
+ @query_source = options[:source]
75
+ @crc = options[:crc]
76
+
77
+ @type ||= :multi unless @query_source.nil?
78
+ if @type == :string && @crc
79
+ @type = is_many? ? :multi : :integer
80
+ end
81
+
82
+ source.attributes << self
83
+ end
84
+
85
+ # Get the part of the SELECT clause related to this attribute. Don't forget
86
+ # to set your model and associations first though.
87
+ #
88
+ # This will concatenate strings and arrays of integers, and convert
89
+ # datetimes to timestamps, as needed.
90
+ #
91
+ def to_select_sql
92
+ return nil unless include_as_association?
93
+
94
+ separator = all_ints? || all_datetimes? || @crc ? ',' : ' '
95
+
96
+ clause = @columns.collect { |column|
97
+ part = column_with_prefix(column)
98
+ case type
99
+ when :string
100
+ adapter.convert_nulls(part)
101
+ when :datetime
102
+ adapter.cast_to_datetime(part)
103
+ when :multi
104
+ part = adapter.cast_to_datetime(part) if is_many_datetimes?
105
+ part = adapter.convert_nulls(part, '0') if is_many_ints?
106
+ part
107
+ else
108
+ part
109
+ end
110
+ }.join(', ')
111
+
112
+ clause = adapter.crc(clause) if @crc
113
+ clause = adapter.concatenate(clause, separator) if concat_ws?
114
+ clause = adapter.group_concatenate(clause, separator) if is_many?
115
+
116
+ "#{clause} AS #{quote_column(unique_name)}"
117
+ end
118
+
119
+ def type_to_config
120
+ {
121
+ :multi => :sql_attr_multi,
122
+ :datetime => :sql_attr_timestamp,
123
+ :string => :sql_attr_str2ordinal,
124
+ :float => :sql_attr_float,
125
+ :boolean => :sql_attr_bool,
126
+ :integer => :sql_attr_uint
127
+ }[type]
128
+ end
129
+
130
+ def include_as_association?
131
+ ! (type == :multi && (query_source == :query || query_source == :ranged_query))
132
+ end
133
+
134
+ # Returns the configuration value that should be used for
135
+ # the attribute.
136
+ # Special case is the multi-valued attribute that needs some
137
+ # extra configuration.
138
+ #
139
+ def config_value(offset = nil, delta = false)
140
+ if type == :multi
141
+ multi_config = include_as_association? ? "field" :
142
+ source_value(offset, delta).gsub(/\s+/m, " ").strip
143
+ "uint #{unique_name} from #{multi_config}"
144
+ else
145
+ unique_name
146
+ end
147
+ end
148
+
149
+ # Returns the type of the column. If that's not already set, it returns
150
+ # :multi if there's the possibility of more than one value, :string if
151
+ # there's more than one association, otherwise it figures out what the
152
+ # actual column's datatype is and returns that.
153
+ #
154
+ def type
155
+ @type ||= begin
156
+ base_type = case
157
+ when is_many?, is_many_ints?
158
+ :multi
159
+ when @associations.values.flatten.length > 1
160
+ :string
161
+ else
162
+ translated_type_from_database
163
+ end
164
+
165
+ if base_type == :string && @crc
166
+ base_type = :integer
167
+ else
168
+ @crc = false unless base_type == :multi && is_many_strings? && @crc
169
+ end
170
+
171
+ base_type
172
+ end
173
+ end
174
+
175
+ def updatable?
176
+ [:integer, :datetime, :boolean].include?(type) && !is_string?
177
+ end
178
+
179
+ def live_value(instance)
180
+ object = instance
181
+ column = @columns.first
182
+ column.__stack.each { |method|
183
+ object = object.send(method)
184
+ return sphinx_value(nil) if object.nil?
185
+ }
186
+
187
+ sphinx_value object.send(column.__name)
188
+ end
189
+
190
+ def all_ints?
191
+ all_of_type?(:integer)
192
+ end
193
+
194
+ def all_datetimes?
195
+ all_of_type?(:datetime, :date, :timestamp)
196
+ end
197
+
198
+ def all_strings?
199
+ all_of_type?(:string, :text)
200
+ end
201
+
202
+ private
203
+
204
+ def source_value(offset, delta)
205
+ if is_string?
206
+ return "#{query_source.to_s.dasherize}; #{columns.first.__name}"
207
+ end
208
+
209
+ query = query(offset)
210
+
211
+ if query_source == :ranged_query
212
+ query += query_clause
213
+ query += " AND #{query_delta.strip}" if delta
214
+ "ranged-query; #{query}; #{range_query}"
215
+ else
216
+ query += "WHERE #{query_delta.strip}" if delta
217
+ "query; #{query}"
218
+ end
219
+ end
220
+
221
+ def query(offset)
222
+ base_assoc = base_association_for_mva
223
+ end_assoc = end_association_for_mva
224
+ raise "Could not determine SQL for MVA" if base_assoc.nil?
225
+
226
+ <<-SQL
227
+ SELECT #{foreign_key_for_mva base_assoc}
228
+ #{ThinkingSphinx.unique_id_expression(offset)} AS #{quote_column('id')},
229
+ #{primary_key_for_mva(end_assoc)} AS #{quote_column(unique_name)}
230
+ FROM #{quote_table_name base_assoc.table} #{association_joins}
231
+ SQL
232
+ end
233
+
234
+ def query_clause
235
+ foreign_key = foreign_key_for_mva base_association_for_mva
236
+ "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
237
+ end
238
+
239
+ def query_delta
240
+ foreign_key = foreign_key_for_mva base_association_for_mva
241
+ <<-SQL
242
+ #{foreign_key} IN (SELECT #{quote_column model.primary_key}
243
+ FROM #{model.quoted_table_name}
244
+ WHERE #{@source.index.delta_object.clause(model, true)})
245
+ SQL
246
+ end
247
+
248
+ def range_query
249
+ assoc = base_association_for_mva
250
+ foreign_key = foreign_key_for_mva assoc
251
+ "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
252
+ end
253
+
254
+ def primary_key_for_mva(assoc)
255
+ quote_with_table(
256
+ assoc.table, assoc.primary_key_from_reflection || columns.first.__name
257
+ )
258
+ end
259
+
260
+ def foreign_key_for_mva(assoc)
261
+ quote_with_table assoc.table, assoc.reflection.primary_key_name
262
+ end
263
+
264
+ def end_association_for_mva
265
+ @association_for_mva ||= associations[columns.first].detect { |assoc|
266
+ assoc.has_column?(columns.first.__name)
267
+ }
268
+ end
269
+
270
+ def base_association_for_mva
271
+ @first_association_for_mva ||= begin
272
+ assoc = end_association_for_mva
273
+ while !assoc.parent.nil?
274
+ assoc = assoc.parent
275
+ end
276
+
277
+ assoc
278
+ end
279
+ end
280
+
281
+ def association_joins
282
+ joins = []
283
+ assoc = end_association_for_mva
284
+ while assoc != base_association_for_mva
285
+ joins << assoc.to_sql
286
+ assoc = assoc.parent
287
+ end
288
+
289
+ joins.join(' ')
290
+ end
291
+
292
+ def is_many_ints?
293
+ concat_ws? && all_ints?
294
+ end
295
+
296
+ def is_many_datetimes?
297
+ is_many? && all_datetimes?
298
+ end
299
+
300
+ def is_many_strings?
301
+ is_many? && all_strings?
302
+ end
303
+
304
+ def type_from_database
305
+ klass = @associations.values.flatten.first ?
306
+ @associations.values.flatten.first.reflection.klass : @model
307
+
308
+ column = klass.columns.detect { |col|
309
+ @columns.collect { |c| c.__name.to_s }.include? col.name
310
+ }
311
+ column.nil? ? nil : column.type
312
+ end
313
+
314
+ def translated_type_from_database
315
+ case type_from_db = type_from_database
316
+ when :datetime, :string, :float, :boolean, :integer
317
+ type_from_db
318
+ when :decimal
319
+ :float
320
+ when :timestamp, :date
321
+ :datetime
322
+ else
323
+ raise <<-MESSAGE
324
+
325
+ Cannot automatically map attribute #{unique_name} in #{@model.name} to an
326
+ equivalent Sphinx type (integer, float, boolean, datetime, string as ordinal).
327
+ You could try to explicitly convert the column's value in your define_index
328
+ block:
329
+ has "CAST(column AS INT)", :type => :integer, :as => :column
330
+ MESSAGE
331
+ end
332
+ end
333
+
334
+ def all_of_type?(*column_types)
335
+ @columns.all? { |col|
336
+ klasses = @associations[col].empty? ? [@model] :
337
+ @associations[col].collect { |assoc| assoc.reflection.klass }
338
+ klasses.all? { |klass|
339
+ column = klass.columns.detect { |column| column.name == col.__name.to_s }
340
+ !column.nil? && column_types.include?(column.type)
341
+ }
342
+ }
343
+ end
344
+
345
+ def sphinx_value(value)
346
+ case value
347
+ when TrueClass
348
+ 1
349
+ when FalseClass, NilClass
350
+ 0
351
+ when Time
352
+ value.to_i
353
+ when Date
354
+ value.to_time.to_i
355
+ when String
356
+ value.to_crc32
357
+ else
358
+ value
359
+ end
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,22 @@
1
+ module ThinkingSphinx
2
+ class AutoVersion
3
+ def self.detect
4
+ version = ThinkingSphinx::Configuration.instance.controller.sphinx_version
5
+ case version
6
+ when '0.9.8', '0.9.9'
7
+ require "riddle/#{version}"
8
+ else
9
+ STDERR.puts %Q{
10
+ Sphinx cannot be found on your system. You may need to configure the following
11
+ settings in your config/sphinx.yml file:
12
+ * bin_path
13
+ * searchd_binary_name
14
+ * indexer_binary_name
15
+
16
+ For more information, read the documentation:
17
+ http://freelancing-god.github.com/ts/en/advanced_config.html
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module ThinkingSphinx
2
+ class ClassFacet < ThinkingSphinx::Facet
3
+ def name
4
+ :class
5
+ end
6
+
7
+ def attribute_name
8
+ "class_crc"
9
+ end
10
+
11
+ def value(object, attribute_value)
12
+ object.class.name
13
+ end
14
+ end
15
+ end