flying-sphinx 0.2.6 → 0.3.0

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.6
1
+ 0.3.0
data/lib/flying_sphinx.rb CHANGED
@@ -8,6 +8,8 @@ require 'riddle/0.9.9'
8
8
 
9
9
  require 'flying_sphinx/api'
10
10
  require 'flying_sphinx/configuration'
11
+ require 'flying_sphinx/delayed_delta'
12
+ require 'flying_sphinx/flag_as_deleted_job'
11
13
  require 'flying_sphinx/heroku_shared_adapter'
12
14
  require 'flying_sphinx/index_request'
13
15
  require 'flying_sphinx/tunnel'
@@ -0,0 +1,85 @@
1
+ class FlyingSphinx::DelayedDelta < ThinkingSphinx::Deltas::DefaultDelta
2
+ # Adds a job to the queue, if it doesn't already exist. This is to ensure
3
+ # multiple indexing requests for the same delta index don't get added, as the
4
+ # index only needs to be processed once.
5
+ #
6
+ # Because indexing jobs are all the same object, they all get serialised to
7
+ # the same YAML value.
8
+ #
9
+ # @param [Object] object The job, which must respond to the #perform method.
10
+ # @param [Integer] priority (0)
11
+ #
12
+ def self.enqueue(object, priority = 0)
13
+ return if duplicates_exist? object
14
+
15
+ ::Delayed::Job.enqueue(object, :priority => priority)
16
+ end
17
+
18
+ # Checks whether a given job already exists in the queue.
19
+ #
20
+ # @param [Object] object The job
21
+ # @return [Boolean] True if a duplicate of the job already exists in the queue
22
+ #
23
+ def self.duplicates_exist?(object)
24
+ ::Delayed::Job.count(
25
+ :conditions => {
26
+ :handler => object.to_yaml,
27
+ :locked_at => nil
28
+ }
29
+ ) > 0
30
+ end
31
+
32
+ # Adds a job to the queue for processing the given model's delta index. A job
33
+ # for hiding the instance in the core index is also created, if an instance is
34
+ # provided.
35
+ #
36
+ # Neither job will be queued if updates or deltas are disabled, or if the
37
+ # instance (when given) is not toggled to be in the delta index. The first two
38
+ # options are controlled via ThinkingSphinx.updates_enabled? and
39
+ # ThinkingSphinx.deltas_enabled?.
40
+ #
41
+ # @param [Class] model the ActiveRecord model to index.
42
+ # @param [ActiveRecord::Base] instance the instance of the given model that
43
+ # has changed. Optional.
44
+ # @return [Boolean] true
45
+ #
46
+ def index(model, instance = nil)
47
+ return true if skip? instance
48
+
49
+ self.class.enqueue(
50
+ FlyingSphinx::IndexRequest.new(model.delta_index_names),
51
+ delayed_job_priority
52
+ )
53
+
54
+ Delayed::Job.enqueue(
55
+ FlyingSphinx::FlagAsDeletedJob.new(
56
+ model.core_index_names, instance.sphinx_document_id
57
+ ),
58
+ :priority => delayed_job_priority
59
+ ) if instance
60
+
61
+ true
62
+ end
63
+
64
+ private
65
+
66
+ def config
67
+ @config ||= ThinkingSphinx::Configuration.instance
68
+ end
69
+
70
+ def delayed_job_priority
71
+ config.delayed_job_priority
72
+ end
73
+
74
+ # Checks whether jobs should be enqueued. Only true if updates and deltas are
75
+ # enabled, and the instance (if there is one) is toggled.
76
+ #
77
+ # @param [ActiveRecord::Base, NilClass] instance
78
+ # @return [Boolean]
79
+ #
80
+ def skip?(instance)
81
+ !ThinkingSphinx.updates_enabled? ||
82
+ !ThinkingSphinx.deltas_enabled? ||
83
+ (instance && !toggled(instance))
84
+ end
85
+ end
@@ -0,0 +1,45 @@
1
+ # A simple job for flagging a specified Sphinx document in a given index as
2
+ # 'deleted'.
3
+ #
4
+ class FlyingSphinx::FlagAsDeletedJob
5
+ attr_accessor :indices, :document_id
6
+
7
+ # Initialises the object with an index name and document id. Please note that
8
+ # the document id is Sphinx's unique identifier, and will almost certainly not
9
+ # be the model instance's primary key value.
10
+ #
11
+ # @param [String] index The index name
12
+ # @param [Integer] document_id The document id
13
+ #
14
+ def initialize(indices, document_id)
15
+ @indices, @document_id = indices, document_id
16
+ end
17
+
18
+ # Updates the sphinx_deleted attribute for the given document, setting the
19
+ # value to 1 (true). This is not a special attribute in Sphinx, but is used
20
+ # by Thinking Sphinx to ignore deleted values between full re-indexing. It's
21
+ # particularly useful in this situation to avoid old values in the core index
22
+ # and just use the new values in the delta index as a reference point.
23
+ #
24
+ # @return [Boolean] true
25
+ #
26
+ def perform
27
+ indices.each do |index|
28
+ client.update(
29
+ index,
30
+ ['sphinx_deleted'],
31
+ {@document_id => [1]}
32
+ ) if ThinkingSphinx.search_for_id(@document_id, index)
33
+ end
34
+ rescue Riddle::ConnectionError
35
+ # If it fails here, so be it.
36
+ ensure
37
+ true
38
+ end
39
+
40
+ private
41
+
42
+ def client
43
+ @client ||= ThinkingSphinx::Configuration.instance.client
44
+ end
45
+ end
@@ -1,26 +1,60 @@
1
1
  class FlyingSphinx::IndexRequest
