friendlyfashion-thinking-sphinx 2.0.13

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 (175) hide show
  1. data/HISTORY +244 -0
  2. data/LICENCE +20 -0
  3. data/README.textile +235 -0
  4. data/features/abstract_inheritance.feature +10 -0
  5. data/features/alternate_primary_key.feature +27 -0
  6. data/features/attribute_transformation.feature +22 -0
  7. data/features/attribute_updates.feature +77 -0
  8. data/features/deleting_instances.feature +67 -0
  9. data/features/direct_attributes.feature +11 -0
  10. data/features/excerpts.feature +21 -0
  11. data/features/extensible_delta_indexing.feature +9 -0
  12. data/features/facets.feature +88 -0
  13. data/features/facets_across_model.feature +29 -0
  14. data/features/field_sorting.feature +18 -0
  15. data/features/handling_edits.feature +94 -0
  16. data/features/retry_stale_indexes.feature +24 -0
  17. data/features/searching_across_models.feature +20 -0
  18. data/features/searching_by_index.feature +40 -0
  19. data/features/searching_by_model.feature +175 -0
  20. data/features/searching_with_find_arguments.feature +56 -0
  21. data/features/sphinx_detection.feature +25 -0
  22. data/features/sphinx_scopes.feature +68 -0
  23. data/features/step_definitions/alpha_steps.rb +16 -0
  24. data/features/step_definitions/beta_steps.rb +7 -0
  25. data/features/step_definitions/common_steps.rb +201 -0
  26. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  27. data/features/step_definitions/facet_steps.rb +96 -0
  28. data/features/step_definitions/find_arguments_steps.rb +36 -0
  29. data/features/step_definitions/gamma_steps.rb +15 -0
  30. data/features/step_definitions/scope_steps.rb +19 -0
  31. data/features/step_definitions/search_steps.rb +94 -0
  32. data/features/step_definitions/sphinx_steps.rb +35 -0
  33. data/features/sti_searching.feature +19 -0
  34. data/features/support/env.rb +27 -0
  35. data/features/support/lib/generic_delta_handler.rb +8 -0
  36. data/features/thinking_sphinx/database.example.yml +3 -0
  37. data/features/thinking_sphinx/db/.gitignore +1 -0
  38. data/features/thinking_sphinx/db/fixtures/alphas.rb +8 -0
  39. data/features/thinking_sphinx/db/fixtures/authors.rb +1 -0
  40. data/features/thinking_sphinx/db/fixtures/betas.rb +11 -0
  41. data/features/thinking_sphinx/db/fixtures/boxes.rb +9 -0
  42. data/features/thinking_sphinx/db/fixtures/categories.rb +1 -0
  43. data/features/thinking_sphinx/db/fixtures/cats.rb +3 -0
  44. data/features/thinking_sphinx/db/fixtures/comments.rb +24 -0
  45. data/features/thinking_sphinx/db/fixtures/developers.rb +31 -0
  46. data/features/thinking_sphinx/db/fixtures/dogs.rb +3 -0
  47. data/features/thinking_sphinx/db/fixtures/extensible_betas.rb +10 -0
  48. data/features/thinking_sphinx/db/fixtures/foxes.rb +3 -0
  49. data/features/thinking_sphinx/db/fixtures/gammas.rb +10 -0
  50. data/features/thinking_sphinx/db/fixtures/music.rb +4 -0
  51. data/features/thinking_sphinx/db/fixtures/people.rb +1001 -0
  52. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +1 -0
  53. data/features/thinking_sphinx/db/fixtures/posts.rb +10 -0
  54. data/features/thinking_sphinx/db/fixtures/robots.rb +8 -0
  55. data/features/thinking_sphinx/db/fixtures/tags.rb +27 -0
  56. data/features/thinking_sphinx/db/migrations/create_alphas.rb +8 -0
  57. data/features/thinking_sphinx/db/migrations/create_animals.rb +5 -0
  58. data/features/thinking_sphinx/db/migrations/create_authors.rb +3 -0
  59. data/features/thinking_sphinx/db/migrations/create_authors_posts.rb +6 -0
  60. data/features/thinking_sphinx/db/migrations/create_betas.rb +5 -0
  61. data/features/thinking_sphinx/db/migrations/create_boxes.rb +5 -0
  62. data/features/thinking_sphinx/db/migrations/create_categories.rb +3 -0
  63. data/features/thinking_sphinx/db/migrations/create_comments.rb +10 -0
  64. data/features/thinking_sphinx/db/migrations/create_developers.rb +7 -0
  65. data/features/thinking_sphinx/db/migrations/create_extensible_betas.rb +5 -0
  66. data/features/thinking_sphinx/db/migrations/create_gammas.rb +3 -0
  67. data/features/thinking_sphinx/db/migrations/create_genres.rb +3 -0
  68. data/features/thinking_sphinx/db/migrations/create_music.rb +6 -0
  69. data/features/thinking_sphinx/db/migrations/create_people.rb +13 -0
  70. data/features/thinking_sphinx/db/migrations/create_posts.rb +6 -0
  71. data/features/thinking_sphinx/db/migrations/create_robots.rb +4 -0
  72. data/features/thinking_sphinx/db/migrations/create_taggings.rb +5 -0
  73. data/features/thinking_sphinx/db/migrations/create_tags.rb +4 -0
  74. data/features/thinking_sphinx/models/alpha.rb +23 -0
  75. data/features/thinking_sphinx/models/andrew.rb +17 -0
  76. data/features/thinking_sphinx/models/animal.rb +5 -0
  77. data/features/thinking_sphinx/models/author.rb +3 -0
  78. data/features/thinking_sphinx/models/beta.rb +13 -0
  79. data/features/thinking_sphinx/models/box.rb +8 -0
  80. data/features/thinking_sphinx/models/cat.rb +3 -0
  81. data/features/thinking_sphinx/models/category.rb +4 -0
  82. data/features/thinking_sphinx/models/comment.rb +10 -0
  83. data/features/thinking_sphinx/models/developer.rb +21 -0
  84. data/features/thinking_sphinx/models/dog.rb +3 -0
  85. data/features/thinking_sphinx/models/extensible_beta.rb +9 -0
  86. data/features/thinking_sphinx/models/fox.rb +5 -0
  87. data/features/thinking_sphinx/models/gamma.rb +5 -0
  88. data/features/thinking_sphinx/models/genre.rb +3 -0
  89. data/features/thinking_sphinx/models/medium.rb +5 -0
  90. data/features/thinking_sphinx/models/music.rb +10 -0
  91. data/features/thinking_sphinx/models/person.rb +24 -0
  92. data/features/thinking_sphinx/models/post.rb +22 -0
  93. data/features/thinking_sphinx/models/robot.rb +12 -0
  94. data/features/thinking_sphinx/models/tag.rb +3 -0
  95. data/features/thinking_sphinx/models/tagging.rb +4 -0
  96. data/lib/cucumber/thinking_sphinx/external_world.rb +12 -0
  97. data/lib/cucumber/thinking_sphinx/internal_world.rb +137 -0
  98. data/lib/cucumber/thinking_sphinx/sql_logger.rb +28 -0
  99. data/lib/thinking-sphinx.rb +1 -0
  100. data/lib/thinking_sphinx/action_controller.rb +31 -0
  101. data/lib/thinking_sphinx/active_record/attribute_updates.rb +53 -0
  102. data/lib/thinking_sphinx/active_record/collection_proxy.rb +47 -0
  103. data/lib/thinking_sphinx/active_record/collection_proxy_with_scopes.rb +27 -0
  104. data/lib/thinking_sphinx/active_record/delta.rb +67 -0
  105. data/lib/thinking_sphinx/active_record/has_many_association.rb +44 -0
  106. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +21 -0
  107. data/lib/thinking_sphinx/active_record/log_subscriber.rb +61 -0
  108. data/lib/thinking_sphinx/active_record/scopes.rb +110 -0
  109. data/lib/thinking_sphinx/active_record.rb +386 -0
  110. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +87 -0
  111. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +62 -0
  112. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +188 -0
  113. data/lib/thinking_sphinx/association.rb +230 -0
  114. data/lib/thinking_sphinx/attribute.rb +405 -0
  115. data/lib/thinking_sphinx/auto_version.rb +40 -0
  116. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  117. data/lib/thinking_sphinx/class_facet.rb +20 -0
  118. data/lib/thinking_sphinx/configuration.rb +375 -0
  119. data/lib/thinking_sphinx/context.rb +76 -0
  120. data/lib/thinking_sphinx/core/string.rb +15 -0
  121. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  122. data/lib/thinking_sphinx/deltas.rb +28 -0
  123. data/lib/thinking_sphinx/deploy/capistrano.rb +99 -0
  124. data/lib/thinking_sphinx/excerpter.rb +23 -0
  125. data/lib/thinking_sphinx/facet.rb +135 -0
  126. data/lib/thinking_sphinx/facet_search.rb +170 -0
  127. data/lib/thinking_sphinx/field.rb +98 -0
  128. data/lib/thinking_sphinx/index/builder.rb +315 -0
  129. data/lib/thinking_sphinx/index/faux_column.rb +118 -0
  130. data/lib/thinking_sphinx/index.rb +159 -0
  131. data/lib/thinking_sphinx/join.rb +37 -0
  132. data/lib/thinking_sphinx/property.rb +187 -0
  133. data/lib/thinking_sphinx/railtie.rb +43 -0
  134. data/lib/thinking_sphinx/search.rb +1061 -0
  135. data/lib/thinking_sphinx/search_methods.rb +439 -0
  136. data/lib/thinking_sphinx/sinatra.rb +7 -0
  137. data/lib/thinking_sphinx/source/internal_properties.rb +51 -0
  138. data/lib/thinking_sphinx/source/sql.rb +174 -0
  139. data/lib/thinking_sphinx/source.rb +194 -0
  140. data/lib/thinking_sphinx/tasks.rb +142 -0
  141. data/lib/thinking_sphinx/test.rb +55 -0
  142. data/lib/thinking_sphinx/version.rb +3 -0
  143. data/lib/thinking_sphinx.rb +297 -0
  144. data/spec/fixtures/data.sql +32 -0
  145. data/spec/fixtures/database.yml.default +3 -0
  146. data/spec/fixtures/models.rb +164 -0
  147. data/spec/fixtures/structure.sql +146 -0
  148. data/spec/spec_helper.rb +61 -0
  149. data/spec/sphinx_helper.rb +60 -0
  150. data/spec/support/rails.rb +25 -0
  151. data/spec/thinking_sphinx/active_record/delta_spec.rb +122 -0
  152. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +173 -0
  153. data/spec/thinking_sphinx/active_record/scopes_spec.rb +176 -0
  154. data/spec/thinking_sphinx/active_record_spec.rb +573 -0
  155. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +145 -0
  156. data/spec/thinking_sphinx/association_spec.rb +250 -0
  157. data/spec/thinking_sphinx/attribute_spec.rb +552 -0
  158. data/spec/thinking_sphinx/auto_version_spec.rb +103 -0
  159. data/spec/thinking_sphinx/configuration_spec.rb +326 -0
  160. data/spec/thinking_sphinx/context_spec.rb +126 -0
  161. data/spec/thinking_sphinx/core/array_spec.rb +9 -0
  162. data/spec/thinking_sphinx/core/string_spec.rb +9 -0
  163. data/spec/thinking_sphinx/excerpter_spec.rb +49 -0
  164. data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
  165. data/spec/thinking_sphinx/facet_spec.rb +359 -0
  166. data/spec/thinking_sphinx/field_spec.rb +127 -0
  167. data/spec/thinking_sphinx/index/builder_spec.rb +532 -0
  168. data/spec/thinking_sphinx/index/faux_column_spec.rb +36 -0
  169. data/spec/thinking_sphinx/index_spec.rb +189 -0
  170. data/spec/thinking_sphinx/search_methods_spec.rb +156 -0
  171. data/spec/thinking_sphinx/search_spec.rb +1455 -0
  172. data/spec/thinking_sphinx/source_spec.rb +267 -0
  173. data/spec/thinking_sphinx/test_spec.rb +20 -0
  174. data/spec/thinking_sphinx_spec.rb +204 -0
  175. metadata +524 -0
