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.
- data/LICENCE +20 -0
- data/README +60 -0
- data/lib/riddle.rb +26 -0
- data/lib/riddle/client.rb +639 -0
- data/lib/riddle/client/filter.rb +44 -0
- data/lib/riddle/client/message.rb +65 -0
- data/lib/riddle/client/response.rb +84 -0
- data/lib/test.rb +46 -0
- data/lib/thinking_sphinx.rb +102 -0
- data/lib/thinking_sphinx/active_record.rb +141 -0
- data/lib/thinking_sphinx/active_record/delta.rb +97 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
- data/lib/thinking_sphinx/active_record/search.rb +50 -0
- data/lib/thinking_sphinx/association.rb +144 -0
- data/lib/thinking_sphinx/attribute.rb +284 -0
- data/lib/thinking_sphinx/configuration.rb +283 -0
- data/lib/thinking_sphinx/field.rb +200 -0
- data/lib/thinking_sphinx/index.rb +340 -0
- data/lib/thinking_sphinx/index/builder.rb +195 -0
- data/lib/thinking_sphinx/index/faux_column.rb +110 -0
- data/lib/thinking_sphinx/rails_additions.rb +56 -0
- data/lib/thinking_sphinx/search.rb +482 -0
- data/lib/thinking_sphinx/tasks.rb +86 -0
- data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +207 -0
- data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
- data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
- data/spec/unit/thinking_sphinx/active_record_spec.rb +236 -0
- data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
- data/spec/unit/thinking_sphinx/attribute_spec.rb +360 -0
- data/spec/unit/thinking_sphinx/configuration_spec.rb +493 -0
- data/spec/unit/thinking_sphinx/field_spec.rb +219 -0
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +33 -0
- data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +68 -0
- data/spec/unit/thinking_sphinx/index_spec.rb +277 -0
- data/spec/unit/thinking_sphinx/search_spec.rb +190 -0
- data/spec/unit/thinking_sphinx_spec.rb +129 -0
- data/tasks/thinking_sphinx_tasks.rake +1 -0
- 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
|
data/lib/test.rb
ADDED
@@ -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
|