ryanb-thinking_sphinx 0.9.8

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 (38) hide show
  1. data/LICENCE +20 -0
  2. data/README +60 -0
  3. data/lib/riddle.rb +26 -0
  4. data/lib/riddle/client.rb +639 -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 +102 -0
  10. data/lib/thinking_sphinx/active_record.rb +141 -0
  11. data/lib/thinking_sphinx/active_record/delta.rb +97 -0
  12. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  13. data/lib/thinking_sphinx/active_record/search.rb +50 -0
  14. data/lib/thinking_sphinx/association.rb +144 -0
  15. data/lib/thinking_sphinx/attribute.rb +284 -0
  16. data/lib/thinking_sphinx/configuration.rb +283 -0
  17. data/lib/thinking_sphinx/field.rb +200 -0
  18. data/lib/thinking_sphinx/index.rb +340 -0
  19. data/lib/thinking_sphinx/index/builder.rb +195 -0
  20. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  21. data/lib/thinking_sphinx/rails_additions.rb +56 -0
  22. data/lib/thinking_sphinx/search.rb +482 -0
  23. data/lib/thinking_sphinx/tasks.rb +86 -0
  24. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +207 -0
  25. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  26. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
  27. data/spec/unit/thinking_sphinx/active_record_spec.rb +236 -0
  28. data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
  29. data/spec/unit/thinking_sphinx/attribute_spec.rb +360 -0
  30. data/spec/unit/thinking_sphinx/configuration_spec.rb +493 -0
  31. data/spec/unit/thinking_sphinx/field_spec.rb +219 -0
  32. data/spec/unit/thinking_sphinx/index/builder_spec.rb +33 -0
  33. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +68 -0
  34. data/spec/unit/thinking_sphinx/index_spec.rb +277 -0
  35. data/spec/unit/thinking_sphinx/search_spec.rb +190 -0
  36. data/spec/unit/thinking_sphinx_spec.rb +129 -0
  37. data/tasks/thinking_sphinx_tasks.rake +1 -0
  38. metadata +103 -0