@@ -0,0 +1,135 @@
1
+ module ThinkingSphinx
2
+ class Facet
3
+ attr_reader :property, :value_source
4
+
5
+ def initialize(property, value_source = nil)
6
+ @property = property
7
+ @value_source = value_source
8
+
9
+ if property.columns.length != 1
10
+ raise "Can't translate Facets on multiple-column field or attribute"
11
+ end
12
+ end
13
+
14
+ def self.name_for(facet)
15
+ case facet
16
+ when Facet
17
+ facet.name
18
+ when String, Symbol
19
+ return :class if facet.to_s == 'sphinx_internal_class'
20
+ facet.to_s.gsub(/(_facet|_crc)$/,'').to_sym
21
+ end
22
+ end
23
+
24
+ def self.attribute_name_for(name)
25
+ name.to_s == 'class' ? 'class_crc' : "#{name}_facet"
26
+ end
27
+
28
+ def self.attribute_name_from_value(name, value)
29
+ case value
30
+ when String
31
+ attribute_name_for(name)
32
+ when Array
33
+ if value.all? { |val| val.is_a?(Integer) }
34
+ name
35
+ else
36
+ attribute_name_for(name)
37
+ end
38
+ else
39
+ name
40
+ end
41
+ end
42
+
43
+ def self.translate?(property)
44
+ return true if property.is_a?(Field)
45
+
46
+ case property.type
47
+ when :string
48
+ true
49
+ when :integer, :boolean, :datetime, :float
50
+ false
51
+ when :multi
52
+ !property.all_ints?
53
+ end
54
+ end
55
+
56
+ def name
57
+ property.unique_name
58
+ end
59
+
60
+ def attribute_name
61
+ if translate?
62
+ Facet.attribute_name_for(@property.unique_name)
63
+ else
64
+ @property.unique_name.to_s
65
+ end
66
+ end
67
+
68
+ def translate?
69
+ Facet.translate?(@property)
70
+ end
71
+
72
+ def type
73
+ @property.is_a?(Field) ? :string : @property.type
74
+ end
75
+
76
+ def float?
77
+ @property.type == :float
78
+ end
79
+
80
+ def value(object, attribute_hash)
81
+ attribute_value = attribute_hash['@groupby']
82
+ return translate(object, attribute_value) if translate? || float?
83
+
84
+ case @property.type
85
+ when :datetime
86
+ Time.at(attribute_value)
87
+ when :boolean
88
+ attribute_value > 0
89
+ else
90
+ attribute_value
91
+ end
92
+ end
93
+
94
+ def to_s
95
+ name
96
+ end
97
+
98
+ private
99
+
100
+ def translate(object, attribute_value)
101
+ objects = source_objects(object)
102
+ return if objects.blank?
103
+
104
+ method = value_source || column.__name
105
+ object = objects.one? ? objects.first : objects.detect { |item|
106
+ result = item.send(method)
107
+ case result
108
+ when String
109
+ result.to_crc32 == attribute_value
110
+ when NilClass
111
+ false
112
+ else
113
+ result == attribute_value
114
+ end
115
+ }
116
+
117
+ object.try(method)
118
+ end
119
+
120
+ def source_objects(object)
121
+ column.__stack.each { |method|
122
+ object = Array(object).collect { |item|
123
+ item.send(method)
124
+ }.flatten.compact
125
+
126
+ return nil if object.empty?
127
+ }
128
+ Array(object)
129
+ end
130
+
131
+ def column
132
+ @property.columns.first
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,170 @@
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_facet 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
+ return if facet_names.empty?
48
+
49
+ ThinkingSphinx::Search.bundle_searches(facet_names) { |sphinx, name|
50
+ sphinx.search *(args + [facet_search_options(name)])
51
+ }.each_with_index { |search, index|
52
+ add_from_results facet_names[index], search
53
+ }
54
+ end
55
+
56
+ def facet_search_options(facet_name)
57
+ options.merge(
58
+ :group_function => :attr,
59
+ :limit => max_matches,
60
+ :max_matches => max_matches,
61
+ :page => 1,
62
+ :group_by => facet_name,
63
+ :ids_only => !translate?(facet_name)
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 translate?(name)
105
+ facet = facet_from_name(name)
106
+ facet.translate? || facet.float?
107
+ end
108
+
109
+ def config
110
+ ThinkingSphinx::Configuration.instance
111
+ end
112
+
113
+ def max_matches
114
+ @max_matches ||= config.configuration.searchd.max_matches || 1000
115
+ end
116
+
117
+ # example: facet = country_facet; name = :country
118
+ def add_from_results(facet, search)
119
+ name = ThinkingSphinx::Facet.name_for(facet)
120
+ facet = facet_from_name(facet)
121
+
122
+ self[name] ||= {}
123
+
124
+ return if search.empty?
125
+
126
+ search.each_with_match do |result, match|
127
+ facet_value = facet.value(result, match[:attributes])
128
+
129
+ self[name][facet_value] ||= 0
130
+ self[name][facet_value] += match[:attributes]["@count"]
131
+ end
132
+ end
133
+
134
+ def underlying_value(key, value)
135
+ case value
136
+ when Array
137
+ value.collect { |item| underlying_value(key, item) }
138
+ when String
139
+ value.to_crc32
140
+ else
141
+ value
142
+ end
143
+ end
144
+
145
+ def facet_from_object(object, name)
146
+ facet = nil
147
+ klass = object.class
148
+
149
+ while klass != ::ActiveRecord::Base && facet.nil?
150
+ facet = klass.sphinx_facets.detect { |facet|
151
+ facet.attribute_name == name
152
+ }
153
+ klass = klass.superclass
154
+ end
155
+
156
+ facet
157
+ end
158
+
159
+ def facet_from_name(name)
160
+ name = ThinkingSphinx::Facet.name_for(name)
161
+ all_facets.detect { |facet|
162
+ facet.name == name
163
+ }
164
+ end
165
+
166
+ def class_facet
167
+ Riddle.loaded_version.to_i < 2 ? 'class_crc' : 'sphinx_internal_class'
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,98 @@
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
+ # - :file => true
23
+ # - :with => :attribute # or :wordcount
24
+ #
25
+ # Alias is only required in three circumstances: when there's
26
+ # another attribute or field with the same name, when the column name is
27
+ # 'id', or when there's more than one column.
28
+ #
29
+ # Sortable defaults to false - but is quite useful when set to true, as
30
+ # it creates an attribute with the same string value (which Sphinx converts
31
+ # to an integer value), which can be sorted by. Thinking Sphinx is smart
32
+ # enough to realise that when you specify fields in sort statements, you
33
+ # mean their respective attributes.
34
+ #
35
+ # If you have partial matching enabled (ie: enable_star), then you can
36
+ # specify certain fields to have their prefixes and infixes indexed. Keep
37
+ # in mind, though, that Sphinx's default is _all_ fields - so once you
38
+ # highlight a particular field, no other fields in the index will have
39
+ # these partial indexes.
40
+ #
41
+ # Here's some examples:
42
+ #
43
+ # Field.new(
44
+ # Column.new(:name)
45
+ # )
46
+ #
47
+ # Field.new(
48
+ # [Column.new(:first_name), Column.new(:last_name)],
49
+ # :as => :name, :sortable => true
50
+ # )
51
+ #
52
+ # Field.new(
53
+ # [Column.new(:posts, :subject), Column.new(:posts, :content)],
54
+ # :as => :posts, :prefixes => true
55
+ # )
56
+ #
57
+ def initialize(source, columns, options = {})
58
+ super
59
+
60
+ @sortable = options[:sortable] || false
61
+ @infixes = options[:infixes] || false
62
+ @prefixes = options[:prefixes] || false
63
+ @file = options[:file] || false
64
+ @with = options[:with]
65
+
66
+ source.fields << self
67
+ end
68
+
69
+ # Get the part of the SELECT clause related to this field. Don't forget
70
+ # to set your model and associations first though.
71
+ #
72
+ # This will concatenate strings if there's more than one data source or
73
+ # multiple data values (has_many or has_and_belongs_to_many associations).
74
+ #
75
+ def to_select_sql
76
+ return nil unless available?
77
+
78
+ clause = columns_with_prefixes.join(', ')
79
+
80
+ clause = adapter.concatenate(clause) if concat_ws?
81
+ clause = adapter.group_concatenate(clause) if is_many?
82
+
83
+ "#{clause} AS #{quote_column(unique_name)}"
84
+ end
85
+
86
+ def file?
87
+ @file
88
+ end
89
+
90
+ def with_attribute?
91
+ @with == :attribute
92
+ end
93
+
94
+ def with_wordcount?
95
+ @with == :wordcount
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,315 @@
1
+ require 'blankslate' unless defined?(::BasicObject)
2
+
3
+ module ThinkingSphinx
4
+ class Index
5
+ # The Builder class is the core for the index definition block processing.
6
+ # There are four methods you really need to pay attention to:
7
+ # - indexes
8
+ # - has
9
+ # - where
10
+ # - set_property/set_properties
11
+ #
12
+ # The first two of these methods allow you to define what data makes up
13
+ # your indexes. #where provides a method to add manual SQL conditions, and
14
+ # set_property allows you to set some settings on a per-index basis. Check
15
+ # out each method's documentation for better ideas of usage.
16
+ #
17
+ class Builder < (defined?(::BasicObject) ? ::BasicObject : BlankSlate)
18
+ def self.generate(model, name = nil, &block)
19
+ index = ::ThinkingSphinx::Index.new(model)
20
+ index.name = name unless name.nil?
21
+
22
+ new(index, &block) if block_given?
23
+
24
+ index.delta_object = ::ThinkingSphinx::Deltas.parse index
25
+ index
26
+ end
27
+
28
+ def initialize(index, &block)
29
+ @index = index
30
+ @explicit_source = false
31
+
32
+ self.instance_eval &block
33
+
34
+ if no_fields?
35
+ raise "At least one field is necessary for an index"
36
+ end
37
+ end
38
+
39
+ def define_source(&block)
40
+ if @explicit_source
41
+ @source = ::ThinkingSphinx::Source.new(@index)
42
+ @index.sources << @source
43
+ else
44
+ @explicit_source = true
45
+ end
46
+
47
+ self.instance_eval &block
48
+ end
49
+
50
+ def use_local_indices(*indexes)
51
+ @index.additional_indices += indexes.map {|index_name| "#{index_name.to_s}_core"}
52
+ end
53
+ alias_method :use_local_index, :use_local_indices
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 = ::ThinkingSphinx::Field.new(source,
105
+ ::ThinkingSphinx::Index::FauxColumn.coerce(columns), options)
106
+
107
+ add_sort_attribute field, options if field.sortable
108
+ add_facet_attribute field, options if field.faceted
109
+ end
110
+ end
111
+
112
+ # This is the method to add attributes to your index (hence why it is
113
+ # aliased as 'attribute'). The syntax is the same as #indexes, so use
114
+ # that as starting point, but keep in mind the following points.
115
+ #
116
+ # An attribute can have an alias (the :as option), but it is always
117
+ # sortable - so you don't need to explicitly request that. You _can_
118
+ # specify the data type of the attribute (the :type option), but the
119
+ # code's pretty good at figuring that out itself from peering into the
120
+ # database.
121
+ #
122
+ # Attributes are limited to the following types: integers, floats,
123
+ # datetimes (converted to timestamps), booleans, strings and MVAs
124
+ # (:multi). Don't forget that Sphinx converts string attributes to
125
+ # integers, which are useful for sorting, but that's about it.
126
+ #
127
+ # Collection of integers are known as multi-value attributes (MVAs).
128
+ # Generally these would be through a has_many relationship, like in this
129
+ # example:
130
+ #
131
+ # has posts(:id), :as => :post_ids
132
+ #
133
+ # This allows you to filter on any of the values tied to a specific
134
+ # record. Might be best to read through the Sphinx documentation to get
135
+ # a better idea of that though.
136
+ #
137
+ # Adding SQL Fragment Attributes
138
+ #
139
+ # You can also define an attribute using an SQL fragment, useful for
140
+ # when you would like to index a calculated value. Don't forget to set
141
+ # the type of the attribute though:
142
+ #
143
+ # has "age < 18", :as => :minor, :type => :boolean
144
+ #
145
+ # If you're creating attributes for latitude and longitude, don't
146
+ # forget that Sphinx expects these values to be in radians.
147
+ #
148
+ def has(*args)
149
+ options = args.extract_options!
150
+ args.each do |columns|
151
+ attribute = ::ThinkingSphinx::Attribute.new(source,
152
+ ::ThinkingSphinx::Index::FauxColumn.coerce(columns), options)
153
+
154
+ add_facet_attribute attribute, options if attribute.faceted
155
+ end
156
+ end
157
+
158
+ def facet(*args)
159
+ options = args.extract_options!
160
+ options[:facet] = true
161
+
162
+ args.each do |columns|
163
+ attribute = ::ThinkingSphinx::Attribute.new(source,
164
+ ::ThinkingSphinx::Index::FauxColumn.coerce(columns), options)
165
+
166
+ add_facet_attribute attribute, options
167
+ end
168
+ end
169
+
170
+ def join(*args)
171
+ args.each do |association|
172
+ ::ThinkingSphinx::Join.new(source, association)
173
+ end
174
+ end
175
+
176
+ # Use this method to add some manual SQL conditions for your index
177
+ # request. You can pass in as many strings as you like, they'll get
178
+ # joined together with ANDs later on.
179
+ #
180
+ # where "user_id = 10"
181
+ # where "parent_type = 'Article'", "created_at < NOW()"
182
+ #
183
+ def where(*args)
184
+ source.conditions += args
185
+ end
186
+
187
+ # Use this method to add some manual SQL strings to the GROUP BY
188
+ # clause. You can pass in as many strings as you'd like, they'll get
189
+ # joined together with commas later on.
190
+ #
191
+ # group_by "lat", "lng"
192
+ #
193
+ def group_by(*args)
194
+ source.groupings += args
195
+ end
196
+
197
+ # This is what to use to set properties on the index. Chief amongst
198
+ # those is the delta property - to allow automatic updates to your
199
+ # indexes as new models are added and edited - but also you can
200
+ # define search-related properties which will be the defaults for all
201
+ # searches on the model.
202
+ #
203
+ # set_property :delta => true
204
+ # set_property :field_weights => {"name" => 100}
205
+ # set_property :order => "name ASC"
206
+ # set_property :select => 'name'
207
+ #
208
+ # Also, the following two properties are particularly relevant for
209
+ # geo-location searching - latitude_attr and longitude_attr. If your
210
+ # attributes for these two values are named something other than
211
+ # lat/latitude or lon/long/longitude, you can dictate what they are
212
+ # when defining the index, so you don't need to specify them for every
213
+ # geo-related search.
214
+ #
215
+ # set_property :latitude_attr => "lt", :longitude_attr => "lg"
216
+ #
217
+ # Please don't forget to add a boolean field named 'delta' to your
218
+ # model's database table if enabling the delta index for it.
219
+ # Valid options for the delta property are:
220
+ #
221
+ # true
222
+ # false
223
+ # :default
224
+ # :delayed
225
+ # :datetime
226
+ #
227
+ # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
228
+ # your own handling for delta indexing.
229
+ #
230
+ def set_property(*args)
231
+ options = args.extract_options!
232
+ options.each do |key, value|
233
+ set_single_property key, value
234
+ end
235
+
236
+ set_single_property args[0], args[1] if args.length == 2
237
+ end
238
+ alias_method :set_properties, :set_property
239
+
240
+ # Handles the generation of new columns for the field and attribute
241
+ # definitions.
242
+ #
243
+ def method_missing(method, *args)
244
+ ::ThinkingSphinx::Index::FauxColumn.new(method, *args)
245
+ end
246
+
247
+ # A method to allow adding fields from associations which have names
248
+ # that clash with method names in the Builder class (ie: properties,
249
+ # fields, attributes).
250
+ #
251
+ # Example: indexes assoc(:properties).column
252
+ #
253
+ def assoc(assoc, *args)
254
+ ::ThinkingSphinx::Index::FauxColumn.new(assoc, *args)
255
+ end
256
+
257
+ # Use this method to generate SQL for your attributes, conditions, etc.
258
+ # You can pass in as whatever ActiveRecord::Base.sanitize_sql accepts.
259
+ #
260
+ # where sanitize_sql(["active = ?", true])
261
+ # #=> WHERE active = 1
262
+ #
263
+ def sanitize_sql(*args)
264
+ @index.model.send(:sanitize_sql, *args)
265
+ end
266
+
267
+ private
268
+
269
+ def source
270
+ @source ||= begin
271
+ source = ::ThinkingSphinx::Source.new(@index)
272
+ @index.sources << source
273
+ source
274
+ end
275
+ end
276
+
277
+ def set_single_property(key, value)
278
+ source_options = ::ThinkingSphinx::Configuration::SourceOptions
279
+ if source_options.include?(key.to_s)
280
+ source.options.merge! key => value
281
+ else
282
+ @index.local_options.merge! key => value
283
+ end
284
+ end
285
+
286
+ def add_sort_attribute(field, options)
287
+ add_internal_attribute field, options, "_sort"
288
+ end
289
+
290
+ def add_facet_attribute(property, options)
291
+ add_internal_attribute property, options, "_facet", true
292
+ @index.model.sphinx_facets << property.to_facet
293
+ end
294
+
295
+ def add_internal_attribute(property, options, suffix, crc = false)
296
+ return unless ::ThinkingSphinx::Facet.translate?(property)
297
+
298
+ ::ThinkingSphinx::Attribute.new(source,
299
+ property.columns.collect { |col| col.clone },
300
+ options.merge(
301
+ :type => property.is_a?(::ThinkingSphinx::Field) ? :string : options[:type],
302
+ :as => property.unique_name.to_s.concat(suffix).to_sym,
303
+ :crc => crc
304
+ ).except(:facet)
305
+ )
306
+ end
307
+
308
+ def no_fields?
309
+ @index.sources.empty? || @index.sources.any? { |source|
310
+ source.fields.length == 0
311
+ }
312
+ end
313
+ end
314
+ end
315
+ end