2
- attr_reader :configuration, :index_id
2
+ attr_reader :index_id, :indices
3
3
 
4
- def initialize(configuration)
5
- @configuration = configuration
4
+ # Remove all Delta jobs from the queue. If the
5
+ # delayed_jobs table does not exist, this method will do nothing.
6
+ #
7
+ def self.cancel_jobs
8
+ return unless ::Delayed::Job.table_exists?
6
9
 
10
+ ::Delayed::Job.delete_all "handler LIKE '--- !ruby/object:FlyingSphinx::%'"
11
+ end
12
+
13
+ def initialize(indices = [])
14
+ @indices = indices
15
+ end
16
+
17
+ # Shows index name in Delayed::Job#name.
18
+ #
19
+ def display_name
20
+ "#{self.class.name} for #{indices.join(', ')}"
21
+ end
22
+
23
+ def update_and_index
7
24
  update_sphinx_configuration
8
-
9
- FlyingSphinx::Tunnel.connect(configuration) do
10
- begin_request unless request_begun?
11
-
12
- !request_complete?
13
- end
25
+ index
26
+ end
27
+
28
+ # Runs Sphinx's indexer tool to process the index. Currently assumes Sphinx is
29
+ # running.
30
+ #
31
+ # @return [Boolean] true
32
+ #
33
+ def perform
34
+ index
35
+ true
14
36
  end
15
37
 
16
38
  private
17
39
 
40
+ def configuration
41
+ @configuration ||= FlyingSphinx::Configuration.new
42
+ end
43
+
18
44
  def update_sphinx_configuration
19
45
  api.put '/app', :configuration => configuration.sphinx_configuration
20
46
  end
21
47
 
48
+ def index
49
+ FlyingSphinx::Tunnel.connect(configuration) do
50
+ begin_request unless request_begun?
51
+
52
+ !request_complete?
53
+ end
54
+ end
55
+
22
56
  def begin_request
23
- @index_id = api.post('/app/indices')
57
+ @index_id = api.post('/app/indices', :indices => indices.join(','))
24
58
  @request_begun = true
25
59
  end
26
60
 
@@ -1,7 +1,8 @@
1
1
  namespace :fs do
2
2
  task :index => :environment do
3
3
  puts "Starting Index Request"
4
- FlyingSphinx::IndexRequest.new FlyingSphinx::Configuration.new
4
+ FlyingSphinx::IndexRequest.cancel_jobs
5
+ FlyingSphinx::IndexRequest.new.update_and_index
5
6
  puts "Index Request has completed"
6
7
  end
7
8
 
