freelancing-god-thinking-sphinx 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/LICENCE +20 -0
  2. data/README +25 -0
  3. data/lib/riddle.rb +22 -0
  4. data/lib/riddle/client.rb +593 -0
  5. data/lib/riddle/client/filter.rb +44 -0
  6. data/lib/riddle/client/message.rb +65 -0
  7. data/lib/riddle/client/response.rb +84 -0
  8. data/lib/test.rb +46 -0
  9. data/lib/thinking_sphinx.rb +79 -0
  10. data/lib/thinking_sphinx/active_record.rb +115 -0
  11. data/lib/thinking_sphinx/active_record/delta.rb +86 -0
  12. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  13. data/lib/thinking_sphinx/active_record/search.rb +36 -0
  14. data/lib/thinking_sphinx/association.rb +140 -0
  15. data/lib/thinking_sphinx/attribute.rb +279 -0
  16. data/lib/thinking_sphinx/configuration.rb +275 -0
  17. data/lib/thinking_sphinx/field.rb +186 -0
  18. data/lib/thinking_sphinx/index.rb +234 -0
  19. data/lib/thinking_sphinx/index/builder.rb +197 -0
  20. data/lib/thinking_sphinx/index/faux_column.rb +97 -0
  21. data/lib/thinking_sphinx/rails_additions.rb +56 -0
  22. data/lib/thinking_sphinx/search.rb +413 -0
  23. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +184 -0
  24. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  25. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +0 -0
  26. data/spec/unit/thinking_sphinx/active_record_spec.rb +85 -0
  27. data/spec/unit/thinking_sphinx/association_spec.rb +0 -0
  28. data/spec/unit/thinking_sphinx/attribute_spec.rb +73 -0
  29. data/spec/unit/thinking_sphinx/configuration_spec.rb +7 -0
  30. data/spec/unit/thinking_sphinx/field_spec.rb +51 -0
  31. data/spec/unit/thinking_sphinx/index/builder_spec.rb +33 -0
  32. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +41 -0
  33. data/spec/unit/thinking_sphinx/index_spec.rb +5 -0
  34. data/spec/unit/thinking_sphinx/search_spec.rb +121 -0
  35. data/spec/unit/thinking_sphinx_spec.rb +82 -0
  36. data/tasks/thinking_sphinx_tasks.rake +1 -0
  37. data/tasks/thinking_sphinx_tasks.rb +86 -0
  38. metadata +90 -0
