telvue-rsolr 2.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ require 'uri'
2
+
3
+ module RSolr::Uri
4
+ # Creates a Solr based query string.
5
+ # Keys that have arrays values are set multiple times:
6
+ # params_to_solr(:q => 'query', :fq => ['a', 'b'])
7
+ # is converted to:
8
+ # ?q=query&fq=a&fq=b
9
+ # @param [boolean] escape false if no URI escaping is to be performed. Default true.
10
+ # @return [String] Solr query params as a String, suitable for use in a url
11
+ def self.params_to_solr(params, escape = true)
12
+ return URI.encode_www_form(params.reject{|k,v| k.to_s.empty? || v.to_s.empty?}) if escape
13
+
14
+ # escape = false if we are here
15
+ mapped = params.map do |k, v|
16
+ next if v.to_s.empty?
17
+ if v.class == ::Array
18
+ params_to_solr(v.map { |x| [k, x] }, false)
19
+ else
20
+ "#{k}=#{v}"
21
+ end
22
+ end
23
+ mapped.compact.join("&")
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ module RSolr
2
+ VERSION = "2.2.2"
3
+
4
+ def self.version
5
+ VERSION
6
+ end
7
+ end
@@ -0,0 +1,150 @@
1
+ module RSolr::Xml
2
+ Document = RSolr::Document
3
+ Field = RSolr::Field
4
+
5
+ class Generator < RSolr::Generator
6
+ class << self
7
+ attr_accessor :use_nokogiri
8
+
9
+ def builder_proc
10
+ if use_nokogiri
11
+ require 'nokogiri' unless defined?(::Nokogiri::XML::Builder)
12
+ :nokogiri_build
13
+ else
14
+ require 'builder' unless defined?(::Builder::XmlMarkup)
15
+ :builder_build
16
+ end
17
+ end
18
+ end
19
+ self.use_nokogiri = defined?(::Nokogiri::XML::Builder) ? true : false
20
+
21
+ CONTENT_TYPE = 'text/xml'.freeze
22
+
23
+ def content_type
24
+ CONTENT_TYPE
25
+ end
26
+
27
+
28
+ def nokogiri_build &block
29
+ b = ::Nokogiri::XML::Builder.new do |xml|
30
+ block_given? ? yield(xml) : xml
31
+ end
32
+ '<?xml version="1.0" encoding="UTF-8"?>'+b.to_xml(:indent => 0, :encoding => 'UTF-8', :save_with => ::Nokogiri::XML::Node::SaveOptions::AS_XML | ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION).strip
33
+ end
34
+ protected :nokogiri_build
35
+
36
+ def builder_build &block
37
+ b = ::Builder::XmlMarkup.new(:indent => 0, :margin => 0, :encoding => 'UTF-8')
38
+ b.instruct!
39
+ block_given? ? yield(b) : b
40
+ end
41
+ protected :builder_build
42
+
43
+ def build &block
44
+ self.send(self.class.builder_proc,&block)
45
+ end
46
+
47
+ # generates "add" xml for updating solr
48
+ # "data" can be a hash or an array of hashes.
49
+ # - each hash should be a simple key=>value pair representing a solr doc.
50
+ # If a value is an array, multiple fields will be created.
51
+ #
52
+ # "add_attrs" can be a hash for setting the add xml element attributes.
53
+ #
54
+ # This method can also accept a block.
55
+ # The value yielded to the block is a Message::Document; for each solr doc in "data".
56
+ # You can set xml element attributes for each "doc" element or individual "field" elements.
57
+ #
58
+ # For example:
59
+ #
60
+ # solr.add({:id=>1, :nickname=>'Tim'}, {:boost=>5.0, :commitWithin=>1.0}) do |doc_msg|
61
+ # doc_msg.attrs[:boost] = 10.00 # boost the document
62
+ # nickname = doc_msg.field_by_name(:nickname)
63
+ # nickname.attrs[:boost] = 20 if nickname.value=='Tim' # boost a field
64
+ # end
65
+ #
66
+ # would result in an add element having the attributes boost="10.0"
67
+ # and a commitWithin="1.0".
68
+ # Each doc element would have a boost="10.0".
69
+ # The "nickname" field would have a boost="20.0"
70
+ # if the doc had a "nickname" field with the value of "Tim".
71
+ #
72
+ def add data, add_attrs = nil, &block
73
+ add_attrs ||= {}
74
+ data = RSolr::Array.wrap(data)
75
+ build do |xml|
76
+ xml.add(add_attrs) do |add_node|
77
+ data.each do |doc|
78
+ doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
79
+ yield doc if block_given?
80
+ doc_node_builder = to_xml(doc)
81
+ self.class.use_nokogiri ? add_node.doc_(doc.attrs,&doc_node_builder) : add_node.doc(doc.attrs,&doc_node_builder)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # generates a <commit/> message
88
+ def commit opts = nil
89
+ opts ||= {}
90
+ build {|xml| xml.commit(opts) }
91
+ end
92
+
93
+ # generates a <optimize/> message
94
+ def optimize opts = nil
95
+ opts ||= {}
96
+ build {|xml| xml.optimize(opts) }
97
+ end
98
+
99
+ # generates a <rollback/> message
100
+ def rollback
101
+ build {|xml| xml.rollback({}) }
102
+ end
103
+
104
+ # generates a <delete><id>ID</id></delete> message
105
+ # "ids" can be a single value or array of values
106
+ def delete_by_id ids
107
+ ids = RSolr::Array.wrap(ids)
108
+ build do |xml|
109
+ xml.delete do |delete_node|
110
+ ids.each do |id|
111
+ self.class.use_nokogiri ? delete_node.id_(id) : delete_node.id(id)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ # generates a <delete><query>ID</query></delete> message
118
+ # "queries" can be a single value or an array of values
119
+ def delete_by_query(queries)
120
+ queries = RSolr::Array.wrap(queries)
121
+ build do |xml|
122
+ xml.delete do |delete_node|
123
+ queries.each { |query| delete_node.query(query) }
124
+ end
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def to_xml(doc)
131
+ lambda do |doc_node|
132
+ doc.fields.each do |field_obj|
133
+ value = field_obj.value
134
+
135
+ if field_obj.name.to_s == RSolr::Document::CHILD_DOCUMENT_KEY
136
+ child_node_builder = to_xml(field_obj.value)
137
+ self.class.use_nokogiri ? doc_node.doc_(&child_node_builder) : doc_node.doc(&child_node_builder)
138
+ elsif value.is_a?(Hash) && value.length == 1 && field_obj.attrs[:update].nil?
139
+ update_attr, real_value = value.first
140
+ doc_node.field real_value, field_obj.attrs.merge(update: update_attr)
141
+ elsif value.nil?
142
+ doc_node.field field_obj.value, field_obj.attrs.merge(null: true)
143
+ else
144
+ doc_node.field field_obj.value, field_obj.attrs
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,46 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ require "rsolr/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "telvue-rsolr"
8
+ s.summary = "A Ruby client for Apache Solr"
9
+ s.description = %q{RSolr aims to provide a simple and extensible library for working with Solr}
10
+ s.version = RSolr::VERSION
11
+ s.authors = ["Antoine Latter", "Dmitry Lihachev",
12
+ "Lucas Souza", "Peter Kieltyka",
13
+ "Rob Di Marco", "Magnus Bergmark",
14
+ "Jonathan Rochkind", "Chris Beer",
15
+ "Craig Smith", "Randy Souza",
16
+ "Colin Steele", "Peter Kieltyka",
17
+ "Lorenzo Riccucci", "Mike Perham",
18
+ "Mat Brown", "Shairon Toledo",
19
+ "Matthew Rudy", "Fouad Mardini",
20
+ "Jeremy Hinegardner", "Nathan Witmer",
21
+ "Naomi Dushay",
22
+ "\"shima\"", "Ben Liu"]
23
+ s.email = ["bliu@telvue.com"]
24
+ s.license = 'Apache-2.0'
25
+ s.homepage = "https://github.com/telvue/rsolr"
26
+ s.rubyforge_project = "rsolr"
27
+ s.files = `git ls-files`.split("\n")
28
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
29
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
30
+ s.require_paths = ["lib"]
31
+
32
+ s.required_ruby_version = '>= 1.9.3'
33
+
34
+ s.requirements << 'Apache Solr'
35
+
36
+ s.add_dependency 'builder', '>= 2.1.2'
37
+ s.add_dependency 'faraday', '>= 0.9.0'
38
+
39
+ s.add_development_dependency 'activesupport'
40
+ s.add_development_dependency 'nokogiri', '>= 1.4.0'
41
+ s.add_development_dependency 'rake', '>= 10.0'
42
+ s.add_development_dependency 'rdoc', '>= 4.0'
43
+ s.add_development_dependency 'rspec', '~> 3.0'
44
+ s.add_development_dependency 'simplecov'
45
+ s.add_development_dependency 'solr_wrapper'
46
+ end
@@ -0,0 +1,355 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RSolr::Client do
4
+ let(:connection) { nil }
5
+ let(:url) { "http://localhost:9999/solr" }
6
+ let(:connection_options) { { url: url, read_timeout: 42, open_timeout: 43, update_format: :xml } }
7
+
8
+ let(:client) do
9
+ RSolr::Client.new connection, connection_options
10
+ end
11
+
12
+ let(:client_with_proxy) do
13
+ RSolr::Client.new connection, connection_options.merge(proxy: 'http://localhost:8080')
14
+ end
15
+
16
+ context "initialize" do
17
+ it "should accept whatevs and set it as the @connection" do
18
+ expect(RSolr::Client.new(:whatevs).connection).to eq(:whatevs)
19
+ end
20
+
21
+ it "should use :update_path from options" do
22
+ client = RSolr::Client.new(:whatevs, { update_path: 'update_test' })
23
+ expect(client.update_path).to eql('update_test')
24
+ end
25
+
26
+ it "should use 'update' for update_path by default" do
27
+ client = RSolr::Client.new(:whatevs)
28
+ expect(client.update_path).to eql('update')
29
+ end
30
+
31
+ it "should use :proxy from options" do
32
+ client = RSolr::Client.new(:whatevs, { proxy: 'http://my.proxy/' })
33
+ expect(client.proxy.to_s).to eql('http://my.proxy/')
34
+ end
35
+
36
+ it "should use 'nil' for proxy by default" do
37
+ client = RSolr::Client.new(:whatevs)
38
+ expect(client.proxy).to be_nil
39
+ end
40
+
41
+ it "should use 'false' for proxy if passed 'false'" do
42
+ client = RSolr::Client.new(:whatevs, { proxy: false })
43
+ expect(client.proxy).to eq(false)
44
+ end
45
+
46
+ context "with an non-HTTP url" do
47
+ let(:url) { "fake://localhost:9999/solr" }
48
+
49
+ it "raises an argument error" do
50
+ expect { client }.to raise_error ArgumentError, "You must provide an HTTP(S) url."
51
+ end
52
+ end
53
+
54
+ context "with an HTTPS url" do
55
+ let(:url) { "https://localhost:9999/solr" }
56
+
57
+ it "creates a connection" do
58
+ expect(client.uri).to be_kind_of URI::HTTPS
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ context "send_and_receive" do
65
+ it "should forward these method calls the #connection object" do
66
+ [:get, :post, :head].each do |meth|
67
+ expect(client).to receive(:execute).
68
+ and_return({:status => 200, :body => "{}", :headers => {}})
69
+ client.send_and_receive '', :method => meth, :params => {}, :data => nil, :headers => {}
70
+ end
71
+ end
72
+ end
73
+
74
+ context "post" do
75
+ it "should pass the expected params to the connection's #execute method" do
76
+ request_opts = {:data => "the data", :method=>:post, :headers => {"Content-Type" => "text/plain"}}
77
+ expect(client).to receive(:execute).
78
+ with(hash_including(request_opts)).
79
+ and_return(
80
+ :body => "",
81
+ :status => 200,
82
+ :headers => {"Content-Type"=>"text/plain"}
83
+ )
84
+ client.post "update", request_opts
85
+ end
86
+ end
87
+
88
+ context "add" do
89
+ it "should send xml to the connection's #post method" do
90
+ expect(client).to receive(:execute).
91
+ with(
92
+ hash_including({
93
+ :path => "update",
94
+ :headers => {"Content-Type"=>"text/xml"},
95
+ :method => :post,
96
+ :data => "<xml/>"
97
+ })
98
+ ).
99
+ and_return(
100
+ :body => "",
101
+ :status => 200,
102
+ :headers => {"Content-Type"=>"text/xml"}
103
+ )
104
+ expect(client.builder).to receive(:add).
105
+ with({:id=>1}, {:commitWith=>10}).
106
+ and_return("<xml/>")
107
+ client.add({:id=>1}, :add_attributes => {:commitWith=>10})
108
+ end
109
+
110
+ context 'when the client is configured for json updates' do
111
+ let(:client) do
112
+ RSolr::Client.new nil, :url => "http://localhost:9999/solr", :read_timeout => 42, :open_timeout=>43, :update_format => :json
113
+ end
114
+ it "should send json to the connection's #post method" do
115
+ expect(client).to receive(:execute).
116
+ with(hash_including({
117
+ :path => 'update',
118
+ :headers => {"Content-Type" => 'application/json'},
119
+ :method => :post,
120
+ :data => '{"hello":"this is json"}'
121
+ })
122
+ ).
123
+ and_return(
124
+ :body => "",
125
+ :status => 200,
126
+ :headers => {"Content-Type"=>"text/xml"}
127
+ )
128
+ expect(client.builder).to receive(:add).
129
+ with({:id => 1}, {:commitWith=>10}).
130
+ and_return('{"hello":"this is json"}')
131
+ client.add({:id=>1}, :add_attributes => {:commitWith=>10})
132
+ end
133
+
134
+ it "should send json to the connection's #post method" do
135
+ expect(client).to receive(:execute).
136
+ with(hash_including({
137
+ :path => 'update',
138
+ :headers => {'Content-Type'=>'application/json'},
139
+ :method => :post,
140
+ :data => '{"optimise" : {}}'
141
+ })
142
+ ).
143
+ and_return(
144
+ :body => "",
145
+ :status => 200,
146
+ :headers => {"Content-Type"=>"text/xml"}
147
+ )
148
+ client.update(:data => '{"optimise" : {}}')
149
+ end
150
+ end
151
+ end
152
+
153
+ context "update" do
154
+ it "should send data to the connection's #post method" do
155
+ expect(client).to receive(:execute).
156
+ with(hash_including({
157
+ :path => "update",
158
+ :headers => {"Content-Type"=>"text/xml"},
159
+ :method => :post,
160
+ :data => "<optimize/>"
161
+ })
162
+ ).
163
+ and_return(
164
+ :body => "",
165
+ :status => 200,
166
+ :headers => {"Content-Type"=>"text/xml"}
167
+ )
168
+ client.update(:data => "<optimize/>")
169
+ end
170
+
171
+ it "should use #update_path" do
172
+ expect(client).to receive(:post).with('update_test', any_args)
173
+ expect(client).to receive(:update_path).and_return('update_test')
174
+ client.update({})
175
+ end
176
+
177
+ it "should use path from opts" do
178
+ expect(client).to receive(:post).with('update_opts', any_args)
179
+ allow(client).to receive(:update_path).and_return('update_test')
180
+ client.update({path: 'update_opts'})
181
+ end
182
+ end
183
+
184
+ context "post based helper methods:" do
185
+ [:commit, :optimize, :rollback].each do |meth|
186
+ it "should send a #{meth} message to the connection's #post method" do
187
+ expect(client).to receive(:execute).
188
+ with(hash_including({
189
+ :path => "update",
190
+ :headers => {"Content-Type"=>"text/xml"},
191
+ :method => :post,
192
+ :data => "<?xml version=\"1.0\" encoding=\"UTF-8\"?><#{meth}/>"
193
+ })
194
+ ).
195
+ and_return(
196
+ :body => "",
197
+ :status => 200,
198
+ :headers => {"Content-Type"=>"text/xml"}
199
+ )
200
+ client.send meth
201
+ end
202
+ end
203
+ end
204
+
205
+ context "delete_by_id" do
206
+ it "should send data to the connection's #post method" do
207
+ expect(client).to receive(:execute).
208
+ with(
209
+ hash_including({
210
+ :path => "update",
211
+ :headers => {"Content-Type"=>"text/xml"},
212
+ :method => :post,
213
+ :data => "<?xml version=\"1.0\" encoding=\"UTF-8\"?><delete><id>1</id></delete>"
214
+ })
215
+ ).
216
+ and_return(
217
+ :body => "",
218
+ :status => 200,
219
+ :headers => {"Content-Type"=>"text/xml"}
220
+ )
221
+ client.delete_by_id 1
222
+ end
223
+ end
224
+
225
+ context "delete_by_query" do
226
+ it "should send data to the connection's #post method" do
227
+ expect(client).to receive(:execute).
228
+ with(
229
+ hash_including({
230
+ :path => "update",
231
+ :headers => {"Content-Type"=>"text/xml"},
232
+ :method => :post,
233
+ :data => "<?xml version=\"1.0\" encoding=\"UTF-8\"?><delete><query fq=\"category:&quot;trash&quot;\"/></delete>"
234
+ })
235
+ ).
236
+ and_return(
237
+ :body => "",
238
+ :status => 200,
239
+ :headers => {"Content-Type"=>"text/xml"}
240
+ )
241
+ client.delete_by_query :fq => "category:\"trash\""
242
+ end
243
+ end
244
+
245
+ context "adapt_response" do
246
+ it 'should not try to evaluate ruby when the :qt is not :ruby' do
247
+ body = '{"time"=>"NOW"}'
248
+ result = client.adapt_response({:params=>{}}, {:status => 200, :body => body, :headers => {}})
249
+ expect(result).to eq(body)
250
+ end
251
+
252
+ it 'should evaluate ruby responses when the :wt is :ruby' do
253
+ body = '{"time"=>"NOW"}'
254
+ result = client.adapt_response({:params=>{:wt=>:ruby}}, {:status => 200, :body => body, :headers => {}})
255
+ expect(result).to eq({"time"=>"NOW"})
256
+ end
257
+
258
+ it 'should evaluate json responses when the :wt is :json' do
259
+ body = '{"time": "NOW"}'
260
+ result = client.adapt_response({:params=>{:wt=>:json}}, {:status => 200, :body => body, :headers => {}})
261
+ if defined? JSON
262
+ expect(result).to eq({"time"=>"NOW"})
263
+ else
264
+ # ruby 1.8 without the JSON gem
265
+ expect(result).to eq('{"time": "NOW"}')
266
+ end
267
+ end
268
+
269
+ it 'should return a response for a head request' do
270
+ result = client.adapt_response({:method=>:head,:params=>{}}, {:status => 200, :body => nil, :headers => {}})
271
+ expect(result.response[:status]).to eq 200
272
+ end
273
+
274
+ it "ought raise a RSolr::Error::InvalidRubyResponse when the ruby is indeed frugged, or even fruggified" do
275
+ expect {
276
+ client.adapt_response({:params=>{:wt => :ruby}}, {:status => 200, :body => "<woops/>", :headers => {}})
277
+ }.to raise_error RSolr::Error::InvalidRubyResponse
278
+ end
279
+
280
+ end
281
+
282
+ context "indifferent access" do
283
+ it "should raise a RuntimeError if the #with_indifferent_access extension isn't loaded" do
284
+ hide_const("::RSolr::HashWithIndifferentAccessWithResponse")
285
+ hide_const("ActiveSupport::HashWithIndifferentAccess")
286
+ body = "{'foo'=>'bar'}"
287
+ result = client.adapt_response({:params=>{:wt=>:ruby}}, {:status => 200, :body => body, :headers => {}})
288
+ expect { result.with_indifferent_access }.to raise_error RuntimeError
289
+ end
290
+
291
+ it "should provide indifferent access" do
292
+ require 'active_support/core_ext/hash/indifferent_access'
293
+ body = "{'foo'=>'bar'}"
294
+ result = client.adapt_response({:params=>{:wt=>:ruby}}, {:status => 200, :body => body, :headers => {}})
295
+ indifferent_result = result.with_indifferent_access
296
+
297
+ expect(result).to be_a(RSolr::Response)
298
+ expect(result['foo']).to eq('bar')
299
+ expect(result[:foo]).to be_nil
300
+
301
+ expect(indifferent_result).to be_a(RSolr::Response)
302
+ expect(indifferent_result['foo']).to eq('bar')
303
+ expect(indifferent_result[:foo]).to eq('bar')
304
+ end
305
+ end
306
+
307
+ context "build_request" do
308
+ let(:data) { 'data' }
309
+ let(:params) { { q: 'test', fq: [0,1] } }
310
+ let(:options) { { method: :post, params: params, data: data, headers: {} } }
311
+ subject { client.build_request('select', options) }
312
+
313
+ context "when params are symbols" do
314
+ it 'should return a request context array' do
315
+ [/fq=0/, /fq=1/, /q=test/, /wt=json/].each do |pattern|
316
+ expect(subject[:query]).to match pattern
317
+ end
318
+ expect(subject[:data]).to eq("data")
319
+ expect(subject[:headers]).to eq({})
320
+ end
321
+ end
322
+
323
+ context "when params are strings" do
324
+ let(:params) { { 'q' => 'test', 'wt' => 'json' } }
325
+ it 'should return a request context array' do
326
+ expect(subject[:query]).to eq 'q=test&wt=json'
327
+ expect(subject[:data]).to eq("data")
328
+ expect(subject[:headers]).to eq({})
329
+ end
330
+ end
331
+
332
+ context "when a Hash is passed in as data" do
333
+ let(:data) { { q: 'test', fq: [0,1] } }
334
+ let(:options) { { method: :post, data: data, headers: {} } }
335
+
336
+ it "sets the Content-Type header to application/x-www-form-urlencoded; charset=UTF-8" do
337
+ expect(subject[:query]).to eq("wt=json")
338
+ [/fq=0/, /fq=1/, /q=test/].each do |pattern|
339
+ expect(subject[:data]).to match pattern
340
+ end
341
+ expect(subject[:data]).not_to match(/wt=json/)
342
+ expect(subject[:headers]).to eq({"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8"})
343
+ end
344
+ end
345
+
346
+ it "should properly handle proxy configuration" do
347
+ result = client_with_proxy.build_request('select',
348
+ :method => :post,
349
+ :data => {:q=>'test', :fq=>[0,1]},
350
+ :headers => {}
351
+ )
352
+ expect(result[:uri].to_s).to match %r{^http://localhost:9999/solr/}
353
+ end
354
+ end
355
+ end