@@ -0,0 +1,146 @@
1
+ require 'spec_helper'
2
+
3
+ describe FlyingSphinx::DelayedDelta do
4
+ describe '.enqueue' do
5
+ before :each do
6
+ Delayed::Job.stub!(:count => 0)
7
+ end
8
+
9
+ it "should enqueue if there's no existing jobs for the same index" do
10
+ Delayed::Job.should_receive(:enqueue)
11
+
12
+ FlyingSphinx::DelayedDelta.enqueue(stub('object'))
13
+ end
14
+
15
+ it "should not enqueue the job if there's an existing job already" do
16
+ Delayed::Job.stub!(:count => 1)
17
+ Delayed::Job.should_not_receive(:enqueue)
18
+
19
+ FlyingSphinx::DelayedDelta.enqueue(stub('object'))
20
+ end
21
+ end
22
+
23
+ describe '#index' do
24
+ let(:config) { ThinkingSphinx::Configuration.instance }
25
+ let(:delayed_delta) { FlyingSphinx::DelayedDelta.new stub('instance'), {} }
26
+ let(:model) {
27
+ stub 'foo',
28
+ :name => 'foo',
29
+ :core_index_names => ['foo_core'],
30
+ :delta_index_names => ['foo_delta']
31
+ }
32
+ let(:instance) { stub('instance', :sphinx_document_id => 42) }
33
+
34
+ before :each do
35
+ ThinkingSphinx.updates_enabled = true
36
+ ThinkingSphinx.deltas_enabled = true
37
+
38
+ config.delayed_job_priority = 2
39
+
40
+ FlyingSphinx::DelayedDelta.stub!(:enqueue => true)
41
+ Delayed::Job.stub!(:enqueue => true, :inspect => 'Delayed::Job')
42
+
43
+ delayed_delta.stub!(:toggled => true)
44
+ end
45
+
46
+ context 'updates disabled' do
47
+ before :each do
48
+ ThinkingSphinx.updates_enabled = false
49
+ end
50
+
51
+ it "should not enqueue a delta job" do
52
+ FlyingSphinx::DelayedDelta.should_not_receive(:enqueue)
53
+
54
+ delayed_delta.index model
55
+ end
56
+
57
+ it "should not enqueue a flag as deleted job" do
58
+ Delayed::Job.should_not_receive(:enqueue)
59
+
60
+ delayed_delta.index model
61
+ end
62
+ end
63
+
64
+ context 'deltas disabled' do
65
+ before :each do
66
+ ThinkingSphinx.deltas_enabled = false
67
+ end
68
+
69
+ it "should not enqueue a delta job" do
70
+ FlyingSphinx::DelayedDelta.should_not_receive(:enqueue)
71
+
72
+ delayed_delta.index model
73
+ end
74
+
75
+ it "should not enqueue a flag as deleted job" do
76
+ Delayed::Job.should_not_receive(:enqueue)
77
+
78
+ delayed_delta.index model
79
+ end
80
+ end
81
+
82
+ context "instance isn't toggled" do
83
+ before :each do
84
+ delayed_delta.stub!(:toggled => false)
85
+ end
86
+
87
+ it "should not enqueue a delta job" do
88
+ FlyingSphinx::DelayedDelta.should_not_receive(:enqueue)
89
+
90
+ delayed_delta.index model, instance
91
+ end
92
+
93
+ it "should not enqueue a flag as deleted job" do
94
+ Delayed::Job.should_not_receive(:enqueue)
95
+
96
+ delayed_delta.index model, instance
97
+ end
98
+ end
99
+
100
+ it "should enqueue a delta job for the appropriate indexes" do
101
+ FlyingSphinx::DelayedDelta.should_receive(:enqueue) do |job, priority|
102
+ job.indices.should == ['foo_delta']
103
+ end
104
+
105
+ delayed_delta.index model
106
+ end
107
+
108
+ it "should use the defined priority for the delta job" do
109
+ FlyingSphinx::DelayedDelta.should_receive(:enqueue) do |job, priority|
110
+ priority.should == 2
111
+ end
112
+
113
+ delayed_delta.index model
114
+ end
115
+
116
+ it "should enqueue a flag-as-deleted job for the appropriate indexes" do
117
+ Delayed::Job.should_receive(:enqueue) do |job, options|
118
+ job.indices.should == ['foo_core']
119
+ end
120
+
121
+ delayed_delta.index model, instance
122
+ end
123
+
124
+ it "should enqueue a flag-as-deleted job for the appropriate id" do
125
+ Delayed::Job.should_receive(:enqueue) do |job, options|
126
+ job.document_id.should == 42
127
+ end
128
+
129
+ delayed_delta.index model, instance
130
+ end
131
+
132
+ it "should use the defined priority for the flag-as-deleted job" do
133
+ Delayed::Job.should_receive(:enqueue) do |job, options|
134
+ options[:priority].should == 2
135
+ end
136
+
137
+ delayed_delta.index model, instance
138
+ end
139
+
140
+ it "should not enqueue a flag-as-deleted job if no instance is provided" do
141
+ Delayed::Job.should_not_receive(:enqueue)
142
+
143
+ delayed_delta.index model
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe FlyingSphinx::FlagAsDeletedJob do
4
+ describe '#perform' do
5
+ let(:config) { ThinkingSphinx::Configuration.instance }
6
+ let(:client) { stub('client', :update => true) }
7
+ let(:job) { FlyingSphinx::FlagAsDeletedJob.new(['foo_core'], 12) }
8
+
9
+ before :each do
10
+ config.stub!(:client => client)
11
+ ThinkingSphinx.stub!(:search_for_id => true)
12
+ end
13
+
14
+ it "should not update if the document isn't in the index" do
15
+ ThinkingSphinx.stub!(:search_for_id => false)
16
+ client.should_not_receive(:update)
17
+
18
+ job.perform
19
+ end
20
+
21
+ it "should update the specified index" do
22
+ client.should_receive(:update) do |index, attributes, values|
23
+ index.should == 'foo_core'
24
+ end
25
+
26
+ job.perform
27
+ end
28
+
29
+ it "should update all specified indexes" do
30
+ job.indices = ['foo_core', 'bar_core']
31
+ client.should_receive(:update).with('foo_core', anything, anything)
32
+ client.should_receive(:update).with('bar_core', anything, anything)
33
+
34
+ job.perform
35
+ end
36
+
37
+ it "should update the sphinx_deleted attribute" do
38
+ client.should_receive(:update) do |index, attributes, values|
39
+ attributes.should == ['sphinx_deleted']
40
+ end
41
+
42
+ job.perform
43
+ end
44
+
45
+ it "should set sphinx_deleted for the given document to true" do
46
+ client.should_receive(:update) do |index, attributes, values|
47
+ values[12].should == [1]
48
+ end
49
+
50
+ job.perform
51
+ end
52
+
53
+ it "should check for the existence of the document in the specified index" do
54
+ ThinkingSphinx.should_receive(:search_for_id) do |id, index|
55
+ index.should == 'foo_core'
56
+ end
57
+
58
+ job.perform
59
+ end
60
+
61
+ it "should check for the existence of the given document id" do
62
+ ThinkingSphinx.should_receive(:search_for_id) do |id, index|
63
+ id.should == 12
64
+ end
65
+
66
+ job.perform
67
+ end
68
+ end
69
+ end
@@ -2,28 +2,106 @@ require 'spec_helper'
2
2
 
