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,125 @@
1
+ module ThinkingSphinx
2
+ class Facet
3
+ attr_reader :property
4
+
5
+ def initialize(property)
6
+ @property = property
7
+
8
+ if property.columns.length != 1
9
+ raise "Can't translate Facets on multiple-column field or attribute"
10
+ end
11
+ end
12
+
13
+ def self.name_for(facet)
14
+ case facet
15
+ when Facet
16
+ facet.name
17
+ when String, Symbol
18
+ facet.to_s.gsub(/(_facet|_crc)$/,'').to_sym
19
+ end
20
+ end
21
+
22
+ def self.attribute_name_for(name)
23
+ name.to_s == 'class' ? 'class_crc' : "#{name}_facet"
24
+ end
25
+
26
+ def self.attribute_name_from_value(name, value)
27
+ case value
28
+ when String
29
+ attribute_name_for(name)
30
+ when Array
31
+ if value.all? { |val| val.is_a?(Integer) }
32
+ name
33
+ else
34
+ attribute_name_for(name)
35
+ end
36
+ else
37
+ name
38
+ end
39
+ end
40
+
41
+ def self.translate?(property)
42
+ return true if property.is_a?(Field)
43
+
44
+ case property.type
45
+ when :string
46
+ true
47
+ when :integer, :boolean, :datetime, :float
48
+ false
49
+ when :multi
50
+ !property.all_ints?
51
+ end
52
+ end
53
+
54
+ def name
55
+ property.unique_name
56
+ end
57
+
58
+ def attribute_name
59
+ if translate?
60
+ Facet.attribute_name_for(@property.unique_name)
61
+ else
62
+ @property.unique_name.to_s
63
+ end
64
+ end
65
+
66
+ def translate?
67
+ Facet.translate?(@property)
68
+ end
69
+
70
+ def type
71
+ @property.is_a?(Field) ? :string : @property.type
72
+ end
73
+
74
+ def value(object, attribute_value)
75
+ return translate(object, attribute_value) if translate? || float?
76
+
77
+ case @property.type
78
+ when :datetime
79
+ Time.at(attribute_value)
80
+ when :boolean
81
+ attribute_value > 0
82
+ else
83
+ attribute_value
84
+ end
85
+ end
86
+
87
+ def to_s
88
+ name
89
+ end
90
+
91
+ private
92
+
93
+ def translate(object, attribute_value)
94
+ objects = source_objects(object)
95
+ return nil if objects.nil? || objects.empty?
96
+
97
+ if objects.length > 1
98
+ objects.collect { |item| item.send(column.__name) }.detect { |item|
99
+ item.to_crc32 == attribute_value
100
+ }
101
+ else
102
+ objects.first.send(column.__name)
103
+ end
104
+ end
105
+
106
+ def source_objects(object)
107
+ column.__stack.each { |method|
108
+ object = Array(object).collect { |item|
109
+ item.send(method)
110
+ }.flatten.compact
111
+
112
+ return nil if object.empty?
113
+ }
114
+ Array(object)
115
+ end
116
+
117
+ def column
118
+ @property.columns.first
119
+ end
120
+
121
+ def float?
122
+ @property.type == :float
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,136 @@
1
+ module ThinkingSphinx
2
+ class FacetSearch < Hash
3
+ attr_accessor :args, :options
4
+
5
+ def initialize(*args)
6
+ ThinkingSphinx.context.define_indexes
7
+
8
+ @options = args.extract_options!
9
+ @args = args
10
+
11
+ set_default_options
12
+
13
+ populate
14
+ end
15
+
16
+ def for(hash = {})
17
+ for_options = {:with => {}}.merge(options)
18
+
19
+ hash.each do |key, value|
20
+ attrib = ThinkingSphinx::Facet.attribute_name_from_value(key, value)
21
+ for_options[:with][attrib] = underlying_value key, value
22
+ end
23
+
24
+ ThinkingSphinx.search *(args + [for_options])
25
+ end
26
+
27
+ def facet_names
28
+ @facet_names ||= begin
29
+ names = options[:all_facets] ?
30
+ facet_names_for_all_classes : facet_names_common_to_all_classes
31
+
32
+ names.delete "class_crc" unless options[:class_facet]
33
+ names
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def set_default_options
40
+ options[:all_facets] ||= false
41
+ if options[:class_facet].nil?
42
+ options[:class_facet] = ((options[:classes] || []).length != 1)
43
+ end
44
+ end
45
+
46
+ def populate
47
+ facet_names.each do |name|
48
+ search_options = facet_search_options.merge(:group_by => name)
49
+ add_from_results name, ThinkingSphinx.search(
50
+ *(args + [search_options])
51
+ )
52
+ end
53
+ end
54
+
55
+ def facet_search_options
56
+ config = ThinkingSphinx::Configuration.instance
57
+ max = config.configuration.searchd.max_matches || 1000
58
+
59
+ options.merge(
60
+ :group_function => :attr,
61
+ :limit => max,
62
+ :max_matches => max,
63
+ :page => 1
64
+ )
65
+ end
66
+
67
+ def facet_classes
68
+ (
69
+ options[:classes] || ThinkingSphinx.context.indexed_models.collect { |model|
70
+ model.constantize
71
+ }
72
+ ).select { |klass| klass.sphinx_facets.any? }
73
+ end
74
+
75
+ def all_facets
76
+ facet_classes.collect { |klass|
77
+ klass.sphinx_facets
78
+ }.flatten.select { |facet|
79
+ options[:facets].blank? || Array(options[:facets]).include?(facet.name)
80
+ }
81
+ end
82
+
83
+ def facet_names_for_all_classes
84
+ all_facets.group_by { |facet|
85
+ facet.name
86
+ }.collect { |name, facets|
87
+ if facets.collect { |facet| facet.type }.uniq.length > 1
88
+ raise "Facet #{name} exists in more than one model with different types"
89
+ end
90
+ facets.first.attribute_name
91
+ }
92
+ end
93
+
94
+ def facet_names_common_to_all_classes
95
+ facet_names_for_all_classes.select { |name|
96
+ facet_classes.all? { |klass|
97
+ klass.sphinx_facets.detect { |facet|
98
+ facet.attribute_name == name
99
+ }
100
+ }
101
+ }
102
+ end
103
+
104
+ def add_from_results(facet, results)
105
+ name = ThinkingSphinx::Facet.name_for(facet)
106
+
107
+ self[name] ||= {}
108
+
109
+ return if results.empty?
110
+
111
+ facet = facet_from_object(results.first, facet) if facet.is_a?(String)
112
+
113
+ results.each_with_groupby_and_count { |result, group, count|
114
+ facet_value = facet.value(result, group)
115
+
116
+ self[name][facet_value] ||= 0
117
+ self[name][facet_value] += count
118
+ }
119
+ end
120
+
121
+ def underlying_value(key, value)
122
+ case value
123
+ when Array
124
+ value.collect { |item| underlying_value(key, item) }
125
+ when String
126
+ value.to_crc32
127
+ else
128
+ value
129
+ end
130
+ end
131
+
132
+ def facet_from_object(object, name)
133
+ object.sphinx_facets.detect { |facet| facet.attribute_name == name }
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,82 @@
1
+ module ThinkingSphinx
2
+ # Fields - holding the string data which Sphinx indexes for your searches.
3
+ # This class isn't really useful to you unless you're hacking around with the
4
+ # internals of Thinking Sphinx - but hey, don't let that stop you.
5
+ #
6
+ # One key thing to remember - if you're using the field manually to
7
+ # generate SQL statements, you'll need to set the base model, and all the
8
+ # associations. Which can get messy. Use Index.link!, it really helps.
9
+ #
10
+ class Field < ThinkingSphinx::Property
11
+ attr_accessor :sortable, :infixes, :prefixes
12
+
13
+ # To create a new field, you'll need to pass in either a single Column
14
+ # or an array of them, and some (optional) options. The columns are
15
+ # references to the data that will make up the field.
16
+ #
17
+ # Valid options are:
18
+ # - :as => :alias_name
19
+ # - :sortable => true
20
+ # - :infixes => true
21
+ # - :prefixes => true
22
+ #
23
+ # Alias is only required in three circumstances: when there's
24
+ # another attribute or field with the same name, when the column name is
25
+ # 'id', or when there's more than one column.
26
+ #
27
+ # Sortable defaults to false - but is quite useful when set to true, as
28
+ # it creates an attribute with the same string value (which Sphinx converts
29
+ # to an integer value), which can be sorted by. Thinking Sphinx is smart
30
+ # enough to realise that when you specify fields in sort statements, you
31
+ # mean their respective attributes.
32
+ #
33
+ # If you have partial matching enabled (ie: enable_star), then you can
34
+ # specify certain fields to have their prefixes and infixes indexed. Keep
35
+ # in mind, though, that Sphinx's default is _all_ fields - so once you
36
+ # highlight a particular field, no other fields in the index will have
37
+ # these partial indexes.
38
+ #
39
+ # Here's some examples:
40
+ #
41
+ # Field.new(
42
+ # Column.new(:name)
43
+ # )
44
+ #
45
+ # Field.new(
46
+ # [Column.new(:first_name), Column.new(:last_name)],
47
+ # :as => :name, :sortable => true
48
+ # )
49
+ #
50
+ # Field.new(
51
+ # [Column.new(:posts, :subject), Column.new(:posts, :content)],
52
+ # :as => :posts, :prefixes => true
53
+ # )
54
+ #
55
+ def initialize(source, columns, options = {})
56
+ super
57
+
58
+ @sortable = options[:sortable] || false
59
+ @infixes = options[:infixes] || false
60
+ @prefixes = options[:prefixes] || false
61
+
62
+ source.fields << self
63
+ end
64
+
65
+ # Get the part of the SELECT clause related to this field. Don't forget
66
+ # to set your model and associations first though.
67
+ #
68
+ # This will concatenate strings if there's more than one data source or
69
+ # multiple data values (has_many or has_and_belongs_to_many associations).
70
+ #
71
+ def to_select_sql
72
+ clause = @columns.collect { |column|
73
+ column_with_prefix(column)
74
+ }.join(', ')
75
+
76
+ clause = adapter.concatenate(clause) if concat_ws?
77
+ clause = adapter.group_concatenate(clause) if is_many?
78
+
79
+ "#{adapter.cast_to_string clause } AS #{quote_column(unique_name)}"
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,296 @@
1
+ module ThinkingSphinx
2
+ class Index
3
+ # The Builder class is the core for the index definition block processing.
4
+ # There are four methods you really need to pay attention to:
5
+ # - indexes
6
+ # - has
7
+ # - where
8
+ # - set_property/set_properties
9
+ #
10
+ # The first two of these methods allow you to define what data makes up
11
+ # your indexes. #where provides a method to add manual SQL conditions, and
12
+ # set_property allows you to set some settings on a per-index basis. Check
13
+ # out each method's documentation for better ideas of usage.
14
+ #
15
+ class Builder
16
+ instance_methods.grep(/^[^_]/).each { |method|
17
+ next if method.to_s == "instance_eval"
18
+ define_method(method) {
19
+ caller.grep(/irb.completion/).empty? ? method_missing(method) : super
20
+ }
21
+ }
22
+
23
+ def self.generate(model, name = nil, &block)
24
+ index = ThinkingSphinx::Index.new(model)
25
+ index.name = name unless name.nil?
26
+
27
+ Builder.new(index, &block) if block_given?
28
+
29
+ index.delta_object = ThinkingSphinx::Deltas.parse index
30
+ index
31
+ end
32
+
33
+ def initialize(index, &block)
34
+ @index = index
35
+ @explicit_source = false
36
+
37
+ self.instance_eval &block
38
+
39
+ if no_fields?
40
+ raise "At least one field is necessary for an index"
41
+ end
42
+ end
43
+
44
+ def define_source(&block)
45
+ if @explicit_source
46
+ @source = ThinkingSphinx::Source.new(@index)
47
+ @index.sources << @source
48
+ else
49
+ @explicit_source = true
50
+ end
51
+
52
+ self.instance_eval &block
53
+ end
54
+
55
+ # This is how you add fields - the strings Sphinx looks at - to your
56
+ # index. Technically, to use this method, you need to pass in some
57
+ # columns and options - but there's some neat method_missing stuff
58
+ # happening, so lets stick to the expected syntax within a define_index
59
+ # block.
60
+ #
61
+ # Expected options are :as, which points to a column alias in symbol
62
+ # form, and :sortable, which indicates whether you want to sort by this
63
+ # field.
64
+ #
65
+ # Adding Single-Column Fields:
66
+ #
67
+ # You can use symbols or methods - and can chain methods together to
68
+ # get access down the associations tree.
69
+ #
70
+ # indexes :id, :as => :my_id
71
+ # indexes :name, :sortable => true
72
+ # indexes first_name, last_name, :sortable => true
73
+ # indexes users.posts.content, :as => :post_content
74
+ # indexes users(:id), :as => :user_ids
75
+ #
76
+ # Keep in mind that if any keywords for Ruby methods - such as id or
77
+ # name - clash with your column names, you need to use the symbol
78
+ # version (see the first, second and last examples above).
79
+ #
80
+ # If you specify multiple columns (example #2), a field will be created
81
+ # for each. Don't use the :as option in this case. If you want to merge
82
+ # those columns together, continue reading.
83
+ #
84
+ # Adding Multi-Column Fields:
85
+ #
86
+ # indexes [first_name, last_name], :as => :name
87
+ # indexes [location, parent.location], :as => :location
88
+ #
89
+ # To combine multiple columns into a single field, you need to wrap
90
+ # them in an Array, as shown by the above examples. There's no
91
+ # limitations on whether they're symbols or methods or what level of
92
+ # associations they come from.
93
+ #
94
+ # Adding SQL Fragment Fields
95
+ #
96
+ # You can also define a field using an SQL fragment, useful for when
97
+ # you would like to index a calculated value.
98
+ #
99
+ # indexes "age < 18", :as => :minor
100
+ #
101
+ def indexes(*args)
102
+ options = args.extract_options!
103
+ args.each do |columns|
104
+ field = Field.new(source, FauxColumn.coerce(columns), options)
105
+
106
+ add_sort_attribute field, options if field.sortable
107
+ add_facet_attribute field, options if field.faceted
108
+ end
109
+ end
110
+
111
+ # This is the method to add attributes to your index (hence why it is
112
+ # aliased as 'attribute'). The syntax is the same as #indexes, so use
113
+ # that as starting point, but keep in mind the following points.
114
+ #
115
+ # An attribute can have an alias (the :as option), but it is always
116
+ # sortable - so you don't need to explicitly request that. You _can_
117
+ # specify the data type of the attribute (the :type option), but the
118
+ # code's pretty good at figuring that out itself from peering into the
119
+ # database.
120
+ #
121
+ # Attributes are limited to the following types: integers, floats,
122
+ # datetimes (converted to timestamps), booleans and strings. Don't
123
+ # forget that Sphinx converts string attributes to integers, which are
124
+ # useful for sorting, but that's about it.
125
+ #
126
+ # You can also have a collection of integers for multi-value attributes
127
+ # (MVAs). Generally these would be through a has_many relationship,
128
+ # like in this example:
129
+ #
130
+ # has posts(:id), :as => :post_ids
131
+ #
132
+ # This allows you to filter on any of the values tied to a specific
133
+ # record. Might be best to read through the Sphinx documentation to get
134
+ # a better idea of that though.
135
+ #
136
+ # Adding SQL Fragment Attributes
137
+ #
138
+ # You can also define an attribute using an SQL fragment, useful for
139
+ # when you would like to index a calculated value. Don't forget to set
140
+ # the type of the attribute though:
141
+ #
142
+ # has "age < 18", :as => :minor, :type => :boolean
143
+ #
144
+ # If you're creating attributes for latitude and longitude, don't
145
+ # forget that Sphinx expects these values to be in radians.
146
+ #
147
+ def has(*args)
148
+ options = args.extract_options!
149
+ args.each do |columns|
150
+ attribute = Attribute.new(source, FauxColumn.coerce(columns), options)
151
+
152
+ add_facet_attribute attribute, options if attribute.faceted
153
+ end
154
+ end
155
+
156
+ def facet(*args)
157
+ options = args.extract_options!
158
+ options[:facet] = true
159
+
160
+ args.each do |columns|
161
+ attribute = Attribute.new(source, FauxColumn.coerce(columns), options)
162
+
163
+ add_facet_attribute attribute, options
164
+ end
165
+ end
166
+
167
+ # Use this method to add some manual SQL conditions for your index
168
+ # request. You can pass in as many strings as you like, they'll get
169
+ # joined together with ANDs later on.
170
+ #
171
+ # where "user_id = 10"
172
+ # where "parent_type = 'Article'", "created_at < NOW()"
173
+ #
174
+ def where(*args)
175
+ source.conditions += args
176
+ end
177
+
178
+ # Use this method to add some manual SQL strings to the GROUP BY
179
+ # clause. You can pass in as many strings as you'd like, they'll get
180
+ # joined together with commas later on.
181
+ #
182
+ # group_by "lat", "lng"
183
+ #
184
+ def group_by(*args)
185
+ source.groupings += args
186
+ end
187
+
188
+ # This is what to use to set properties on the index. Chief amongst
189
+ # those is the delta property - to allow automatic updates to your
190
+ # indexes as new models are added and edited - but also you can
191
+ # define search-related properties which will be the defaults for all
192
+ # searches on the model.
193
+ #
194
+ # set_property :delta => true
195
+ # set_property :field_weights => {"name" => 100}
196
+ # set_property :order => "name ASC"
197
+ # set_property :select => 'name'
198
+ #
199
+ # Also, the following two properties are particularly relevant for
200
+ # geo-location searching - latitude_attr and longitude_attr. If your
201
+ # attributes for these two values are named something other than
202
+ # lat/latitude or lon/long/longitude, you can dictate what they are
203
+ # when defining the index, so you don't need to specify them for every
204
+ # geo-related search.
205
+ #
206
+ # set_property :latitude_attr => "lt", :longitude_attr => "lg"
207
+ #
208
+ # Please don't forget to add a boolean field named 'delta' to your
209
+ # model's database table if enabling the delta index for it.
210
+ # Valid options for the delta property are:
211
+ #
212
+ # true
213
+ # false
214
+ # :default
215
+ # :delayed
216
+ # :datetime
217
+ #
218
+ # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
219
+ # your own handling for delta indexing.
220
+ #
221
+ def set_property(*args)
222
+ options = args.extract_options!
223
+ options.each do |key, value|
224
+ set_single_property key, value
225
+ end
226
+
227
+ set_single_property args[0], args[1] if args.length == 2
228
+ end
229
+ alias_method :set_properties, :set_property
230
+
231
+ # Handles the generation of new columns for the field and attribute
232
+ # definitions.
233
+ #
234
+ def method_missing(method, *args)
235
+ FauxColumn.new(method, *args)
236
+ end
237
+
238
+ # A method to allow adding fields from associations which have names
239
+ # that clash with method names in the Builder class (ie: properties,
240
+ # fields, attributes).
241
+ #
242
+ # Example: indexes assoc(:properties).column
243
+ #
244
+ def assoc(assoc, *args)
245
+ FauxColumn.new(assoc, *args)
246
+ end
247
+
248
+ private
249
+
250
+ def source
251
+ @source ||= begin
252
+ source = ThinkingSphinx::Source.new(@index)
253
+ @index.sources << source
254
+ source
255
+ end
256
+ end
257
+
258
+ def set_single_property(key, value)
259
+ source_options = ThinkingSphinx::Configuration::SourceOptions
260
+ if source_options.include?(key.to_s)
261
+ source.options.merge! key => value
262
+ else
263
+ @index.local_options.merge! key => value
264
+ end
265
+ end
266
+
267
+ def add_sort_attribute(field, options)
268
+ add_internal_attribute field, options, "_sort"
269
+ end
270
+
271
+ def add_facet_attribute(property, options)
272
+ add_internal_attribute property, options, "_facet", true
273
+ @index.model.sphinx_facets << property.to_facet
274
+ end
275
+
276
+ def add_internal_attribute(property, options, suffix, crc = false)
277
+ return unless ThinkingSphinx::Facet.translate?(property)
278
+
279
+ Attribute.new(source,
280
+ property.columns.collect { |col| col.clone },
281
+ options.merge(
282
+ :type => property.is_a?(Field) ? :string : options[:type],
283
+ :as => property.unique_name.to_s.concat(suffix).to_sym,
284
+ :crc => crc
285
+ ).except(:facet)
286
+ )
287
+ end
288
+
289
+ def no_fields?
290
+ @index.sources.empty? || @index.sources.any? { |source|
291
+ source.fields.length == 0
292
+ }
293
+ end
294
+ end
295
+ end
296
+ end