@@ -0,0 +1,44 @@
1
+ module Riddle
2
+ class Client
3
+ # Used for querying Sphinx.
4
+ class Filter
5
+ attr_accessor :attribute, :values, :exclude
6
+
7
+ # Attribute name, values (which can be an array or a range), and whether
8
+ # the filter should be exclusive.
9
+ def initialize(attribute, values, exclude=false)
10
+ @attribute, @values, @exclude = attribute, values, exclude
11
+ end
12
+
13
+ def exclude?
14
+ self.exclude
15
+ end
16
+
17
+ # Returns the message for this filter to send to the Sphinx service
18
+ def query_message
19
+ message = Message.new
20
+
21
+ message.append_string self.attribute.to_s
22
+ case self.values
23
+ when Range
24
+ if self.values.first.is_a?(Float) && self.values.last.is_a?(Float)
25
+ message.append_int FilterTypes[:float_range]
26
+ message.append_floats self.values.first, self.values.last
27
+ else
28
+ message.append_int FilterTypes[:range]
29
+ message.append_ints self.values.first, self.values.last
30
+ end
31
+ when Array
32
+ message.append_int FilterTypes[:values]
33
+ message.append_int self.values.length
34
+ # using to_f is a hack from the php client - to workaround 32bit
35
+ # signed ints on x32 platforms
36
+ message.append_ints *self.values.collect { |val| val.to_f }
37
+ end
38
+ message.append_int self.exclude? ? 1 : 0
39
+
40
+ message.to_s
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,65 @@
1
+ module Riddle
2
+ class Client
3
+ # This class takes care of the translation of ints, strings and arrays to
4
+ # the format required by the Sphinx service.
5
+ class Message
6
+ def initialize
7
+ @message = ""
8
+ @size_method = @message.respond_to?(:bytesize) ? :bytesize : :length
9
+ end
10
+
11
+ # Append raw data (only use if you know what you're doing)
12
+ def append(*args)
13
+ return if args.length == 0
14
+
15
+ args.each { |arg| @message << arg }
16
+ end
17
+
18
+ # Append a string's length, then the string itself
19
+ def append_string(str)
20
+ @message << [str.send(@size_method)].pack('N') + str
21
+ end
22
+
23
+ # Append an integer
24
+ def append_int(int)
25
+ @message << [int].pack('N')
26
+ end
27
+
28
+ def append_64bit_int(int)
29
+ @message << [int >> 32, int & 0xFFFFFFFF].pack('NN')
30
+ end
31
+
32
+ # Append a float
33
+ def append_float(float)
34
+ @message << [float].pack('f').unpack('L*').pack("N")
35
+ end
36
+
37
+ # Append multiple integers
38
+ def append_ints(*ints)
39
+ ints.each { |int| append_int(int) }
40
+ end
41
+
42
+ def append_64bit_ints(*ints)
43
+ ints.each { |int| append_64bit_int(int) }
44
+ end
45
+
46
+ # Append multiple floats
47
+ def append_floats(*floats)
48
+ floats.each { |float| append_float(float) }
49
+ end
50
+
51
+ # Append an array of strings - first appends the length of the array,
52
+ # then each item's length and value.
53
+ def append_array(array)
54
+ append_int(array.length)
55
+
56
+ array.each { |item| append_string(item) }
57
+ end
58
+
59
+ # Returns the entire message
60
+ def to_s
61
+ @message
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,84 @@
1
+ module Riddle
2
+ class Client
3
+ # Used to interrogate responses from the Sphinx daemon. Keep in mind none
4
+ # of the methods here check whether the data they're grabbing are what the
5
+ # user expects - it just assumes the user knows what the data stream is
6
+ # made up of.
7
+ class Response
8
+ # Create with the data to interpret
9
+ def initialize(str)
10
+ @str = str
11
+ @marker = 0
12
+ end
13
+
14
+ # Return the next string value in the stream
15
+ def next
16
+ len = next_int
17
+ result = @str[@marker, len]
18
+ @marker += len
19
+
20
+ return result
21
+ end
22
+
23
+ # Return the next integer value from the stream
24
+ def next_int
25
+ int = @str[@marker, 4].unpack('N*').first
26
+ @marker += 4
27
+
28
+ return int
29
+ end
30
+
31
+ def next_64bit_int
32
+ high, low = @str[@marker, 8].unpack('N*N*')[0..1]
33
+ @marker += 8
34
+
35
+ return (high << 32) + low
36
+ end
37
+
38
+ # Return the next float value from the stream
39
+ def next_float
40
+ float = @str[@marker, 4].unpack('N*').pack('L').unpack('f*').first
41
+ @marker += 4
42
+
43
+ return float
44
+ end
45
+
46
+ # Returns an array of string items
47
+ def next_array
48
+ count = next_int
49
+ items = []
50
+ for i in 0...count
51
+ items << self.next
52
+ end
53
+
54
+ return items
55
+ end
56
+
57
+ # Returns an array of int items
58
+ def next_int_array
59
+ count = next_int
60
+ items = []
61
+ for i in 0...count
62
+ items << self.next_int
63
+ end
64
+
65
+ return items
66
+ end
67
+
68
+ def next_float_array
69
+ count = next_int
70
+ items = []
71
+ for i in 0...count
72
+ items << self.next_float
73
+ end
74
+
75
+ return items
76
+ end
77
+
78
+ # Returns the length of the streamed data
79
+ def length
80
+ @str.length
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,46 @@
1
+ require 'thinking_sphinx'
2
+
3
+ ActiveRecord::Base.establish_connection(
4
+ :adapter => 'mysql',
5
+ :database => 'nullus_development',
6
+ :username => 'nullus',
7
+ :password => 'wossname',
8
+ :host => 'localhost'
9
+ )
10
+ ActiveRecord::Base.logger = Logger.new(STDERR)
11
+
12
+ class User < ActiveRecord::Base
13
+ has_many :posts, :foreign_key => "created_by"
14
+ end
15
+
16
+ class Post < ActiveRecord::Base
17
+ belongs_to :creator, :foreign_key => "created_by", :class_name => "User"
18
+ belongs_to :updater, :foreign_key => "updated_by", :class_name => "User"
19
+ belongs_to :topic
20
+ end
21
+
22
+ class Topic < ActiveRecord::Base
23
+ belongs_to :creator, :foreign_key => "created_by", :class_name => "User"
24
+ belongs_to :forum
25
+ has_many :posts
26
+ end
27
+
28
+ class Forum < ActiveRecord::Base
29
+ belongs_to :creator, :foreign_key => "created_by", :class_name => "User"
30
+ has_many :topics
31
+ end
32
+
33
+ def index
34
+ @index ||= ThinkingSphinx::Index.new(Topic) do
35
+ indexes posts.content, :as => :posts
36
+ indexes posts.creator.login, :as => :authors
37
+
38
+ has :created_at
39
+ has :id, :as => :topic_id
40
+ has :forum_id
41
+ has posts(:id), :as => :post_ids
42
+ has posts.creator(:id), :as => :user_ids
43
+
44
+ where "posts.created_at < NOW()"
45
+ end
46
+ end
@@ -0,0 +1,102 @@
1
+ require 'active_record'
2
+ require 'riddle'
3
+
4
+ require 'thinking_sphinx/active_record'
5
+ require 'thinking_sphinx/association'
6
+ require 'thinking_sphinx/attribute'
7
+ require 'thinking_sphinx/configuration'
8
+ require 'thinking_sphinx/field'
9
+ require 'thinking_sphinx/index'
10
+ require 'thinking_sphinx/rails_additions'
11
+ require 'thinking_sphinx/search'
12
+
13
+ ActiveRecord::Base.send(:include, ThinkingSphinx::ActiveRecord)
14
+
15
+ Merb::Plugins.add_rakefiles(
16
+ File.join(File.dirname(__FILE__), "thinking_sphinx", "tasks")
17
+ ) if defined?(Merb)
18
+
19
+ module ThinkingSphinx
20
+ module Version #:nodoc:
21
+ Major = 0
22
+ Minor = 9
23
+ Tiny = 8
24
+
25
+ String = [Major, Minor, Tiny].join('.')
26
+ end
27
+
28
+ # A ConnectionError will get thrown when a connection to Sphinx can't be
29
+ # made.
30
+ class ConnectionError < StandardError
31
+ end
32
+
33
+ # The collection of indexed models. Keep in mind that Rails lazily loads
34
+ # its classes, so this may not actually be populated with _all_ the models
35
+ # that have Sphinx indexes.
36
+ def self.indexed_models
37
+ @@indexed_models ||= []
38
+ end
39
+
40
+ # Check if index definition is disabled.
41
+ #
42
+ def self.define_indexes?
43
+ @@define_indexes = true unless defined?(@@define_indexes)
44
+ @@define_indexes == true
45
+ end
46
+
47
+ # Enable/disable indexes - you may want to do this while migrating data.
48
+ #
49
+ # ThinkingSphinx.define_indexes = false
50
+ #
51
+ def self.define_indexes=(value)
52
+ @@define_indexes = value
53
+ end
54
+
55
+ @@deltas_enabled = nil
56
+
57
+ # Check if delta indexing is enabled.
58
+ #
59
+ def self.deltas_enabled?
60
+ @@deltas_enabled = (ThinkingSphinx::Configuration.environment != 'test') if @@deltas_enabled.nil?
61
+ @@deltas_enabled
62
+ end
63
+
64
+ # Enable/disable all delta indexing.
65
+ #
66
+ # ThinkingSphinx.deltas_enabled = false
67
+ #
68
+ def self.deltas_enabled=(value)
69
+ @@deltas_enabled = value
70
+ end
71
+
72
+ @@updates_enabled = nil
73
+
74
+ # Check if updates are enabled. True by default, unless within the test
75
+ # environment.
76
+ #
77
+ def self.updates_enabled?
78
+ @@updates_enabled = (ThinkingSphinx::Configuration.environment != 'test') if @@updates_enabled.nil?
79
+ @@updates_enabled
80
+ end
81
+
82
+ # Enable/disable updates to Sphinx
83
+ #
84
+ # ThinkingSphinx.updates_enabled = false
85
+ #
86
+ def self.updates_enabled=(value)
87
+ @@updates_enabled = value
88
+ end
89
+
90
+ # Checks to see if MySQL will allow simplistic GROUP BY statements. If not,
91
+ # or if not using MySQL, this will return false.
92
+ #
93
+ def self.use_group_by_shortcut?
94
+ ::ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter") &&
95
+ ::ActiveRecord::Base.connection.is_a?(
96
+ ::ActiveRecord::ConnectionAdapters::MysqlAdapter
97
+ ) &&
98
+ ::ActiveRecord::Base.connection.select_all(
99
+ "SELECT @@global.sql_mode, @@session.sql_mode;"
100
+ ).all? { |key,value| value.nil? || value[/ONLY_FULL_GROUP_BY/].nil? }
101
+ end
102
+ end
@@ -0,0 +1,141 @@
1
+ require 'thinking_sphinx/active_record/delta'
2
+ require 'thinking_sphinx/active_record/search'
3
+ require 'thinking_sphinx/active_record/has_many_association'
4
+
5
+ module ThinkingSphinx
6
+ # Core additions to ActiveRecord models - define_index for creating indexes
7
+ # for models. If you want to interrogate the index objects created for the
8
+ # model, you can use the class-level accessor :indexes.
9
+ #
10
+ module ActiveRecord
11
+ def self.included(base)
12
+ base.class_eval do
13
+ class_inheritable_array :indexes
14
+ class << self
15
+ # Allows creation of indexes for Sphinx. If you don't do this, there
16
+ # isn't much point trying to search (or using this plugin at all,
17
+ # really).
18
+ #
19
+ # An example or two:
20
+ #
21
+ # define_index
22
+ # indexes :id, :as => :model_id
23
+ # indexes name
24
+ # end
25
+ #
26
+ # You can also grab fields from associations - multiple levels deep
27
+ # if necessary.
28
+ #
29
+ # define_index do
30
+ # indexes tags.name, :as => :tag
31
+ # indexes articles.content
32
+ # indexes orders.line_items.product.name, :as => :product
33
+ # end
34
+ #
35
+ # And it will automatically concatenate multiple fields:
36
+ #
37
+ # define_index do
38
+ # indexes [author.first_name, author.last_name], :as => :author
39
+ # end
40
+ #
41
+ # The #indexes method is for fields - if you want attributes, use
42
+ # #has instead. All the same rules apply - but keep in mind that
43
+ # attributes are for sorting, grouping and filtering, not searching.
44
+ #
45
+ # define_index do
46
+ # # fields ...
47
+ #
48
+ # has created_at, updated_at
49
+ # end
50
+ #
51
+ # One last feature is the delta index. This requires the model to
52
+ # have a boolean field named 'delta', and is enabled as follows:
53
+ #
54
+ # define_index do
55
+ # # fields ...
56
+ # # attributes ...
57
+ #
58
+ # set_property :delta => true
59
+ # end
60
+ #
61
+ # Check out the more detailed documentation for each of these methods
62
+ # at ThinkingSphinx::Index::Builder.
63
+ #
64
+ def define_index(&block)
65
+ return unless ThinkingSphinx.define_indexes?
66
+
67
+ self.indexes ||= []
68
+ index = Index.new(self, &block)
69
+
70
+ self.indexes << index
71
+ unless ThinkingSphinx.indexed_models.include?(self.name)
72
+ ThinkingSphinx.indexed_models << self.name
73
+ end
74
+
75
+ if index.delta?
76
+ before_save :toggle_delta
77
+ after_commit :index_delta
78
+ end
79
+
80
+ after_destroy :toggle_deleted
81
+
82
+ index
83
+ end
84
+ alias_method :sphinx_index, :define_index
85
+
86
+ # Generate a unique CRC value for the model's name, to use to
87
+ # determine which Sphinx documents belong to which AR records.
88
+ #
89
+ # Really only written for internal use - but hey, if it's useful to
90
+ # you in some other way, awesome.
91
+ #
92
+ def to_crc32
93
+ result = 0xFFFFFFFF
94
+ self.name.each_byte do |byte|
95
+ result ^= byte
96
+ 8.times do
97
+ result = (result >> 1) ^ (0xEDB88320 * (result & 1))
98
+ end
99
+ end
100
+ result ^ 0xFFFFFFFF
101
+ end
102
+ end
103
+ end
104
+
105
+ base.send(:include, ThinkingSphinx::ActiveRecord::Delta)
106
+ base.send(:include, ThinkingSphinx::ActiveRecord::Search)
107
+
108
+ ::ActiveRecord::Associations::HasManyAssociation.send(
109
+ :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
110
+ )
111
+ ::ActiveRecord::Associations::HasManyThroughAssociation.send(
112
+ :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
113
+ )
114
+ end
115
+
116
+ def in_core_index?
117
+ @in_core_index ||= self.class.search_for_id(self.id, "#{self.class.name.downcase}_core")
118
+ end
119
+
120
+ def toggle_deleted
121
+ return unless ThinkingSphinx.updates_enabled?
122
+
123
+ config = ThinkingSphinx::Configuration.new
124
+ client = Riddle::Client.new config.address, config.port
125
+
126
+ client.update(
127
+ "#{self.class.indexes.first.name}_core",
128
+ ['sphinx_deleted'],
129
+ {self.id => 1}
130
+ ) if self.in_core_index?
131
+
132
+ client.update(
133
+ "#{self.class.indexes.first.name}_delta",
134
+ ['sphinx_deleted'],
135
+ {self.id => 1}
136
+ ) if ThinkingSphinx.deltas_enabled? &&
137
+ self.class.indexes.any? { |index| index.delta? } &&
138
+ self.delta?
139
+ end
140
+ end
141
+ end