3
3
  describe FlyingSphinx::IndexRequest do
4
4
  let(:api) { FlyingSphinx::API.new 'foo', 'bar' }
5
- let(:configuration) { stub(:configuration, :api => api) }
5
+ let(:configuration) {
6
+ stub(:configuration, :api => api, :sphinx_configuration => 'foo {}')
7
+ }
6
8
 
7
9
  before :each do
10
+ FlyingSphinx::Configuration.stub!(:new => configuration)
8
11
  FlyingSphinx::Tunnel.stub(:connect) { |config, block| block.call }
9
12
  end
10
13
 
11
- describe '#initialize' do
14
+ describe '.cancel_jobs' do
15
+ before :each do
16
+ Delayed::Job.stub!(:delete_all => true)
17
+ end
18
+
19
+ it "should not delete any rows if the delayed_jobs table does not exist" do
20
+ Delayed::Job.stub!(:table_exists? => false)
21
+ Delayed::Job.should_not_receive(:delete_all)
22
+
23
+ FlyingSphinx::IndexRequest.cancel_jobs
24
+ end
25
+
26
+ it "should delete rows if the delayed_jobs table does exist" do
27
+ Delayed::Job.stub!(:table_exists? => true)
28
+ Delayed::Job.should_receive(:delete_all)
29
+
30
+ FlyingSphinx::IndexRequest.cancel_jobs
31
+ end
32
+
33
+ it "should delete only Thinking Sphinx jobs" do
34
+ Delayed::Job.stub!(:table_exists? => true)
35
+ Delayed::Job.should_receive(:delete_all) do |sql|
36
+ sql.should match(/handler LIKE '--- !ruby\/object:FlyingSphinx::\%'/)
37
+ end
38
+
39
+ FlyingSphinx::IndexRequest.cancel_jobs
40
+ end
41
+ end
42
+
43
+ describe '#update_and_index' do
44
+ let(:index_request) { FlyingSphinx::IndexRequest.new }
45
+ let(:conf_params) { {:configuration => 'foo {}'} }
46
+ let(:index_params) { {:indices => ''} }
47
+
12
48
  it "makes a new request" do
13
- api.should_receive(:post).with('/app/indices').and_return(42)
49
+ api.should_receive(:put).with('/app', conf_params).and_return('ok')
50
+ api.should_receive(:post).
51
+ with('/app/indices', index_params).and_return(42)
14
52
  api.should_receive(:get).with('/app/indices/42').and_return('PENDING')
15
53
 
16
54
  begin
