moneypools-thinking-sphinx 1.2.11
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.textile +157 -0
- data/VERSION.yml +4 -0
- data/lib/thinking_sphinx.rb +211 -0
- data/lib/thinking_sphinx/active_record.rb +307 -0
- data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
- data/lib/thinking_sphinx/active_record/delta.rb +87 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +28 -0
- data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +136 -0
- data/lib/thinking_sphinx/association.rb +164 -0
- data/lib/thinking_sphinx/attribute.rb +340 -0
- data/lib/thinking_sphinx/class_facet.rb +15 -0
- data/lib/thinking_sphinx/configuration.rb +282 -0
- data/lib/thinking_sphinx/core/array.rb +7 -0
- data/lib/thinking_sphinx/core/string.rb +15 -0
- data/lib/thinking_sphinx/deltas.rb +30 -0
- data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +68 -0
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +34 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
- data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
- data/lib/thinking_sphinx/excerpter.rb +22 -0
- data/lib/thinking_sphinx/facet.rb +125 -0
- data/lib/thinking_sphinx/facet_search.rb +134 -0
- data/lib/thinking_sphinx/field.rb +82 -0
- data/lib/thinking_sphinx/index.rb +99 -0
- data/lib/thinking_sphinx/index/builder.rb +286 -0
- data/lib/thinking_sphinx/index/faux_column.rb +110 -0
- data/lib/thinking_sphinx/property.rb +162 -0
- data/lib/thinking_sphinx/rails_additions.rb +150 -0
- data/lib/thinking_sphinx/search.rb +689 -0
- data/lib/thinking_sphinx/search_methods.rb +421 -0
- data/lib/thinking_sphinx/source.rb +150 -0
- data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
- data/lib/thinking_sphinx/source/sql.rb +128 -0
- data/lib/thinking_sphinx/tasks.rb +165 -0
- data/rails/init.rb +14 -0
- data/spec/lib/thinking_sphinx/active_record/delta_spec.rb +130 -0
- data/spec/lib/thinking_sphinx/active_record/has_many_association_spec.rb +49 -0
- data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
- data/spec/lib/thinking_sphinx/active_record_spec.rb +364 -0
- data/spec/lib/thinking_sphinx/association_spec.rb +239 -0
- data/spec/lib/thinking_sphinx/attribute_spec.rb +500 -0
- data/spec/lib/thinking_sphinx/configuration_spec.rb +335 -0
- data/spec/lib/thinking_sphinx/core/array_spec.rb +9 -0
- data/spec/lib/thinking_sphinx/core/string_spec.rb +9 -0
- data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
- data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
- data/spec/lib/thinking_sphinx/facet_spec.rb +333 -0
- data/spec/lib/thinking_sphinx/field_spec.rb +154 -0
- data/spec/lib/thinking_sphinx/index/builder_spec.rb +455 -0
- data/spec/lib/thinking_sphinx/index/faux_column_spec.rb +30 -0
- data/spec/lib/thinking_sphinx/index_spec.rb +45 -0
- data/spec/lib/thinking_sphinx/rails_additions_spec.rb +203 -0
- data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
- data/spec/lib/thinking_sphinx/search_spec.rb +1066 -0
- data/spec/lib/thinking_sphinx/source_spec.rb +227 -0
- data/spec/lib/thinking_sphinx_spec.rb +162 -0
- data/tasks/distribution.rb +49 -0
- data/tasks/rails.rake +1 -0
- data/tasks/testing.rb +83 -0
- data/vendor/after_commit/LICENSE +20 -0
- data/vendor/after_commit/README +16 -0
- data/vendor/after_commit/Rakefile +22 -0
- data/vendor/after_commit/init.rb +8 -0
- data/vendor/after_commit/lib/after_commit.rb +45 -0
- data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
- data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
- data/vendor/after_commit/test/after_commit_test.rb +53 -0
- data/vendor/riddle/lib/riddle.rb +30 -0
- data/vendor/riddle/lib/riddle/client.rb +622 -0
- data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
- data/vendor/riddle/lib/riddle/client/message.rb +66 -0
- data/vendor/riddle/lib/riddle/client/response.rb +84 -0
- data/vendor/riddle/lib/riddle/configuration.rb +33 -0
- data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
- data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
- data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
- data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
- data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
- data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
- data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
- data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
- data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
- data/vendor/riddle/lib/riddle/controller.rb +54 -0
- metadata +168 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2008 Nick Muerdter
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
after_commit
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
A Ruby on Rails plugin to add after_commit callbacks. The callbacks that are provided can be used
|
|
5
|
+
to trigger events that run only after the entire transaction is complete. This is beneficial
|
|
6
|
+
in situations where you are doing asynchronous processing and need committed objects.
|
|
7
|
+
|
|
8
|
+
The following callbacks are provided:
|
|
9
|
+
|
|
10
|
+
* (1) after_commit
|
|
11
|
+
* (2) after_commit_on_create
|
|
12
|
+
* (3) after_commit_on_update
|
|
13
|
+
* (4) after_commit_on_destroy
|
|
14
|
+
|
|
15
|
+
The after_commit callback is run for any object that has just been committed. You can obtain finer
|
|
16
|
+
callback control by using the additional <tt>after_commit_on_*</tt> callbacks.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'rake'
|
|
2
|
+
require 'rake/testtask'
|
|
3
|
+
require 'rake/rdoctask'
|
|
4
|
+
|
|
5
|
+
desc 'Default: run unit tests.'
|
|
6
|
+
task :default => :test
|
|
7
|
+
|
|
8
|
+
desc 'Test the after_commit plugin.'
|
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
|
10
|
+
t.libs << 'lib'
|
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
|
12
|
+
t.verbose = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc 'Generate documentation for the after_commit plugin.'
|
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
18
|
+
rdoc.title = 'AfterCommit'
|
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
|
20
|
+
rdoc.rdoc_files.include('README')
|
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
22
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ActiveRecord::Base.send(:include, AfterCommit::ActiveRecord)
|
|
2
|
+
|
|
3
|
+
Object.subclasses_of(ActiveRecord::ConnectionAdapters::AbstractAdapter).each do |klass|
|
|
4
|
+
klass.send(:include, AfterCommit::ConnectionAdapters)
|
|
5
|
+
end
|
|
6
|
+
if defined?(JRUBY_VERSION) and defined?(JdbcSpec::MySQL)
|
|
7
|
+
JdbcSpec::MySQL.send :include, AfterCommit::ConnectionAdapters
|
|
8
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'after_commit/active_record'
|
|
2
|
+
require 'after_commit/connection_adapters'
|
|
3
|
+
|
|
4
|
+
module AfterCommit
|
|
5
|
+
def self.committed_records
|
|
6
|
+
@@committed_records ||= []
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.committed_records=(committed_records)
|
|
10
|
+
@@committed_records = committed_records
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.committed_records_on_create
|
|
14
|
+
@@committed_records_on_create ||= []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.committed_records_on_create=(committed_records)
|
|
18
|
+
@@committed_records_on_create = committed_records
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.committed_records_on_update
|
|
22
|
+
@@committed_records_on_update ||= []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.committed_records_on_update=(committed_records)
|
|
26
|
+
@@committed_records_on_update = committed_records
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.committed_records_on_destroy
|
|
30
|
+
@@committed_records_on_destroy ||= []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.committed_records_on_destroy=(committed_records)
|
|
34
|
+
@@committed_records_on_destroy = committed_records
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
ActiveRecord::Base.send(:include, AfterCommit::ActiveRecord)
|
|
39
|
+
|
|
40
|
+
Object.subclasses_of(ActiveRecord::ConnectionAdapters::AbstractAdapter).each do |klass|
|
|
41
|
+
klass.send(:include, AfterCommit::ConnectionAdapters)
|
|
42
|
+
end
|
|
43
|
+
if defined?(JRUBY_VERSION) and defined?(JdbcSpec::MySQL)
|
|
44
|
+
JdbcSpec::MySQL.send :include, AfterCommit::ConnectionAdapters
|
|
45
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module AfterCommit
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
# Based on the code found in Thinking Sphinx:
|
|
4
|
+
# http://ts.freelancing-gods.com/ which was based on code written by Eli
|
|
5
|
+
# Miller:
|
|
6
|
+
# http://elimiller.blogspot.com/2007/06/proper-cache-expiry-with-aftercommit.html
|
|
7
|
+
# with slight modification from Joost Hietbrink. And now me! Whew.
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.class_eval do
|
|
10
|
+
# The define_callbacks method was added post Rails 2.0.2 - if it
|
|
11
|
+
# doesn't exist, we define the callback manually
|
|
12
|
+
if respond_to?(:define_callbacks)
|
|
13
|
+
define_callbacks :after_commit,
|
|
14
|
+
:after_commit_on_create,
|
|
15
|
+
:after_commit_on_update,
|
|
16
|
+
:after_commit_on_destroy
|
|
17
|
+
else
|
|
18
|
+
class << self
|
|
19
|
+
# Handle after_commit callbacks - call all the registered callbacks.
|
|
20
|
+
def after_commit(*callbacks, &block)
|
|
21
|
+
callbacks << block if block_given?
|
|
22
|
+
write_inheritable_array(:after_commit, callbacks)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def after_commit_on_create(*callbacks, &block)
|
|
26
|
+
callbacks << block if block_given?
|
|
27
|
+
write_inheritable_array(:after_commit_on_create, callbacks)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def after_commit_on_update(*callbacks, &block)
|
|
31
|
+
callbacks << block if block_given?
|
|
32
|
+
write_inheritable_array(:after_commit_on_update, callbacks)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def after_commit_on_destroy(*callbacks, &block)
|
|
36
|
+
callbacks << block if block_given?
|
|
37
|
+
write_inheritable_array(:after_commit_on_destroy, callbacks)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
after_save :add_committed_record
|
|
43
|
+
after_create :add_committed_record_on_create
|
|
44
|
+
after_update :add_committed_record_on_update
|
|
45
|
+
after_destroy :add_committed_record_on_destroy
|
|
46
|
+
|
|
47
|
+
# We need to keep track of records that have been saved or destroyed
|
|
48
|
+
# within this transaction.
|
|
49
|
+
def add_committed_record
|
|
50
|
+
AfterCommit.committed_records << self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def add_committed_record_on_create
|
|
54
|
+
AfterCommit.committed_records_on_create << self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_committed_record_on_update
|
|
58
|
+
AfterCommit.committed_records_on_update << self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_committed_record_on_destroy
|
|
62
|
+
AfterCommit.committed_records << self
|
|
63
|
+
AfterCommit.committed_records_on_destroy << self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def after_commit
|
|
67
|
+
# Deliberately blank.
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Wraps a call to the private callback method so that the the
|
|
71
|
+
# after_commit callback can be made from the ConnectionAdapters when
|
|
72
|
+
# the commit for the transaction has finally succeeded.
|
|
73
|
+
def after_commit_callback
|
|
74
|
+
call_after_commit_callback :after_commit
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def after_commit_on_create_callback
|
|
78
|
+
call_after_commit_callback :after_commit_on_create
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def after_commit_on_update_callback
|
|
82
|
+
call_after_commit_callback :after_commit_on_update
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def after_commit_on_destroy_callback
|
|
86
|
+
call_after_commit_callback :after_commit_on_destroy
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def call_after_commit_callback(call)
|
|
92
|
+
if can_call_after_commit call
|
|
93
|
+
callback call
|
|
94
|
+
clear_after_commit_call call
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def can_call_after_commit(call)
|
|
99
|
+
@calls ||= {}
|
|
100
|
+
@calls[call] ||= false
|
|
101
|
+
if @calls[call]
|
|
102
|
+
return false
|
|
103
|
+
else
|
|
104
|
+
@calls[call] = true
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def clear_after_commit_call(call)
|
|
109
|
+
@calls[call] = false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module AfterCommit
|
|
2
|
+
module ConnectionAdapters
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.class_eval do
|
|
5
|
+
# The commit_db_transaction method gets called when the outermost
|
|
6
|
+
# transaction finishes and everything inside commits. We want to
|
|
7
|
+
# override it so that after this happens, any records that were saved
|
|
8
|
+
# or destroyed within this transaction now get their after_commit
|
|
9
|
+
# callback fired.
|
|
10
|
+
def commit_db_transaction_with_callback
|
|
11
|
+
commit_db_transaction_without_callback
|
|
12
|
+
trigger_after_commit_callbacks
|
|
13
|
+
trigger_after_commit_on_create_callbacks
|
|
14
|
+
trigger_after_commit_on_update_callbacks
|
|
15
|
+
trigger_after_commit_on_destroy_callbacks
|
|
16
|
+
end
|
|
17
|
+
alias_method_chain :commit_db_transaction, :callback
|
|
18
|
+
|
|
19
|
+
# In the event the transaction fails and rolls back, nothing inside
|
|
20
|
+
# should recieve the after_commit callback.
|
|
21
|
+
def rollback_db_transaction_with_callback
|
|
22
|
+
rollback_db_transaction_without_callback
|
|
23
|
+
|
|
24
|
+
AfterCommit.committed_records = []
|
|
25
|
+
AfterCommit.committed_records_on_create = []
|
|
26
|
+
AfterCommit.committed_records_on_update = []
|
|
27
|
+
AfterCommit.committed_records_on_destroy = []
|
|
28
|
+
end
|
|
29
|
+
alias_method_chain :rollback_db_transaction, :callback
|
|
30
|
+
|
|
31
|
+
protected
|
|
32
|
+
def trigger_after_commit_callbacks
|
|
33
|
+
# Trigger the after_commit callback for each of the committed
|
|
34
|
+
# records.
|
|
35
|
+
if AfterCommit.committed_records.any?
|
|
36
|
+
AfterCommit.committed_records.each do |record|
|
|
37
|
+
begin
|
|
38
|
+
record.after_commit_callback
|
|
39
|
+
rescue
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Make sure we clear out our list of committed records now that we've
|
|
45
|
+
# triggered the callbacks for each one.
|
|
46
|
+
AfterCommit.committed_records = []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def trigger_after_commit_on_create_callbacks
|
|
50
|
+
# Trigger the after_commit_on_create callback for each of the committed
|
|
51
|
+
# records.
|
|
52
|
+
if AfterCommit.committed_records_on_create.any?
|
|
53
|
+
AfterCommit.committed_records_on_create.each do |record|
|
|
54
|
+
begin
|
|
55
|
+
record.after_commit_on_create_callback
|
|
56
|
+
rescue
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Make sure we clear out our list of committed records now that we've
|
|
62
|
+
# triggered the callbacks for each one.
|
|
63
|
+
AfterCommit.committed_records_on_create = []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def trigger_after_commit_on_update_callbacks
|
|
67
|
+
# Trigger the after_commit_on_update callback for each of the committed
|
|
68
|
+
# records.
|
|
69
|
+
if AfterCommit.committed_records_on_update.any?
|
|
70
|
+
AfterCommit.committed_records_on_update.each do |record|
|
|
71
|
+
begin
|
|
72
|
+
record.after_commit_on_update_callback
|
|
73
|
+
rescue
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Make sure we clear out our list of committed records now that we've
|
|
79
|
+
# triggered the callbacks for each one.
|
|
80
|
+
AfterCommit.committed_records_on_update = []
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def trigger_after_commit_on_destroy_callbacks
|
|
84
|
+
# Trigger the after_commit_on_destroy callback for each of the committed
|
|
85
|
+
# records.
|
|
86
|
+
if AfterCommit.committed_records_on_destroy.any?
|
|
87
|
+
AfterCommit.committed_records_on_destroy.each do |record|
|
|
88
|
+
begin
|
|
89
|
+
record.after_commit_on_destroy_callback
|
|
90
|
+
rescue
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Make sure we clear out our list of committed records now that we've
|
|
96
|
+
# triggered the callbacks for each one.
|
|
97
|
+
AfterCommit.committed_records_on_destroy = []
|
|
98
|
+
end
|
|
99
|
+
#end protected
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
|
|
2
|
+
require 'test/unit'
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'activerecord'
|
|
5
|
+
require 'after_commit'
|
|
6
|
+
require 'after_commit/active_record'
|
|
7
|
+
require 'after_commit/connection_adapters'
|
|
8
|
+
|
|
9
|
+
ActiveRecord::Base.establish_connection({"adapter" => "sqlite3", "database" => 'test.sqlite3'})
|
|
10
|
+
begin
|
|
11
|
+
ActiveRecord::Base.connection.execute("drop table mock_records");
|
|
12
|
+
rescue
|
|
13
|
+
end
|
|
14
|
+
ActiveRecord::Base.connection.execute("create table mock_records(id int)");
|
|
15
|
+
|
|
16
|
+
require File.dirname(__FILE__) + '/../init.rb'
|
|
17
|
+
|
|
18
|
+
class MockRecord < ActiveRecord::Base
|
|
19
|
+
attr_accessor :after_commit_on_create_called
|
|
20
|
+
attr_accessor :after_commit_on_update_called
|
|
21
|
+
attr_accessor :after_commit_on_destroy_called
|
|
22
|
+
|
|
23
|
+
after_commit_on_create :do_create
|
|
24
|
+
def do_create
|
|
25
|
+
self.after_commit_on_create_called = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
after_commit_on_update :do_update
|
|
29
|
+
def do_update
|
|
30
|
+
self.after_commit_on_update_called = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
after_commit_on_create :do_destroy
|
|
34
|
+
def do_destroy
|
|
35
|
+
self.after_commit_on_destroy_called = true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class AfterCommitTest < Test::Unit::TestCase
|
|
40
|
+
def test_after_commit_on_create_is_called
|
|
41
|
+
assert_equal true, MockRecord.create!.after_commit_on_create_called
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_after_commit_on_update_is_called
|
|
45
|
+
record = MockRecord.create!
|
|
46
|
+
record.save
|
|
47
|
+
assert_equal true, record.after_commit_on_update_called
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_after_commit_on_destroy_is_called
|
|
51
|
+
assert_equal true, MockRecord.create!.destroy.after_commit_on_destroy_called
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
|
|
4
|
+
require 'riddle/client'
|
|
5
|
+
require 'riddle/configuration'
|
|
6
|
+
require 'riddle/controller'
|
|
7
|
+
|
|
8
|
+
module Riddle #:nodoc:
|
|
9
|
+
class ConnectionError < StandardError #:nodoc:
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Version #:nodoc:
|
|
13
|
+
Major = 0
|
|
14
|
+
Minor = 9
|
|
15
|
+
Tiny = 8
|
|
16
|
+
# Revision number for RubyForge's sake, taken from what Sphinx
|
|
17
|
+
# outputs to the command line.
|
|
18
|
+
Rev = 1533
|
|
19
|
+
# Release number to mark my own fixes, beyond feature parity with
|
|
20
|
+
# Sphinx itself.
|
|
21
|
+
Release = 7
|
|
22
|
+
|
|
23
|
+
String = [Major, Minor, Tiny].join('.')
|
|
24
|
+
GemVersion = [Major, Minor, Tiny, Rev, Release].join('.')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.escape(string)
|
|
28
|
+
string.gsub(/[\(\)\|\-!@~"&\/]/) { |char| "\\#{char}" }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
require 'riddle/client/filter'
|
|
2
|
+
require 'riddle/client/message'
|
|
3
|
+
require 'riddle/client/response'
|
|
4
|
+
|
|
5
|
+
module Riddle
|
|
6
|
+
class VersionError < StandardError; end
|
|
7
|
+
class ResponseError < StandardError; end
|
|
8
|
+
|
|
9
|
+
# This class was heavily based on the existing Client API by Dmytro Shteflyuk
|
|
10
|
+
# and Alexy Kovyrin. Their code worked fine, I just wanted something a bit
|
|
11
|
+
# more Ruby-ish (ie. lowercase and underscored method names). I also have
|
|
12
|
+
# used a few helper classes, just to neaten things up.
|
|
13
|
+
#
|
|
14
|
+
# Feel free to use it wherever. Send bug reports, patches, comments and
|
|
15
|
+
# suggestions to pat at freelancing-gods dot com.
|
|
16
|
+
#
|
|
17
|
+
# Most properties of the client are accessible through attribute accessors,
|
|
18
|
+
# and where relevant use symboles instead of the long constants common in
|
|
19
|
+
# other clients.
|
|
20
|
+
# Some examples:
|
|
21
|
+
#
|
|
22
|
+
# client.sort_mode = :extended
|
|
23
|
+
# client.sort_by = "birthday DESC"
|
|
24
|
+
# client.match_mode = :extended
|
|
25
|
+
#
|
|
26
|
+
# To add a filter, you will need to create a Filter object:
|
|
27
|
+
#
|
|
28
|
+
# client.filters << Riddle::Client::Filter.new("birthday",
|
|
29
|
+
# Time.at(1975, 1, 1).to_i..Time.at(1985, 1, 1).to_i, false)
|
|
30
|
+
#
|
|
31
|
+
class Client
|
|
32
|
+
Commands = {
|
|
33
|
+
:search => 0, # SEARCHD_COMMAND_SEARCH
|
|
34
|
+
:excerpt => 1, # SEARCHD_COMMAND_EXCERPT
|
|
35
|
+
:update => 2, # SEARCHD_COMMAND_UPDATE
|
|
36
|
+
:keywords => 3 # SEARCHD_COMMAND_KEYWORDS
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Versions = {
|
|
40
|
+
:search => 0x113, # VER_COMMAND_SEARCH
|
|
41
|
+
:excerpt => 0x100, # VER_COMMAND_EXCERPT
|
|
42
|
+
:update => 0x101, # VER_COMMAND_UPDATE
|
|
43
|
+
:keywords => 0x100 # VER_COMMAND_KEYWORDS
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Statuses = {
|
|
47
|
+
:ok => 0, # SEARCHD_OK
|
|
48
|
+
:error => 1, # SEARCHD_ERROR
|
|
49
|
+
:retry => 2, # SEARCHD_RETRY
|
|
50
|
+
:warning => 3 # SEARCHD_WARNING
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
MatchModes = {
|
|
54
|
+
:all => 0, # SPH_MATCH_ALL
|
|
55
|
+
:any => 1, # SPH_MATCH_ANY
|
|
56
|
+
:phrase => 2, # SPH_MATCH_PHRASE
|
|
57
|
+
:boolean => 3, # SPH_MATCH_BOOLEAN
|
|
58
|
+
:extended => 4, # SPH_MATCH_EXTENDED
|
|
59
|
+
:fullscan => 5, # SPH_MATCH_FULLSCAN
|
|
60
|
+
:extended2 => 6 # SPH_MATCH_EXTENDED2
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
RankModes = {
|
|
64
|
+
:proximity_bm25 => 0, # SPH_RANK_PROXIMITY_BM25
|
|
65
|
+
:bm25 => 1, # SPH_RANK_BM25
|
|
66
|
+
:none => 2, # SPH_RANK_NONE
|
|
67
|
+
:wordcount => 3 # SPH_RANK_WORDCOUNT
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
SortModes = {
|
|
71
|
+
:relevance => 0, # SPH_SORT_RELEVANCE
|
|
72
|
+
:attr_desc => 1, # SPH_SORT_ATTR_DESC
|
|
73
|
+
:attr_asc => 2, # SPH_SORT_ATTR_ASC
|
|
74
|
+
:time_segments => 3, # SPH_SORT_TIME_SEGMENTS
|
|
75
|
+
:extended => 4, # SPH_SORT_EXTENDED
|
|
76
|
+
:expr => 5 # SPH_SORT_EXPR
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
AttributeTypes = {
|
|
80
|
+
:integer => 1, # SPH_ATTR_INTEGER
|
|
81
|
+
:timestamp => 2, # SPH_ATTR_TIMESTAMP
|
|
82
|
+
:ordinal => 3, # SPH_ATTR_ORDINAL
|
|
83
|
+
:bool => 4, # SPH_ATTR_BOOL
|
|
84
|
+
:float => 5, # SPH_ATTR_FLOAT
|
|
85
|
+
:multi => 0x40000000 # SPH_ATTR_MULTI
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
GroupFunctions = {
|
|
89
|
+
:day => 0, # SPH_GROUPBY_DAY
|
|
90
|
+
:week => 1, # SPH_GROUPBY_WEEK
|
|
91
|
+
:month => 2, # SPH_GROUPBY_MONTH
|
|
92
|
+
:year => 3, # SPH_GROUPBY_YEAR
|
|
93
|
+
:attr => 4, # SPH_GROUPBY_ATTR
|
|
94
|
+
:attrpair => 5 # SPH_GROUPBY_ATTRPAIR
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
FilterTypes = {
|
|
98
|
+
:values => 0, # SPH_FILTER_VALUES
|
|
99
|
+
:range => 1, # SPH_FILTER_RANGE
|
|
100
|
+
:float_range => 2 # SPH_FILTER_FLOATRANGE
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
attr_accessor :server, :port, :offset, :limit, :max_matches,
|
|
104
|
+
:match_mode, :sort_mode, :sort_by, :weights, :id_range, :filters,
|
|
105
|
+
:group_by, :group_function, :group_clause, :group_distinct, :cut_off,
|
|
106
|
+
:retry_count, :retry_delay, :anchor, :index_weights, :rank_mode,
|
|
107
|
+
:max_query_time, :field_weights, :timeout
|
|
108
|
+
attr_reader :queue
|
|
109
|
+
|
|
110
|
+
# Can instantiate with a specific server and port - otherwise it assumes
|
|
111
|
+
# defaults of localhost and 3312 respectively. All other settings can be
|
|
112
|
+
# accessed and changed via the attribute accessors.
|
|
113
|
+
def initialize(server=nil, port=nil)
|
|
114
|
+
@server = server || "localhost"
|
|
115
|
+
@port = port || 3312
|
|
116
|
+
|
|
117
|
+
reset
|
|
118
|
+
|
|
119
|
+
@queue = []
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Reset attributes and settings to defaults.
|
|
123
|
+
def reset
|
|
124
|
+
# defaults
|
|
125
|
+
@offset = 0
|
|
126
|
+
@limit = 20
|
|
127
|
+
@max_matches = 1000
|
|
128
|
+
@match_mode = :all
|
|
129
|
+
@sort_mode = :relevance
|
|
130
|
+
@sort_by = ''
|
|
131
|
+
@weights = []
|
|
132
|
+
@id_range = 0..0
|
|
133
|
+
@filters = []
|
|
134
|
+
@group_by = ''
|
|
135
|
+
@group_function = :day
|
|
136
|
+
@group_clause = '@group desc'
|
|
137
|
+
@group_distinct = ''
|
|
138
|
+
@cut_off = 0
|
|
139
|
+
@retry_count = 0
|
|
140
|
+
@retry_delay = 0
|
|
141
|
+
@anchor = {}
|
|
142
|
+
# string keys are index names, integer values are weightings
|
|
143
|
+
@index_weights = {}
|
|
144
|
+
@rank_mode = :proximity_bm25
|
|
145
|
+
@max_query_time = 0
|
|
146
|
+
# string keys are field names, integer values are weightings
|
|
147
|
+
@field_weights = {}
|
|
148
|
+
@timeout = 0
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Set the geo-anchor point - with the names of the attributes that contain
|
|
152
|
+
# the latitude and longitude (in radians), and the reference position.
|
|
153
|
+
# Note that for geocoding to work properly, you must also set
|
|
154
|
+
# match_mode to :extended. To sort results by distance, you will
|
|
155
|
+
# need to set sort_mode to '@geodist asc' for example. Sphinx
|
|
156
|
+
# expects latitude and longitude to be returned from you SQL source
|
|
157
|
+
# in radians.
|
|
158
|
+
#
|
|
159
|
+
# Example:
|
|
160
|
+
# client.set_anchor('lat', -0.6591741, 'long', 2.530770)
|
|
161
|
+
#
|
|
162
|
+
def set_anchor(lat_attr, lat, long_attr, long)
|
|
163
|
+
@anchor = {
|
|
164
|
+
:latitude_attribute => lat_attr,
|
|
165
|
+
:latitude => lat,
|
|
166
|
+
:longitude_attribute => long_attr,
|
|
167
|
+
:longitude => long
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Append a query to the queue. This uses the same parameters as the query
|
|
172
|
+
# method.
|
|
173
|
+
def append_query(search, index = '*', comments = '')
|
|
174
|
+
@queue << query_message(search, index, comments)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Run all the queries currently in the queue. This will return an array of
|
|
178
|
+
# results hashes.
|
|
179
|
+
def run
|
|
180
|
+
response = Response.new request(:search, @queue)
|
|
181
|
+
|
|
182
|
+
results = @queue.collect do
|
|
183
|
+
result = {
|
|
184
|
+
:matches => [],
|
|
185
|
+
:fields => [],
|
|
186
|
+
:attributes => {},
|
|
187
|
+
:attribute_names => [],
|
|
188
|
+
:words => {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
result[:status] = response.next_int
|
|
192
|
+
case result[:status]
|
|
193
|
+
when Statuses[:warning]
|
|
194
|
+
result[:warning] = response.next
|
|
195
|
+
when Statuses[:error]
|
|
196
|
+
result[:error] = response.next
|
|
197
|
+
next result
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
result[:fields] = response.next_array
|
|
201
|
+
|
|
202
|
+
attributes = response.next_int
|
|
203
|
+
for i in 0...attributes
|
|
204
|
+
attribute_name = response.next
|
|
205
|
+
type = response.next_int
|
|
206
|
+
|
|
207
|
+
result[:attributes][attribute_name] = type
|
|
208
|
+
result[:attribute_names] << attribute_name
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
matches = response.next_int
|
|
212
|
+
is_64_bit = response.next_int
|
|
213
|
+
for i in 0...matches
|
|
214
|
+
doc = is_64_bit > 0 ? response.next_64bit_int : response.next_int
|
|
215
|
+
weight = response.next_int
|
|
216
|
+
|
|
217
|
+
result[:matches] << {:doc => doc, :weight => weight, :index => i, :attributes => {}}
|
|
218
|
+
result[:attribute_names].each do |attr|
|
|
219
|
+
result[:matches].last[:attributes][attr] = attribute_from_type(
|
|
220
|
+
result[:attributes][attr], response
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
result[:total] = response.next_int.to_i || 0
|
|
226
|
+
result[:total_found] = response.next_int.to_i || 0
|
|
227
|
+
result[:time] = ('%.3f' % (response.next_int / 1000.0)).to_f || 0.0
|
|
228
|
+
|
|
229
|
+
words = response.next_int
|
|
230
|
+
for i in 0...words
|
|
231
|
+
word = response.next
|
|
232
|
+
docs = response.next_int
|
|
233
|
+
hits = response.next_int
|
|
234
|
+
result[:words][word] = {:docs => docs, :hits => hits}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
result
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
@queue.clear
|
|
241
|
+
results
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Query the Sphinx daemon - defaulting to all indexes, but you can specify
|
|
245
|
+
# a specific one if you wish. The search parameter should be a string
|
|
246
|
+
# following Sphinx's expectations.
|
|
247
|
+
#
|
|
248
|
+
# The object returned from this method is a hash with the following keys:
|
|
249
|
+
#
|
|
250
|
+
# * :matches
|
|
251
|
+
# * :fields
|
|
252
|
+
# * :attributes
|
|
253
|
+
# * :attribute_names
|
|
254
|
+
# * :words
|
|
255
|
+
# * :total
|
|
256
|
+
# * :total_found
|
|
257
|
+
# * :time
|
|
258
|
+
# * :status
|
|
259
|
+
# * :warning (if appropriate)
|
|
260
|
+
# * :error (if appropriate)
|
|
261
|
+
#
|
|
262
|
+
# The key <tt>:matches</tt> returns an array of hashes - the actual search
|
|
263
|
+
# results. Each hash has the document id (<tt>:doc</tt>), the result
|
|
264
|
+
# weighting (<tt>:weight</tt>), and a hash of the attributes for the
|
|
265
|
+
# document (<tt>:attributes</tt>).
|
|
266
|
+
#
|
|
267
|
+
# The <tt>:fields</tt> and <tt>:attribute_names</tt> keys return list of
|
|
268
|
+
# fields and attributes for the documents. The key <tt>:attributes</tt>
|
|
269
|
+
# will return a hash of attribute name and type pairs, and <tt>:words</tt>
|
|
270
|
+
# returns a hash of hashes representing the words from the search, with the
|
|
271
|
+
# number of documents and hits for each, along the lines of:
|
|
272
|
+
#
|
|
273
|
+
# results[:words]["Pat"] #=> {:docs => 12, :hits => 15}
|
|
274
|
+
#
|
|
275
|
+
# <tt>:total</tt>, <tt>:total_found</tt> and <tt>:time</tt> return the
|
|
276
|
+
# number of matches available, the total number of matches (which may be
|
|
277
|
+
# greater than the maximum available, depending on the number of matches
|
|
278
|
+
# and your sphinx configuration), and the time in milliseconds that the
|
|
279
|
+
# query took to run.
|
|
280
|
+
#
|
|
281
|
+
# <tt>:status</tt> is the error code for the query - and if there was a
|
|
282
|
+
# related warning, it will be under the <tt>:warning</tt> key. Fatal errors
|
|
283
|
+
# will be described under <tt>:error</tt>.
|
|
284
|
+
#
|
|
285
|
+
def query(search, index = '*', comments = '')
|
|
286
|
+
@queue << query_message(search, index, comments)
|
|
287
|
+
self.run.first
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Build excerpts from search terms (the +words+) and the text of documents. Excerpts are bodies of text that have the +words+ highlighted.
|
|
291
|
+
# They may also be abbreviated to fit within a word limit.
|
|
292
|
+
#
|
|
293
|
+
# As part of the options hash, you will need to
|
|
294
|
+
# define:
|
|
295
|
+
# * :docs
|
|
296
|
+
# * :words
|
|
297
|
+
# * :index
|
|
298
|
+
#
|
|
299
|
+
# Optional settings include:
|
|
300
|
+
# * :before_match (defaults to <span class="match">)
|
|
301
|
+
# * :after_match (defaults to </span>)
|
|
302
|
+
# * :chunk_separator (defaults to ' … ' - which is an HTML ellipsis)
|
|
303
|
+
# * :limit (defaults to 256)
|
|
304
|
+
# * :around (defaults to 5)
|
|
305
|
+
# * :exact_phrase (defaults to false)
|
|
306
|
+
# * :single_passage (defaults to false)
|
|
307
|
+
#
|
|
308
|
+
# The defaults differ from the official PHP client, as I've opted for
|
|
309
|
+
# semantic HTML markup.
|
|
310
|
+
#
|
|
311
|
+
# Example:
|
|
312
|
+
#
|
|
313
|
+
# client.excerpts(:docs => ["Pat Allan, Pat Cash"], :words => 'Pat', :index => 'pats')
|
|
314
|
+
# #=> ["<span class=\"match\">Pat</span> Allan, <span class=\"match\">Pat</span> Cash"]
|
|
315
|
+
#
|
|
316
|
+
# lorem_lipsum = "Lorem ipsum dolor..."
|
|
317
|
+
#
|
|
318
|
+
# client.excerpts(:docs => ["Pat Allan, #{lorem_lipsum} Pat Cash"], :words => 'Pat', :index => 'pats')
|
|
319
|
+
# #=> ["<span class=\"match\">Pat</span> Allan, Lorem ipsum dolor sit amet, consectetur adipisicing
|
|
320
|
+
# elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua … . Excepteur
|
|
321
|
+
# sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
|
|
322
|
+
# laborum. <span class=\"match\">Pat</span> Cash"]
|
|
323
|
+
#
|
|
324
|
+
# Workflow:
|
|
325
|
+
#
|
|
326
|
+
# Excerpt creation is completely isolated from searching the index. The nominated index is only used to
|
|
327
|
+
# discover encoding and charset information.
|
|
328
|
+
#
|
|
329
|
+
# Therefore, the workflow goes:
|
|
330
|
+
#
|
|
331
|
+
# 1. Do the sphinx query.
|
|
332
|
+
# 2. Fetch the documents found by sphinx from their repositories.
|
|
333
|
+
# 3. Pass the documents' text to +excerpts+ for marking up of matched terms.
|
|
334
|
+
#
|
|
335
|
+
def excerpts(options = {})
|
|
336
|
+
options[:index] ||= '*'
|
|
337
|
+
options[:before_match] ||= '<span class="match">'
|
|
338
|
+
options[:after_match] ||= '</span>'
|
|
339
|
+
options[:chunk_separator] ||= ' … ' # ellipsis
|
|
340
|
+
options[:limit] ||= 256
|
|
341
|
+
options[:around] ||= 5
|
|
342
|
+
options[:exact_phrase] ||= false
|
|
343
|
+
options[:single_passage] ||= false
|
|
344
|
+
|
|
345
|
+
response = Response.new request(:excerpt, excerpts_message(options))
|
|
346
|
+
|
|
347
|
+
options[:docs].collect { response.next }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Update attributes - first parameter is the relevant index, second is an
|
|
351
|
+
# array of attributes to be updated, and the third is a hash, where the
|
|
352
|
+
# keys are the document ids, and the values are arrays with the attribute
|
|
353
|
+
# values - in the same order as the second parameter.
|
|
354
|
+
#
|
|
355
|
+
# Example:
|
|
356
|
+
#
|
|
357
|
+
# client.update('people', ['birthday'], {1 => [Time.at(1982, 20, 8).to_i]})
|
|
358
|
+
#
|
|
359
|
+
def update(index, attributes, values_by_doc)
|
|
360
|
+
response = Response.new request(
|
|
361
|
+
:update,
|
|
362
|
+
update_message(index, attributes, values_by_doc)
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
response.next_int
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Generates a keyword list for a given query. Each keyword is represented
|
|
369
|
+
# by a hash, with keys :tokenised and :normalised. If return_hits is set to
|
|
370
|
+
# true it will also report on the number of hits and documents for each
|
|
371
|
+
# keyword (see :hits and :docs keys respectively).
|
|
372
|
+
def keywords(query, index, return_hits = false)
|
|
373
|
+
response = Response.new request(
|
|
374
|
+
:keywords,
|
|
375
|
+
keywords_message(query, index, return_hits)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
(0...response.next_int).collect do
|
|
379
|
+
hash = {}
|
|
380
|
+
hash[:tokenised] = response.next
|
|
381
|
+
hash[:normalised] = response.next
|
|
382
|
+
|
|
383
|
+
if return_hits
|
|
384
|
+
hash[:docs] = response.next_int
|
|
385
|
+
hash[:hits] = response.next_int
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
hash
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
private
|
|
393
|
+
|
|
394
|
+
# Connects to the Sphinx daemon, and yields a socket to use. The socket is
|
|
395
|
+
# closed at the end of the block.
|
|
396
|
+
def connect(&block)
|
|
397
|
+
socket = nil
|
|
398
|
+
if @timeout == 0
|
|
399
|
+
socket = initialise_connection
|
|
400
|
+
else
|
|
401
|
+
begin
|
|
402
|
+
Timeout.timeout(@timeout) { socket = initialise_connection }
|
|
403
|
+
rescue Timeout::Error
|
|
404
|
+
raise Riddle::ConnectionError,
|
|
405
|
+
"Connection to #{@server} on #{@port} timed out after #{@timeout} seconds"
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
begin
|
|
410
|
+
yield socket
|
|
411
|
+
ensure
|
|
412
|
+
socket.close
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def initialise_connection
|
|
417
|
+
socket = TCPSocket.new @server, @port
|
|
418
|
+
|
|
419
|
+
# Checking version
|
|
420
|
+
version = socket.recv(4).unpack('N*').first
|
|
421
|
+
if version < 1
|
|
422
|
+
socket.close
|
|
423
|
+
raise VersionError, "Can only connect to searchd version 1.0 or better, not version #{version}"
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Send version
|
|
427
|
+
socket.send [1].pack('N'), 0
|
|
428
|
+
|
|
429
|
+
socket
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Send a collection of messages, for a command type (eg, search, excerpts,
|
|
433
|
+
# update), to the Sphinx daemon.
|
|
434
|
+
def request(command, messages)
|
|
435
|
+
response = ""
|
|
436
|
+
status = -1
|
|
437
|
+
version = 0
|
|
438
|
+
length = 0
|
|
439
|
+
message = Array(messages).join("")
|
|
440
|
+
if message.respond_to?(:force_encoding)
|
|
441
|
+
message = message.force_encoding('ASCII-8BIT')
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
connect do |socket|
|
|
445
|
+
case command
|
|
446
|
+
when :search
|
|
447
|
+
# Message length is +4 to account for the following count value for
|
|
448
|
+
# the number of messages (well, that's what I'm assuming).
|
|
449
|
+
socket.send [
|
|
450
|
+
Commands[command], Versions[command],
|
|
451
|
+
4+message.length, messages.length
|
|
452
|
+
].pack("nnNN") + message, 0
|
|
453
|
+
else
|
|
454
|
+
socket.send [
|
|
455
|
+
Commands[command], Versions[command], message.length
|
|
456
|
+
].pack("nnN") + message, 0
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
header = socket.recv(8)
|
|
460
|
+
status, version, length = header.unpack('n2N')
|
|
461
|
+
|
|
462
|
+
while response.length < (length || 0)
|
|
463
|
+
part = socket.recv(length - response.length)
|
|
464
|
+
response << part if part
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
if response.empty? || response.length != length
|
|
469
|
+
raise ResponseError, "No response from searchd (status: #{status}, version: #{version})"
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
case status
|
|
473
|
+
when Statuses[:ok]
|
|
474
|
+
if version < Versions[command]
|
|
475
|
+
puts format("searchd command v.%d.%d older than client (v.%d.%d)",
|
|
476
|
+
version >> 8, version & 0xff,
|
|
477
|
+
Versions[command] >> 8, Versions[command] & 0xff)
|
|
478
|
+
end
|
|
479
|
+
response
|
|
480
|
+
when Statuses[:warning]
|
|
481
|
+
length = response[0, 4].unpack('N*').first
|
|
482
|
+
puts response[4, length]
|
|
483
|
+
response[4 + length, response.length - 4 - length]
|
|
484
|
+
when Statuses[:error], Statuses[:retry]
|
|
485
|
+
raise ResponseError, "searchd error (status: #{status}): #{response[4, response.length - 4]}"
|
|
486
|
+
else
|
|
487
|
+
raise ResponseError, "Unknown searchd error (status: #{status})"
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Generation of the message to send to Sphinx for a search.
|
|
492
|
+
def query_message(search, index, comments = '')
|
|
493
|
+
message = Message.new
|
|
494
|
+
|
|
495
|
+
# Mode, Limits, Sort Mode
|
|
496
|
+
message.append_ints @offset, @limit, MatchModes[@match_mode],
|
|
497
|
+
RankModes[@rank_mode], SortModes[@sort_mode]
|
|
498
|
+
message.append_string @sort_by
|
|
499
|
+
|
|
500
|
+
# Query
|
|
501
|
+
message.append_string search
|
|
502
|
+
|
|
503
|
+
# Weights
|
|
504
|
+
message.append_int @weights.length
|
|
505
|
+
message.append_ints *@weights
|
|
506
|
+
|
|
507
|
+
# Index
|
|
508
|
+
message.append_string index
|
|
509
|
+
|
|
510
|
+
# ID Range
|
|
511
|
+
message.append_int 1
|
|
512
|
+
message.append_64bit_ints @id_range.first, @id_range.last
|
|
513
|
+
|
|
514
|
+
# Filters
|
|
515
|
+
message.append_int @filters.length
|
|
516
|
+
@filters.each { |filter| message.append filter.query_message }
|
|
517
|
+
|
|
518
|
+
# Grouping
|
|
519
|
+
message.append_int GroupFunctions[@group_function]
|
|
520
|
+
message.append_string @group_by
|
|
521
|
+
message.append_int @max_matches
|
|
522
|
+
message.append_string @group_clause
|
|
523
|
+
message.append_ints @cut_off, @retry_count, @retry_delay
|
|
524
|
+
message.append_string @group_distinct
|
|
525
|
+
|
|
526
|
+
# Anchor Point
|
|
527
|
+
if @anchor.empty?
|
|
528
|
+
message.append_int 0
|
|
529
|
+
else
|
|
530
|
+
message.append_int 1
|
|
531
|
+
message.append_string @anchor[:latitude_attribute]
|
|
532
|
+
message.append_string @anchor[:longitude_attribute]
|
|
533
|
+
message.append_floats @anchor[:latitude], @anchor[:longitude]
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Per Index Weights
|
|
537
|
+
message.append_int @index_weights.length
|
|
538
|
+
@index_weights.each do |key,val|
|
|
539
|
+
message.append_string key.to_s
|
|
540
|
+
message.append_int val
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Max Query Time
|
|
544
|
+
message.append_int @max_query_time
|
|
545
|
+
|
|
546
|
+
# Per Field Weights
|
|
547
|
+
message.append_int @field_weights.length
|
|
548
|
+
@field_weights.each do |key,val|
|
|
549
|
+
message.append_string key.to_s
|
|
550
|
+
message.append_int val
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
message.append_string comments
|
|
554
|
+
|
|
555
|
+
message.to_s
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Generation of the message to send to Sphinx for an excerpts request.
|
|
559
|
+
def excerpts_message(options)
|
|
560
|
+
message = Message.new
|
|
561
|
+
|
|
562
|
+
flags = 1
|
|
563
|
+
flags |= 2 if options[:exact_phrase]
|
|
564
|
+
flags |= 4 if options[:single_passage]
|
|
565
|
+
flags |= 8 if options[:use_boundaries]
|
|
566
|
+
flags |= 16 if options[:weight_order]
|
|
567
|
+
|
|
568
|
+
message.append [0, flags].pack('N2') # 0 = mode
|
|
569
|
+
message.append_string options[:index]
|
|
570
|
+
message.append_string options[:words]
|
|
571
|
+
|
|
572
|
+
# options
|
|
573
|
+
message.append_string options[:before_match]
|
|
574
|
+
message.append_string options[:after_match]
|
|
575
|
+
message.append_string options[:chunk_separator]
|
|
576
|
+
message.append_ints options[:limit], options[:around]
|
|
577
|
+
|
|
578
|
+
message.append_array options[:docs]
|
|
579
|
+
|
|
580
|
+
message.to_s
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Generation of the message to send to Sphinx to update attributes of a
|
|
584
|
+
# document.
|
|
585
|
+
def update_message(index, attributes, values_by_doc)
|
|
586
|
+
message = Message.new
|
|
587
|
+
|
|
588
|
+
message.append_string index
|
|
589
|
+
message.append_array attributes
|
|
590
|
+
|
|
591
|
+
message.append_int values_by_doc.length
|
|
592
|
+
values_by_doc.each do |key,values|
|
|
593
|
+
message.append_64bit_int key # document ID
|
|
594
|
+
message.append_ints *values # array of new values (integers)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
message.to_s
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Generates the simple message to send to the daemon for a keywords request.
|
|
601
|
+
def keywords_message(query, index, return_hits)
|
|
602
|
+
message = Message.new
|
|
603
|
+
|
|
604
|
+
message.append_string query
|
|
605
|
+
message.append_string index
|
|
606
|
+
message.append_int return_hits ? 1 : 0
|
|
607
|
+
|
|
608
|
+
message.to_s
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def attribute_from_type(type, response)
|
|
612
|
+
type -= AttributeTypes[:multi] if is_multi = type > AttributeTypes[:multi]
|
|
613
|
+
|
|
614
|
+
case type
|
|
615
|
+
when AttributeTypes[:float]
|
|
616
|
+
is_multi ? response.next_float_array : response.next_float
|
|
617
|
+
else
|
|
618
|
+
is_multi ? response.next_int_array : response.next_int
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
end
|