@@ -0,0 +1,234 @@
1
+ require 'thinking_sphinx/index/builder'
2
+ require 'thinking_sphinx/index/faux_column'
3
+
4
+ module ThinkingSphinx
5
+ # The Index class is a ruby representation of a Sphinx source (not a Sphinx
6
+ # index - yes, I know it's a little confusing. You'll manage). This is
7
+ # another 'internal' Thinking Sphinx class - if you're using it directly,
8
+ # you either know what you're doing, or messing with things beyond your ken.
9
+ # Enjoy.
10
+ #
11
+ class Index
12
+ attr_accessor :model, :fields, :attributes, :conditions, :delta, :options
13
+
14
+ # Create a new index instance by passing in the model it is tied to, and
15
+ # a block to build it with (optional but recommended). For documentation
16
+ # on the syntax for inside the block, the Builder class is what you want.
17
+ #
18
+ # Quick Example:
19
+ #
20
+ # Index.new(User) do
21
+ # indexes login, email
22
+ #
23
+ # has created_at
24
+ #
25
+ # set_property :delta => true
26
+ # end
27
+ #
28
+ def initialize(model, &block)
29
+ @model = model
30
+ @associations = {}
31
+ @fields = []
32
+ @attributes = []
33
+ @conditions = []
34
+ @options = {}
35
+ @delta = false
36
+
37
+ initialize_from_builder(&block) if block_given?
38
+ end
39
+
40
+ # Link all the fields and associations to their corresponding
41
+ # associations and joins. This _must_ be called before interrogating
42
+ # the index's fields and associations for anything that may reference
43
+ # their SQL structure.
44
+ #
45
+ def link!
46
+ base = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(
47
+ @model, [], nil
48
+ )
49
+
50
+ @fields.each { |field|
51
+ field.model ||= @model
52
+ field.columns.each { |col|
53
+ field.associations[col] = associations(col.__stack.clone)
54
+ field.associations[col].each { |assoc| assoc.join_to(base) }
55
+ }
56
+ }
57
+
58
+ @attributes.each { |attribute|
59
+ attribute.model ||= @model
60
+ attribute.columns.each { |col|
61
+ attribute.associations[col] = associations(col.__stack.clone)
62
+ attribute.associations[col].each { |assoc| assoc.join_to(base) }
63
+ }
64
+ }
65
+ end
66
+
67
+ # Generates the big SQL statement to get the data back for all the fields
68
+ # and attributes, using all the relevant association joins. If you want
69
+ # the version filtered for delta values, send through :delta => true in the
70
+ # options. Won't do much though if the index isn't set up to support a
71
+ # delta sibling.
72
+ #
73
+ # Examples:
74
+ #
75
+ # index.to_sql
76
+ # index.to_sql(:delta => true)
77
+ #
78
+ def to_sql(options={})
79
+ assocs = all_associations
80
+
81
+ where_clause = ""
82
+ if self.delta?
83
+ where_clause << " AND #{@model.quoted_table_name}.#{quote_column('delta')}" +" = #{options[:delta] ? 1 : 0}"
84
+ end
85
+ unless @conditions.empty?
86
+ where_clause << " AND " << @conditions.join(" AND ")
87
+ end
88
+
89
+ <<-SQL
90
+ SELECT #{ (
91
+ ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)}"] +
92
+ @fields.collect { |field| field.to_select_sql } +
93
+ @attributes.collect { |attribute| attribute.to_select_sql }
94
+ ).join(", ") }
95
+ FROM #{ @model.table_name }
96
+ #{ assocs.collect { |assoc| assoc.to_sql }.join(' ') }
97
+ WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} >= $start
98
+ AND #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} <= $end
99
+ #{ where_clause }
100
+ GROUP BY #{ (
101
+ ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)}"] +
102
+ @fields.collect { |field| field.to_group_sql }.compact +
103
+ @attributes.collect { |attribute| attribute.to_group_sql }.compact
104
+ ).join(", ") }
105
+ ORDER BY NULL
106
+ SQL
107
+ end
108
+
109
+ # Simple helper method for the query info SQL - which is a statement that
110
+ # returns the single row for a corresponding id.
111
+ #
112
+ def to_sql_query_info
113
+ "SELECT * FROM #{@model.quoted_table_name} WHERE " +
114
+ " #{quote_column(@model.primary_key)} = $id"
115
+ end
116
+
117
+ # Simple helper method for the query range SQL - which is a statement that
118
+ # returns minimum and maximum id values. These can be filtered by delta -
119
+ # so pass in :delta => true to get the delta version of the SQL.
120
+ #
121
+ def to_sql_query_range(options={})
122
+ sql = "SELECT MIN(#{quote_column(@model.primary_key)}), " +
123
+ "MAX(#{quote_column(@model.primary_key)}) " +
124
+ "FROM #{@model.quoted_table_name} "
125
+ sql << "WHERE #{@model.quoted_table_name}.#{quote_column('delta')} " +
126
+ "= #{options[:delta] ? 1 : 0}" if self.delta?
127
+ sql
128
+ end
129
+
130
+ # Returns the SQL query to run before a full index - ie: nothing unless the
131
+ # index has a delta, and then it's an update statement to set delta values
132
+ # back to 0.
133
+ #
134
+ def to_sql_query_pre
135
+ self.delta? ? "UPDATE #{@model.quoted_table_name} SET #{quote_column('delta')} = 0" : ""
136
+ end
137
+
138
+ # Flag to indicate whether this index has a corresponding delta index.
139
+ #
140
+ def delta?
141
+ @delta
142
+ end
143
+
144
+ def adapter
145
+ @adapter ||= case @model.connection.class.name
146
+ when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
147
+ :mysql
148
+ when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
149
+ :postgres
150
+ else
151
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def quote_column(column)
158
+ @model.connection.quote_column_name(column)
159
+ end
160
+
161
+ # Does all the magic with the block provided to the base #initialize.
162
+ # Creates a new class subclassed from Builder, and evaluates the block
163
+ # on it, then pulls all relevant settings - fields, attributes, conditions,
164
+ # properties - into the new index.
165
+ #
166
+ # Also creates a CRC attribute for the model.
167
+ #
168
+ def initialize_from_builder(&block)
169
+ builder = Class.new(Builder)
170
+ builder.setup
171
+
172
+ builder.instance_eval &block
173
+
174
+ @fields = builder.fields
175
+ @attributes = builder.attributes
176
+ @conditions = builder.conditions
177
+ @delta = builder.properties[:delta]
178
+ @options = builder.properties.except(:delta)
179
+
180
+ @attributes << Attribute.new(
181
+ FauxColumn.new(@model.to_crc32.to_s),
182
+ :type => :integer,
183
+ :as => :class_crc
184
+ )
185
+ end
186
+
187
+ # Returns all associations used amongst all the fields and attributes.
188
+ # This includes all associations between the model and what the actual
189
+ # columns are from.
190
+ #
191
+ def all_associations
192
+ @all_associations ||= (
193
+ # field associations
194
+ @fields.collect { |field|
195
+ field.associations.values
196
+ }.flatten +
197
+ # attribute associations
198
+ @attributes.collect { |attrib|
199
+ attrib.associations.values
200
+ }.flatten
201
+ ).uniq.collect { |assoc|
202
+ # get ancestors as well as column-level associations
203
+ assoc.ancestors
204
+ }.flatten.uniq
205
+ end
206
+
207
+ # Gets a stack of associations for a specific path.
208
+ #
209
+ def associations(path, parent = nil)
210
+ assocs = []
211
+
212
+ if parent.nil?
213
+ assocs = association(path.shift)
214
+ else
215
+ assocs = parent.children(path.shift)
216
+ end
217
+
218
+ until path.empty?
219
+ point = path.shift
220
+ assocs = assocs.collect { |assoc|
221
+ assoc.children(point)
222
+ }.flatten
223
+ end
224
+
225
+ assocs
226
+ end
227
+
228
+ # Gets the association stack for a specific key.
229
+ #
230
+ def association(key)
231
+ @associations[key] ||= Association.children(@model, key)
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,197 @@
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 (aliased to includes and attribute)
6
+ # - has (aliased to attribute)
7
+ # - where
8
+ # - set_property (aliased to 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
+ class << self
17
+ # No idea where this is coming from - haven't found it in any ruby or
18
+ # rails documentation. It's not needed though, so it gets undef'd.
19
+ # Hopefully the list of methods that get in the way doesn't get too
20
+ # long.
21
+ undef_method :parent
22
+
23
+ attr_accessor :fields, :attributes, :properties, :conditions
24
+
25
+ # Set up all the collections. Consider this the equivalent of an
26
+ # instance's initialize method.
27
+ #
28
+ def setup
29
+ @fields = []
30
+ @attributes = []
31
+ @properties = {}
32
+ @conditions = []
33
+ end
34
+
35
+ # This is how you add fields - the strings Sphinx looks at - to your
36
+ # index. Technically, to use this method, you need to pass in some
37
+ # columns and options - but there's some neat method_missing stuff
38
+ # happening, so lets stick to the expected syntax within a define_index
39
+ # block.
40
+ #
41
+ # Expected options are :as, which points to a column alias in symbol
42
+ # form, and :sortable, which indicates whether you want to sort by this
43
+ # field.
44
+ #
45
+ # Adding Single-Column Fields:
46
+ #
47
+ # You can use symbols or methods - and can chain methods together to
48
+ # get access down the associations tree.
49
+ #
50
+ # indexes :id, :as => :my_id
51
+ # indexes first_name, last_name, :sortable => true
52
+ # indexes name, :sortable => true
53
+ # indexes users.posts.content, :as => :post_content
54
+ # indexes users(:id), :as => :user_ids
55
+ #
56
+ # Keep in mind that if any keywords for Ruby methods - such as id -
57
+ # clash with your column names, you need to use the symbol version (see
58
+ # the first and last examples above).
59
+ #
60
+ # If you specify multiple columns (example #2), a field will be created
61
+ # for each. Don't use the :as option in this case. If you want to merge
62
+ # those columns together, continue reading.
63
+ #
64
+ # Adding Multi-Column Fields:
65
+ #
66
+ # indexes [first_name, last_name], :as => :name
67
+ # indexes [location, parent.location], :as => :location
68
+ #
69
+ # To combine multiple columns into a single field, you need to wrap
70
+ # them in an Array, as shown by the above examples. There's no
71
+ # limitations on whether they're symbols or methods or what level of
72
+ # associations they come from.
73
+ #
74
+ def indexes(*args)
75
+ options = args.extract_options!
76
+ args.each do |columns|
77
+ columns = FauxColumn.new(columns) if columns.is_a?(Symbol)
78
+ fields << Field.new(columns, options)
79
+
80
+ if fields.last.sortable
81
+ attributes << Attribute.new(
82
+ fields.last.columns.collect { |col| col.clone },
83
+ options.merge(
84
+ :type => :string,
85
+ :as => fields.last.unique_name.to_s.concat("_sort").to_sym
86
+ )
87
+ )
88
+ end
89
+ end
90
+ end
91
+ alias_method :field, :indexes
92
+ alias_method :includes, :indexes
93
+
94
+ # This is the method to add attributes to your index (hence why it is
95
+ # aliased as 'attribute'). The syntax is the same as #indexes, so use
96
+ # that as starting point, but keep in mind the following points.
97
+ #
98
+ # An attribute can have an alias (the :as option), but it is always
99
+ # sortable - so you don't need to explicitly request that. You _can_
100
+ # specify the data type of the attribute (the :type option), but the
101
+ # code's pretty good at figuring that out itself from peering into the
102
+ # database.
103
+ #
104
+ # Attributes are limited to the following types: integers, floats,
105
+ # datetimes (converted to timestamps), booleans and strings. Don't
106
+ # forget that Sphinx converts string attributes to integers, which are
107
+ # useful for sorting, but that's about it.
108
+ #
109
+ # You can also have a collection of integers for multi-value attributes
110
+ # (MVAs). Generally these would be through a has_many relationship,
111
+ # like in this example:
112
+ #
113
+ # has posts(:id), :as => :post_ids
114
+ #
115
+ # This allows you to filter on any of the values tied to a specific
116
+ # record. Might be best to read through the Sphinx documentation to get
117
+ # a better idea of that though.
118
+ #
119
+ # If you're creating attributes for latitude and longitude, don't
120
+ # forget that Sphinx expects these values to be in radians.
121
+ #
122
+ def has(*args)
123
+ options = args.extract_options!
124
+ args.each do |columns|
125
+ columns = case columns
126
+ when Symbol, String
127
+ FauxColumn.new(columns)
128
+ when Array
129
+ columns.collect { |col|
130
+ case col
131
+ when Symbol, String
132
+ FauxColumn.new(col)
133
+ else
134
+ col
135
+ end
136
+ }
137
+ else
138
+ columns
139
+ end
140
+
141
+ attributes << Attribute.new(columns, options)
142
+ end
143
+ end
144
+ alias_method :attribute, :has
145
+
146
+ # Use this method to add some manual SQL conditions for your index
147
+ # request. You can pass in as many strings as you like, they'll get
148
+ # joined together with ANDs later on.
149
+ #
150
+ # where "user_id = 10"
151
+ # where "parent_type = 'Article'", "created_at < NOW()"
152
+ #
153
+ def where(*args)
154
+ @conditions += args
155
+ end
156
+
157
+ # This is what to use to set properties on the index. Chief amongst
158
+ # those is the delta property - to allow automatic updates to your
159
+ # indexes as new models are added and edited - but also you can
160
+ # define search-related properties which will be the defaults for all
161
+ # searches on the model.
162
+ #
163
+ # set_property :delta => true
164
+ # set_property :field_weights => {"name" => 100}
165
+ #
166
+ # Also, the following two properties are particularly relevant for
167
+ # geo-location searching - latitude_attr and longitude_attr. If your
168
+ # attributes for these two values are named something other than
169
+ # lat/latitude or lon/long/longitude, you can dictate what they are
170
+ # when defining the index, so you don't need to specify them for every
171
+ # geo-related search.
172
+ #
173
+ # set_property :latitude_attr => "lt", :longitude => "lg"
174
+ #
175
+ # Please don't forget to add a boolean field named 'delta' to your
176
+ # model's database table if enabling the delta index for it.
177
+ #
178
+ def set_property(*args)
179
+ options = args.extract_options!
180
+ if options.empty?
181
+ @properties[args[0]] = args[1]
182
+ else
183
+ @properties.merge!(options)
184
+ end
185
+ end
186
+ alias_method :set_properties, :set_property
187
+
188
+ # Handles the generation of new columns for the field and attribute
189
+ # definitions.
190
+ #
191
+ def method_missing(method, *args)
192
+ FauxColumn.new(method, *args)
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,97 @@
1
+ module ThinkingSphinx
2
+ class Index
3
+ # Instances of this class represent database columns and the stack of
4
+ # associations that lead from the base model to them.
5
+ #
6
+ # The name and stack are accessible through methods starting with __ to
7
+ # avoid conflicting with the method_missing calls that build the stack.
8
+ #
9
+ class FauxColumn
10
+ # Create a new column with a pre-defined stack. The top element in the
11
+ # stack will get shifted to be the name value.
12
+ #
13
+ def initialize(*stack)
14
+ @name = stack.pop
15
+ @stack = stack
16
+ end
17
+
18
+ # Can't use normal method name, as that could be an association or
19
+ # column name.
20
+ #
21
+ def __name
22
+ @name
23
+ end
24
+
25
+ # Can't use normal method name, as that could be an association or
26
+ # column name.
27
+ #
28
+ def __stack
29
+ @stack
30
+ end
31
+
32
+ # Returns true if the stack is empty *and* if the name is a string -
33
+ # which is an indication that of raw SQL, as opposed to a value from a
34
+ # table's column.
35
+ #
36
+ def is_string?
37
+ @name.is_a?(String) && @stack.empty?
38
+ end
39
+
40
+ # This handles any 'invalid' method calls and sets them as the name,
41
+ # and pushing the previous name into the stack. The object returns
42
+ # itself.
43
+ #
44
+ # If there's a single argument, it becomes the name, and the method
45
+ # symbol goes into the stack as well. Multiple arguments means new
46
+ # columns with the original stack and new names (from each argument) gets
47
+ # returned.
48
+ #
49
+ # Easier to explain with examples:
50
+ #
51
+ # col = FauxColumn.new :a, :b, :c
52
+ # col.__name #=> :c
53
+ # col.__stack #=> [:a, :b]
54
+ #
55
+ # col.whatever #=> col
56
+ # col.__name #=> :whatever
57
+ # col.__stack #=> [:a, :b, :c]
58
+ #
59
+ # col.something(:id) #=> col
60
+ # col.__name #=> :id
61
+ # col.__stack #=> [:a, :b, :c, :whatever, :something]
62
+ #
63
+ # cols = col.short(:x, :y, :z)
64
+ # cols[0].__name #=> :x
65
+ # cols[0].__stack #=> [:a, :b, :c, :whatever, :something, :short]
66
+ # cols[1].__name #=> :y
67
+ # cols[1].__stack #=> [:a, :b, :c, :whatever, :something, :short]
68
+ # cols[2].__name #=> :z
69
+ # cols[2].__stack #=> [:a, :b, :c, :whatever, :something, :short]
70
+ #
71
+ # Also, this allows method chaining to build up a relevant stack:
72
+ #
73
+ # col = FauxColumn.new :a, :b
74
+ # col.__name #=> :b
75
+ # col.__stack #=> [:a]
76
+ #
77
+ # col.one.two.three #=> col
78
+ # col.__name #=> :three
79
+ # col.__stack #=> [:a, :b, :one, :two]
80
+ #
81
+ def method_missing(method, *args)
82
+ @stack << @name
83
+ @name = method
84
+
85
+ if (args.empty?)
86
+ self
87
+ elsif (args.length == 1)
88
+ method_missing(args.first)
89
+ else
90
+ args.collect { |arg|
91
+ FauxColumn.new(@stack + [@name, arg])
92
+ }
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end