17
- Timeout::timeout(0.2) { FlyingSphinx::IndexRequest.new(configuration) }
55
+ Timeout::timeout(0.2) {
56
+ index_request.update_and_index
57
+ }
18
58
  rescue Timeout::Error
19
59
  end
20
60
  end
21
61
 
22
62
  it "should finish when the index request has been completed" do
23
- api.should_receive(:post).with('/app/indices').and_return(42)
63
+ api.should_receive(:put).with('/app', conf_params).and_return('ok')
64
+ api.should_receive(:post).
65
+ with('/app/indices', index_params).and_return(42)
24
66
  api.should_receive(:get).with('/app/indices/42').and_return('FINISHED')
25
67
 
26
- FlyingSphinx::IndexRequest.new(configuration)
68
+ index_request.update_and_index
69
+ end
70
+ end
71
+
72
+ describe '#perform' do
73
+ let(:index_request) { FlyingSphinx::IndexRequest.new ['foo_delta'] }
74
+ let(:index_params) { {:indices => 'foo_delta'} }
75
+
76
+ it "makes a new request" do
77
+ api.should_receive(:post).
78
+ with('/app/indices', index_params).and_return(42)
79
+ api.should_receive(:get).with('/app/indices/42').and_return('PENDING')
80
+
81
+ begin
82
+ Timeout::timeout(0.2) {
83
+ index_request.perform
84
+ }
85
+ rescue Timeout::Error
86
+ end
87
+ end
88
+
89
+ it "should finish when the index request has been completed" do
90
+ api.should_receive(:post).
91
+ with('/app/indices', index_params).and_return(42)
92
+ api.should_receive(:get).with('/app/indices/42').and_return('FINISHED')
93
+
94
+ index_request.perform
95
+ end
96
+ end
97
+
98
+ describe "#display_name" do
99
+ let(:index_request) {
100
+ FlyingSphinx::IndexRequest.new ['foo_core', 'bar_core']
101
+ }
102
+
103
+ it "should display class name with all indexes" do
104
+ index_request.display_name.should == "FlyingSphinx::IndexRequest for foo_core, bar_core"
27
105
  end
28
106
  end
29
107
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flying-sphinx
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 6
10
- version: 0.2.6
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Pat Allan
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-01-01 00:00:00 +11:00
18
+ date: 2011-01-02 00:00:00 +11:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -160,6 +160,22 @@ dependencies:
160
160
  - 2
161
161
  version: 1.2.2
162
162
  requirement: *id009
163
+ - !ruby/object:Gem::Dependency
164
+ type: :development
165
+ prerelease: false
166
+ name: delayed_job
167
+ version_requirements: &id010 !ruby/object:Gem::Requirement
168
+ none: false
169
+ requirements:
170
+ - - "="
171
+ - !ruby/object:Gem::Version
172
+ hash: 15
173
+ segments:
174
+ - 2
175
+ - 1
176
+ - 2
177
+ version: 2.1.2
178
+ requirement: *id010
163
179
  description: Hooks Thinking Sphinx into the Flying Sphinx service
164
180
  email: pat@freelancing-gods.com
165
181
  executables: []
@@ -177,6 +193,8 @@ files:
177
193
  - lib/flying_sphinx.rb
178
194
  - lib/flying_sphinx/api.rb
179
195
  - lib/flying_sphinx/configuration.rb
196
+ - lib/flying_sphinx/delayed_delta.rb
197
+ - lib/flying_sphinx/flag_as_deleted_job.rb
180
198
  - lib/flying_sphinx/heroku_shared_adapter.rb
181
199
  - lib/flying_sphinx/index_request.rb
182
200
  - lib/flying_sphinx/rails.rb
@@ -184,6 +202,8 @@ files:
184
202
  - lib/flying_sphinx/tasks.rb
185
203
  - lib/flying_sphinx/tunnel.rb
186
204
  - spec/flying_sphinx/configuration_spec.rb
205
+ - spec/flying_sphinx/delayed_delta_spec.rb
206
+ - spec/flying_sphinx/flag_as_deleted_job_spec.rb
187
207
  - spec/flying_sphinx/index_request_spec.rb
188
208
  has_rdoc: true
189
209
  homepage: http://flying-sphinx.com
@@ -221,4 +241,6 @@ specification_version: 3
221
241
  summary: Sphinx in the Cloud
222
242
  test_files:
223
243
  - spec/flying_sphinx/configuration_spec.rb
244
+ - spec/flying_sphinx/delayed_delta_spec.rb
245
+ - spec/flying_sphinx/flag_as_deleted_job_spec.rb
224
246
  - spec/flying_sphinx/index_request_spec.rb