cloudsearchable 0.0.1

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.
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+ require 'test_classes/cloud_searchable_test_class'
3
+
4
+ describe Cloudsearchable do
5
+ let(:clazz){ CloudSearchableSampleClassFactory.call }
6
+
7
+ it 'can describe an index that returns ids for the class type' do
8
+ test_index = clazz.cloudsearch_index(:test_index)
9
+ test_index.should be_kind_of(Cloudsearchable::Domain)
10
+ test_index.fields.should have(4).items #3 explicit + 1 for the id of the object
11
+ end
12
+
13
+ it 'has a default index' do
14
+ clazz.cloudsearch_index.should be_kind_of(Cloudsearchable::Domain)
15
+ clazz.cloudsearch_index(:test_index).should_not be(clazz.cloudsearch_index)
16
+ end
17
+
18
+ it 'names domains consistent with CloudSearch limitations' do
19
+ clazz.cloudsearch_index(:test_index).name.should =~ /^[a-z][a-z0-9\-]+$/
20
+ end
21
+
22
+ describe 'an ordinary object' do
23
+ #An instance of the searchable class
24
+ let(:inst) do
25
+ inst = clazz.new
26
+ #arbitrary but plausible values for the core fields
27
+ inst.destroyed = false
28
+ inst.id = 42
29
+ inst.lock_version = 18
30
+ inst.customer = OpenStruct.new :id => 123
31
+ inst.name = "My Name"
32
+ inst
33
+ end
34
+
35
+ it 'supplies the right values to the fields' do
36
+ test_index = clazz.cloudsearch_index(:test_index)
37
+ test_index.fields[:test_class_id].value_for(inst).should be(inst.id)
38
+ test_index.fields[:customer_id].value_for(inst).should be(inst.customer)
39
+ test_index.fields[:test_name].value_for(inst).should be(inst.name)
40
+ test_index.fields[:helpfulness].value_for(inst).should be(1234)
41
+ end
42
+
43
+ it 'reindexes when told to' do
44
+ clazz.cloudsearch_index( ).should_receive(:post_record).with(inst, inst.id, inst.lock_version)
45
+ clazz.cloudsearch_index(:test_index).should_receive(:post_record).with(inst, inst.id, inst.lock_version)
46
+ inst.update_indexes
47
+ end
48
+
49
+ it 'generates a sensible addition sdf document' do
50
+ sdf = clazz.cloudsearch_index(:test_index).send :addition_sdf, inst, inst.id, inst.lock_version
51
+ sdf[:fields][:helpfulness].should be(1234)
52
+ end
53
+ end
54
+
55
+ describe 'a destroyed object' do
56
+ #An instance of the searchable class
57
+ let(:inst) do
58
+ inst = clazz.new
59
+ #arbitrary but plausible values for the core fields
60
+ inst.destroyed = true
61
+ inst
62
+ end
63
+
64
+ it 'reindexes when told to' do
65
+ clazz.cloudsearch_index( ).should_receive(:delete_record).with(inst.id, inst.lock_version)
66
+ clazz.cloudsearch_index(:test_index).should_receive(:delete_record).with(inst.id, inst.lock_version)
67
+ inst.update_indexes
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+ require 'test_classes/cloud_searchable_test_class'
3
+
4
+ describe Cloudsearchable::Domain do
5
+ before(:each) do
6
+ fake_client = double('client')
7
+ CloudSearch.stub(:client).and_return(fake_client)
8
+ end
9
+
10
+ #
11
+ # First call to describe_domains returns it needs reindexing,
12
+ # Second call returns that it is processing,
13
+ # Third call returns that it is done processing
14
+ #
15
+ let(:needs_rebuild_domain) do
16
+ described_class.new('nrb-index').tap do |dom|
17
+ resp = describe_domain_response(dom.name)
18
+ CloudSearch.client.stub(:describe_domains).with(:domain_names => [dom.name]).
19
+ and_return(
20
+ describe_domain_response(dom.name, :required_index_documents => true),
21
+ describe_domain_response(dom.name, :processing => true),
22
+ describe_domain_response(dom.name)
23
+ )
24
+ end
25
+ end
26
+
27
+ # A domain name named 'my-index'
28
+ let(:domain) do
29
+ described_class.new('my-index').tap do |dom|
30
+ CloudSearch.client.stub(:describe_domains).and_return(describe_domain_response(dom.name))
31
+ CloudSearch.client.stub(:describe_domains).with(:domain_names => [dom.name]).and_return(describe_domain_response(dom.name))
32
+ end
33
+ end
34
+
35
+ let(:empty_domain) do
36
+ described_class.new('my-index').tap do |dom|
37
+ CloudSearch.client.stub(:describe_domains).and_return({})
38
+ end
39
+ end
40
+
41
+ it 'can be instantiated' do
42
+ index = domain
43
+ index.name.should end_with('my-index')
44
+ end
45
+
46
+ it 'can haz a literal field' do
47
+ index = domain
48
+ index.add_field(:literary, :literal) { nil }
49
+ index.fields[:literary].type.should eq(:literal)
50
+ end
51
+
52
+ it 'can be initialized with a nested class' do
53
+ class OuterClassForCloudSearch
54
+ class InnerClass
55
+ include Cloudsearchable
56
+ end
57
+ end
58
+
59
+ OuterClassForCloudSearch::InnerClass.cloudsearch_prefix.should match(/^[A-Za-z0-9_-]+$/)
60
+ Object.instance_eval { remove_const :OuterClassForCloudSearch }
61
+ end
62
+
63
+ context "SDF documents" do
64
+ let(:object) { OpenStruct.new(:field_with_nil_value => nil, :field_with_present_value => 42) }
65
+ subject do
66
+ described_class.new('my-index').tap do |d|
67
+ d.add_field(:field_with_nil_value, :literal)
68
+ d.add_field(:field_with_present_value, :literal)
69
+ end
70
+ end
71
+
72
+ it "should generate present fields" do
73
+ subject.addition_sdf(object, "id", 1)[:fields][:field_with_present_value].should == 42
74
+ end
75
+
76
+ it "should not generate nil fields" do
77
+ subject.addition_sdf(object, "id", 1)[:fields][:field_with_nil_value].should be_nil
78
+ end
79
+ end
80
+
81
+ it 'raises if the domain cannot be found' do
82
+ expect { empty_domain.send(:search_endpoint) }.to raise_error( Cloudsearchable::Domain::DomainNotFound,
83
+ /Cloudsearchable could not find the domain/)
84
+ end
85
+
86
+ #
87
+ # A test for the accidental clashing of domain caching
88
+ # on a class variable
89
+ #
90
+ it 'caches endpoints for multiple domains' do
91
+ domain.send(:search_endpoint).should_not eq(needs_rebuild_domain.send(:search_endpoint))
92
+ end
93
+
94
+ it 'endpoint selected is based on the domain name' do
95
+ domain.send(:search_endpoint).should eq describe_domain_response(domain.name)[:domain_status_list][0][:search_service][:endpoint]
96
+ domain.send(:doc_endpoint).should eq describe_domain_response(domain.name)[:domain_status_list][0][:doc_service][:endpoint]
97
+ end
98
+
99
+ it 'sleeps, waiting for reindexing' do
100
+ CloudSearch.client.should_receive(:index_documents).with(:domain_name => needs_rebuild_domain.name)
101
+ CloudSearch.client.should_receive(:describe_domains).exactly(3).times
102
+ needs_rebuild_domain.apply_changes(3).should == true
103
+ end
104
+
105
+ protected
106
+
107
+ #
108
+ # A mockup of the response to a describe_domain request
109
+ #
110
+ def describe_domain_response(domain_name, options = {})
111
+ {
112
+ :domain_status_list=> [
113
+ {
114
+ :search_partition_count=>1,
115
+ :search_service=>{
116
+ :arn=>"arn:aws:cs:us-east-1:510523556749:search/#{domain_name}",
117
+ :endpoint=>"search-#{domain_name}-7bq6utq4fdrwax5r6irje7xlra.us-east-1.cloudsearch.amazonaws.com"
118
+ },
119
+ :num_searchable_docs=>23,
120
+ :search_instance_type=>"search.m1.small",
121
+ :created=>true,
122
+ :domain_id=>"510523556749/#{domain_name}",
123
+ :processing=> options.fetch(:processing, false),
124
+ :search_instance_count=>1,
125
+ :domain_name=>"#{domain_name}",
126
+ :requires_index_documents=> options.fetch(:required_index_documents,false),
127
+ :deleted=>false,
128
+ :doc_service=>{
129
+ :arn=>"arn:aws:cs:us-east-1:510523556749:doc/#{domain_name}",
130
+ :endpoint=>"doc-#{domain_name}-7bq6utq4fdrwax5r6irje7xlra.us-east-1.cloudsearch.amazonaws.com"
131
+ }
132
+ },
133
+ {:search_partition_count=>1,
134
+ :search_service=>
135
+ {:arn=>
136
+ "arn:aws:cs:us-east-1:510523556749:search/dev-llarue-collection-items",
137
+ :endpoint=>
138
+ "search-dev-llarue-collection-items-hjopg2yzhcjdd4qxeglr2v5v7m.us-east-1.cloudsearch.amazonaws.com"},
139
+ :num_searchable_docs=>2,
140
+ :search_instance_type=>"search.m1.small",
141
+ :created=>true,
142
+ :domain_id=>"510523556749/dev-llarue-collection-items",
143
+ :processing=>false,
144
+ :search_instance_count=>1,
145
+ :domain_name=>"dev-llarue-collection-items",
146
+ :requires_index_documents=>false,
147
+ :deleted=>false,
148
+ :doc_service=>
149
+ {:arn=>"arn:aws:cs:us-east-1:510523556749:doc/dev-llarue-collection-items",
150
+ :endpoint=>
151
+ "doc-dev-llarue-collection-items-hjopg2yzhcjdd4qxeglr2v5v7m.us-east-1.cloudsearch.amazonaws.com"}}
152
+ ],
153
+ :response_metadata=>{
154
+ :request_id=>"7d9487a7-1c9f-11e2-9f96-0958b8a97a74"
155
+ }
156
+ }
157
+ end
158
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe Cloudsearchable::Field do
5
+ it 'has a name' do
6
+ field = described_class.new 'fnord', :literal
7
+ field.name.should eq(:fnord)
8
+ end
9
+
10
+ it 'can find its value' do
11
+ test_value = nil
12
+ field = described_class.new('foo', :literal, :source => Proc.new { test_value })
13
+ test_value = 123
14
+ field.value_for(Object.new).should eq(test_value)
15
+
16
+ record = OpenStruct.new :a => test_value
17
+ field2 = described_class.new('bar', :literal, :source => :a)
18
+ field.value_for(record).should eq(test_value)
19
+ end
20
+
21
+ it 'generates a field definition' do
22
+ domain_name = 'narnia'
23
+ field = described_class.new('fnord', :literal, :search_enabled => true)
24
+ CloudSearch.client.should_receive(:define_index_field) do |call|
25
+ call[:domain_name].should eq(domain_name)
26
+ call[:index_field][:literal_options][:search_enabled].should be_true
27
+ end
28
+ field.define_in_domain domain_name
29
+ end
30
+ end
@@ -0,0 +1,305 @@
1
+ require 'spec_helper'
2
+ require 'test_classes/cloud_searchable_test_class'
3
+
4
+ describe Cloudsearchable::Query do
5
+ let(:clazz){ CloudSearchableSampleClassFactory.call }
6
+
7
+ it 'can build a simple search query' do
8
+ clazz.search.where(:customer_id, :eq, 12345).query.to_q[:bq].should =~ /customer_id:'12345'/
9
+ end
10
+
11
+ it 'chains' do
12
+ query = clazz.search.where(customer_id: 12345).where(helpfulness: 42).query.to_q[:bq]
13
+ query.should =~ /customer_id:'12345'/
14
+ query.should =~ /helpfulness:'42'/
15
+ end
16
+
17
+ it 'can build a query with "not equal to" condition' do
18
+ query = clazz.search.where(:customer_id, :!=, 1234).query.to_q[:bq].should =~ /\(not customer_id:'1234'\)/
19
+ end
20
+
21
+ it 'can build a query from a hash' do
22
+ query = clazz.search.where(customer_id: 12345, helpfulness: 42).query.to_q[:bq]
23
+ query.should =~ /customer_id:'12345'/
24
+ query.should =~ /helpfulness:'42'/
25
+ end
26
+
27
+ it "doesn't build queries without a query term" do
28
+ expect do
29
+ query = clazz.search.limit(10).query.to_q
30
+ end.to raise_exception
31
+ end
32
+
33
+ describe '#where' do
34
+ describe 'uint data type' do
35
+ describe 'within_range' do
36
+ it 'supports range query with uint fields' do
37
+ clazz.search.where(:helpfulness, :within_range, "0..#{123}").query.to_q[:bq].should =~ /helpfulness:0..123/
38
+ end
39
+
40
+ it 'supports range query with uint fields using a ruby range' do
41
+ clazz.search.where(:helpfulness, :within_range, 0..123).query.to_q[:bq].should =~ /helpfulness:0..123/
42
+ end
43
+ end
44
+
45
+ describe 'inequalities' do
46
+ it 'supports greater-than with uint fields' do
47
+ clazz.search.where(:helpfulness, :>, 123).query.to_q[:bq].should =~ /helpfulness:124../
48
+ end
49
+
50
+ it 'supports greater-than-or-equal-to with uint fields' do
51
+ clazz.search.where(:helpfulness, :>=, 123).query.to_q[:bq].should =~ /helpfulness:123../
52
+ end
53
+
54
+ it 'supports less-than with uint fields' do
55
+ clazz.search.where(:helpfulness, :<, 123).query.to_q[:bq].should =~ /helpfulness:..122/
56
+ end
57
+
58
+ it 'supports less-than-or-equal-to with uint fields' do
59
+ clazz.search.where(:helpfulness, :<=, 123).query.to_q[:bq].should =~ /helpfulness:..123/
60
+ end
61
+
62
+ it 'does not permit inequality operators with non-integer fields' do
63
+ [:>, :>=, :<, :<=].each do |op|
64
+ ['123', nil, 12.0].each do |value|
65
+ expect { clazz.search.where(:helpfulness, op, value).query.to_q[:bq] }.to raise_error
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+
74
+ it 'supports querying for any of several values of a field' do
75
+ clazz.search.where(:category, :any, %w{big small}).query.to_q[:bq].should include("(or category:'big' category:'small')")
76
+ end
77
+
78
+ it 'supports text method' do
79
+ query = clazz.search.text('test').query.to_q[:q]
80
+ query.should =~ /test/
81
+ end
82
+
83
+ it 'supports chaining text and where clauses together' do
84
+ query = clazz.search.text('test').where(:featured, :==, 'f').query
85
+ query.to_q[:q].should =~ /test/
86
+ query.to_q[:bq].should =~ /featured:'f'/
87
+ end
88
+
89
+ it 'supports ordering with a rank expression' do
90
+ clazz.search.where(customer_id: 12345).order('-helpfulness').query.to_q[:rank].should eq '-helpfulness'
91
+ end
92
+
93
+ it 'supports limit' do
94
+ clazz.search.where(customer_id: 12345).limit(10).query.to_q[:size].should eq 10
95
+ end
96
+
97
+ it 'has high default limit' do
98
+ clazz.search.where(customer_id: 12345).query.to_q[:size].should eq 100000
99
+ end
100
+
101
+ it 'supports offset' do
102
+ clazz.search.where(customer_id: 12345).offset(100).query.to_q[:start].should eq 100
103
+ end
104
+
105
+ context 'queries' do
106
+ before(:each) do
107
+ clazz.cloudsearch_index.should_receive(:execute_query).and_return(cloudsearch_response)
108
+ end
109
+
110
+ context 'query warning' do
111
+ before(:each) do
112
+ clazz.stub(:find).and_return([])
113
+ Cloudsearchable.logger.should_receive(:warn).with(/CS-InvalidFieldOrRankAliasInRankParameter/)
114
+ end
115
+
116
+ let(:query){clazz.search.where(customer_id: 12345).order("-adult")}
117
+ let(:cloudsearch_response) do
118
+ # generated by ranking a literal field that is search-enabled but not result-enabled
119
+ {
120
+ "rank" => "adult",
121
+ "match-expr" => "(label initialized_at:1363464074..1366056074)",
122
+ "hits" => {
123
+ "found" => 285,
124
+ "start" => 0,
125
+ "hit" => [
126
+ {"id" => "40bdd5072b6dbae6245fe4ee837d22e3","data" => {"collection_item_id" => ["PCxSz65GIcZTtc0UpRdT-i--w-1365550370"]}},
127
+ {"id" => "00af8f5f96aa1db7aff77be5651b3bb1","data" => {"collection_item_id" => ["PCxhksJTLRYnoGXvwZik82Fkw-1365020313"]}},
128
+ {"id" => "00b6ac84e3ae402e7698959bf692a53e","data" => {"collection_item_id" => ["PCxs-fIVZnBcTzZ4MtfDguS1A-1365020274"]}},
129
+ {"id" => "018fdee653bff74abd12ac30152a2837","data" => {"collection_item_id" => ["PCxmAGHFtAgyqUrgI3HgM_P6Q-1365548349"]}},
130
+ {"id" => "01d062d24c389906eea2d16b8193eb56","data" => {"collection_item_id" => ["PCxqjaTmwydKM82NqymbryNfg-1365470479"]}},
131
+ {"id" => "01e3ee5d848a30385a4e90eb851b094d","data" => {"collection_item_id" => ["PCxSz65GIcZTtc0UpRdT-i--w-1365550369"]}},
132
+ {"id" => "01fca44cc596adb295ca6ee9f9f36499","data" => {"collection_item_id" => ["PCx7XKbKwOVf1VvEWvTl5c1Eg-1365020176"]}},
133
+ {"id" => "02b85c9835b5045065ee389954a60c5f","data" => {"collection_item_id" => ["PCxp_xid_WeTfTmb5MySEfxhQ-1365115565"]}},
134
+ {"id" => "040c01be434552a1d9e99eef9db87bdd","data" => {"collection_item_id" => ["PCxLOYzA4bCt7-bP6wsZnl-ow-1365020297"]}},
135
+ {"id" => "048567c755e30d6d64d757508f1feaa0","data" => {"collection_item_id" => ["PCxJhhnpYkeSKrOxteQo5Jckw-1365115667"]}}
136
+ ]
137
+ },
138
+ "info" => {
139
+ "rid" => "7df344e77e1076a903e1f2dc1effcf3dde0a89442fb459d00a6e60ac64b8bbfcab1fbc5b35c10949",
140
+ "time-ms" => 3,
141
+ "cpu-time-ms" => 0,
142
+ "messages" => [
143
+ {
144
+ "severity" => "warning",
145
+ "code" => "CS-InvalidFieldOrRankAliasInRankParameter",
146
+ "host" => "7df344e77e1076a9884a6c43665da57c",
147
+ "message" => "Unable to create score object for rank 'adult'"
148
+ }
149
+ ]
150
+ }
151
+ }
152
+ end
153
+
154
+ it 'causes WarningInQueryResult exception' do
155
+ lambda{ query.to_a }.should raise_error(Cloudsearchable::WarningInQueryResult)
156
+ end
157
+
158
+ it 'takes a :fatal_warnings option, and when set to false, does not raise' do
159
+ sample_query = Cloudsearchable::QueryChain.new(double, fatal_warnings: false)
160
+ sample_query.instance_variable_get(:@fatal_warnings).should eq false
161
+
162
+ q = query
163
+ q.query.instance_variable_set(:@fatal_warnings, false)
164
+ lambda{ q.to_a }.should_not raise_error(Cloudsearchable::WarningInQueryResult)
165
+ end
166
+ end
167
+
168
+ context 'valid query results' do
169
+ let(:customer_id){ '12345' }
170
+ let(:other_customer_id){ 'foo' }
171
+
172
+ let(:cloudsearch_response) do
173
+ {
174
+ "rank"=>"-text_relevance",
175
+ "match-expr"=>"(label customer_id:'12345')",
176
+ "hits"=>{
177
+ "found"=>11,
178
+ "start"=>0,
179
+ "info"=>{
180
+ "rid"=>"e2467862eecf73ec8dfcfe0cba1893abbe2e8803402f4da65b1195593c0f78ec3e8f1d29f6e40723",
181
+ "time-ms"=>2,
182
+ "cpu-time-ms"=>0
183
+ },
184
+ "hit"=>[
185
+ {"id"=>"0633e1c9793f5288c58b664356533e81", "data"=>{"test_class_id"=>["ANINSTANCEID"]}},
186
+ # {"id"=>"04931ebede796ae8b435f1fd5291e772", "data"=>{"test_class_id"=>["PCxTj26ZRmV_EnHigQWx0S06w"]}},
187
+ # {"id"=>"72159a172d3043bfcdadb5244862b9ee", "data"=>{"test_class_id"=>["PCxS_apFtZMrKuqyPhFNstzMQ"]}},
188
+ # {"id"=>"1eb815b075bc005e97dc5827e53b9615", "data"=>{"test_class_id"=>["PCxSksjDUBehPWhYYW2Dtj4KQ"]}},
189
+ # {"id"=>"3e4950b829456b13bf1460b25a7aca26", "data"=>{"test_class_id"=>["PCx1oiyh6vrHGSeLvis4USMfQ"]}},
190
+ # {"id"=>"00b441f55fff86d2d746227988da77a9", "data"=>{"test_class_id"=>["PCxpt-aW8topsnTGs-AIkzWCA"]}},
191
+ # {"id"=>"919ea27d21bbdc07ead4688a0d7ceca1", "data"=>{"test_class_id"=>["PCxFHGLbGJ2mzau_a6-gh5ORw"]}},
192
+ # {"id"=>"c663c2d9af342b0038fc808322143cfd", "data"=>{"test_class_id"=>["PCxyFwShwjWBp_WiXB0rFb2WA"]}},
193
+ # {"id"=>"de8f00af5636393e2553c4b4710d3393", "data"=>{"test_class_id"=>["PCxnqXfm8McflBgi4HsYoUXVw"]}},
194
+ # {"id"=>"e297cf21741a4c43697ea2586164a987", "data"=>{"test_class_id"=>["PCxrdk8gAEVbkuUCazu2-qLjQ"]}}
195
+ ]
196
+ }
197
+ }
198
+ end
199
+
200
+ it 'materializes' do
201
+ clazz.should_receive(:find).with(["ANINSTANCEID"]).and_return([customer_id])
202
+ query = clazz.search.where(customer_id: 12345)
203
+ query.to_a.should == [customer_id]
204
+ end
205
+
206
+ it 'materializes db results only once' do
207
+ expected_results = [customer_id, other_customer_id]
208
+ clazz.should_receive(:find).once.and_return(expected_results)
209
+
210
+ query = clazz.search.where(customer_id: 12345)
211
+ query.materialize!
212
+ query.materialize!
213
+ end
214
+
215
+ it 'should not materialize if only asking for found_count' do
216
+ clazz.should_not_receive(:find)
217
+ clazz.search.where(customer_id: 12345).found_count
218
+ end
219
+
220
+ it 'supports each for multiple results' do
221
+ expected_results = [customer_id, other_customer_id]
222
+ clazz.should_receive(:find).with(["ANINSTANCEID"]).and_return(expected_results)
223
+
224
+ results = clazz.search.where(customer_id: 12345).to_a
225
+ (0..results.length).each{ |i| results[i].should eq expected_results[i] }
226
+ end
227
+
228
+ it 'supports each for single results' do
229
+ clazz.should_receive(:find).with(["ANINSTANCEID"]).and_return(customer_id)
230
+
231
+ results = clazz.search.where(customer_id: 12345).to_a
232
+ results.each{ |r| r.should eq customer_id }
233
+ end
234
+
235
+ it 'supports each for nil result' do
236
+ clazz.should_receive(:find).with(["ANINSTANCEID"]).and_return(nil)
237
+
238
+ results = clazz.search.where(customer_id: 12345).to_a
239
+ results.each{ |r| r.should_not be }
240
+ end
241
+
242
+ it 'uses materialized method' do
243
+ clazz.should_receive(:another_find).with(["ANINSTANCEID"]).and_return(customer_id)
244
+ clazz.materialize_method :another_find
245
+ clazz.search.where(customer_id: 12345).to_a
246
+ end
247
+
248
+ it 'returns the correct found count' do
249
+ clazz.search.where(customer_id: 12345).found_count.should == 11
250
+ end
251
+ end
252
+
253
+ context 'invalid query results' do
254
+ let(:cloudsearch_response) do
255
+ {
256
+ "error"=>"info",
257
+ "rid"=>"6ddcaa561c05c4cc85ddb10cb46568af2ef64b0583910e32210f551c238586e40fc3abe629ca87b250796d395a628af6",
258
+ "time-ms"=>20,
259
+ "cpu-time-ms"=>0,
260
+ "messages"=>[
261
+ {
262
+ "severity"=>"fatal",
263
+ "code"=>"CS-UnknownFieldInMatchExpression",
264
+ "message"=>"Field 'asdf' is not defined in the metadata for this collection."
265
+ }
266
+ ]
267
+ }
268
+ end
269
+
270
+ it 'raises an exception when requesting found count with an error response' do
271
+ expect { clazz.search.where(customer_id: 12345).found_count }.to raise_error
272
+ end
273
+ end
274
+
275
+ context 'empty results with non-empty data' do
276
+ let(:cloudsearch_response) do
277
+ {
278
+ # Empty-yet-present data may occur with a NOT query, such as "(not customer_id:'XYZ')".
279
+ # Refer to: https://aws.amazon.com/support/case?caseId=107084141&language=en
280
+ "rank" => "-text_relevance",
281
+ "match-expr" => "(not customer_id:'A3E4T85Q6WPY4F')",
282
+ "hits" => {
283
+ "found" => 2087,
284
+ "start" => 0,
285
+ "hit" => [
286
+ {"id" => "fb9fb53e32c4b3714cf39be4b855d34b", "data" => { "test_class_id" => []}},
287
+ ]
288
+ },
289
+ "info" => {
290
+ "rid" => "621cf310b88f32076b1908e45b4930aafb872497bdbf3b5e64065619c0dcec96bbe513281093d6c7",
291
+ "time-ms" => 3,
292
+ "cpu-time-ms" => 0
293
+ }
294
+ }
295
+ end
296
+
297
+ it 'does not raise an exception' do
298
+ clazz.should_receive(:find).with([]).and_return(nil)
299
+ clazz.search.where(:customer_id, :!=, 'ABCDE')
300
+ expect { clazz.search.where(:customer_id, :!=, 'ABCDE').to_a }.to_not raise_error
301
+ end
302
+ end
303
+ end
304
+
305
+ end