solr4r 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.
data/README ADDED
@@ -0,0 +1,40 @@
1
+ = solr4r - A Ruby client for Apache Solr
2
+
3
+ == VERSION
4
+
5
+ This documentation refers to solr4r version 0.0.1
6
+
7
+
8
+ == DESCRIPTION
9
+
10
+ TODO
11
+
12
+
13
+ == LINKS
14
+
15
+ Documentation:: http://blackwinter.github.com/solr4r
16
+ Source code:: http://github.com/blackwinter/solr4r
17
+ RubyGem:: http://rubygems.org/gems/solr4r
18
+
19
+
20
+ == AUTHORS
21
+
22
+ * Jens Wille <mailto:jens.wille@gmail.com>
23
+
24
+
25
+ == LICENSE AND COPYRIGHT
26
+
27
+ Copyright (C) 2014 Jens Wille
28
+
29
+ solr4r is free software: you can redistribute it and/or modify it
30
+ under the terms of the GNU Affero General Public License as published by
31
+ the Free Software Foundation, either version 3 of the License, or (at your
32
+ option) any later version.
33
+
34
+ solr4r is distributed in the hope that it will be useful, but
35
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
36
+ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
37
+ License for more details.
38
+
39
+ You should have received a copy of the GNU Affero General Public License
40
+ along with solr4r. If not, see <http://www.gnu.org/licenses/>.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require File.expand_path(%q{../lib/solr4r/version}, __FILE__)
2
+
3
+ begin
4
+ require 'hen'
5
+
6
+ Hen.lay! {{
7
+ gem: {
8
+ name: %q{solr4r},
9
+ version: Solr4R::VERSION,
10
+ summary: %q{A Ruby client for Apache Solr},
11
+ description: %q{Access the Apache Solr search server from Ruby},
12
+ author: %q{Jens Wille},
13
+ email: %q{jens.wille@gmail.com},
14
+ license: %q{AGPL-3.0},
15
+ homepage: :blackwinter,
16
+ dependencies: { curb: ['~> 0.8', '> 0.8.5'], nokogiri: '~> 1.6' },
17
+
18
+ required_ruby_version: '>= 1.9.3'
19
+ }
20
+ }}
21
+ rescue LoadError => err
22
+ warn "Please install the `hen' gem. (#{err})"
23
+ end
data/TODO ADDED
@@ -0,0 +1,3 @@
1
+ * request: https://github.com/taf2/curb/pull/196
2
+ * documentation
3
+ * specs
@@ -0,0 +1,291 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # solr4r -- A Ruby client for Apache Solr #
7
+ # #
8
+ # Copyright (C) 2014 Jens Wille #
9
+ # #
10
+ # Mir is free software: you can redistribute it and/or modify it under the #
11
+ # terms of the GNU Affero General Public License as published by the Free #
12
+ # Software Foundation, either version 3 of the License, or (at your option) #
13
+ # any later version. #
14
+ # #
15
+ # solr4r is distributed in the hope that it will be useful, but WITHOUT ANY #
16
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
17
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
18
+ # more details. #
19
+ # #
20
+ # You should have received a copy of the GNU Affero General Public License #
21
+ # along with solr4r. If not, see <http://www.gnu.org/licenses/>. #
22
+ # #
23
+ ###############################################################################
24
+ #++
25
+
26
+ require 'nokogiri'
27
+
28
+ module Solr4R
29
+
30
+ Document = Nokogiri::XML::Document
31
+
32
+ class Builder < Nokogiri::XML::Builder
33
+
34
+ DEFAULT_OPTIONS = {
35
+ encoding: 'UTF-8'
36
+ }
37
+
38
+ def initialize(options = {})
39
+ if block_given?
40
+ raise ArgumentError,
41
+ 'block argument not supported, use options hash instead'
42
+ end
43
+
44
+ super(@options = DEFAULT_OPTIONS.merge(options))
45
+
46
+ @_solr_doc = @doc
47
+ end
48
+
49
+ # See Schema[http://wiki.apache.org/solr/UpdateXmlMessages#add.2Freplace_documents].
50
+ #
51
+ # Examples:
52
+ #
53
+ # # single document
54
+ # add(employeeId: '05991', office: 'Bridgewater', skills: %w[Perl Java])
55
+ #
56
+ # <?xml version="1.0" encoding="UTF-8"?>
57
+ # <add>
58
+ # <doc>
59
+ # <field name="employeeId">05991</field>
60
+ # <field name="office">Bridgewater</field>
61
+ # <field name="skills">Perl</field>
62
+ # <field name="skills">Java</field>
63
+ # </doc>
64
+ # </add>
65
+ #
66
+ # # multiple documents
67
+ # add([{ employeeId: '05992', office: 'Blackwater' }, { employeeId: '05993', skills: 'Ruby' }])
68
+ #
69
+ # <?xml version="1.0" encoding="UTF-8"?>
70
+ # <add>
71
+ # <doc>
72
+ # <field name="employeeId">05992</field>
73
+ # <field name="office">Blackwater</field>
74
+ # </doc>
75
+ # <doc>
76
+ # <field name="employeeId">05993</field>
77
+ # <field name="skills">Ruby</field>
78
+ # </doc>
79
+ # </add>
80
+ #
81
+ # # add attributes
82
+ # add([id: 42, text: 'blah'], commitWithin: 23)
83
+ #
84
+ # <?xml version="1.0" encoding="UTF-8"?>
85
+ # <add commitWithin="23">
86
+ # <doc>
87
+ # <field name="id">42</field>
88
+ # <field name="text">blah</field>
89
+ # </doc>
90
+ # </add>
91
+ #
92
+ # # document attributes
93
+ # add([[{ id: 42, text: 'blah' }, { boost: 10.0 }]])
94
+ #
95
+ # <?xml version="1.0" encoding="UTF-8"?>
96
+ # <add>
97
+ # <doc boost="10.0">
98
+ # <field name="id">42</field>
99
+ # <field name="text">blah</field>
100
+ # </doc>
101
+ # </add>
102
+ #
103
+ # # field attributes
104
+ # add(id: 42, text: ['blah', boost: 2.0])
105
+ #
106
+ # <?xml version="1.0" encoding="UTF-8"?>
107
+ # <add>
108
+ # <doc>
109
+ # <field name="id">42</field>
110
+ # <field boost="2.0" name="text">blah</field>
111
+ # </doc>
112
+ # </add>
113
+ #
114
+ # # all attributes together
115
+ # add([[{ id: 42, text: ['blah', boost: 2.0] }, { boost: 10.0 }]], commitWithin: 23)
116
+ #
117
+ # <?xml version="1.0" encoding="UTF-8"?>
118
+ # <add commitWithin="23">
119
+ # <doc boost="10.0">
120
+ # <field name="id">42</field>
121
+ # <field boost="2.0" name="text">blah</field>
122
+ # </doc>
123
+ # </add>
124
+ def add(doc, attributes = {})
125
+ to_xml(:add, attributes) { |add_node|
126
+ doc = [doc] unless doc.is_a?(Array)
127
+ doc.each { |hash, doc_attributes|
128
+ doc_(doc_attributes) { |doc_node|
129
+ hash.each { |key, values|
130
+ values = values.is_a?(Array) ? values.dup : [values]
131
+
132
+ field_attributes = values.last.is_a?(Hash) ? values.pop : {}
133
+ field_attributes = field_attributes.merge(name: key)
134
+
135
+ values.each { |value| doc_node.field_(value, field_attributes) }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ end
141
+
142
+ # See Schema[http://wiki.apache.org/solr/UpdateXmlMessages#A.22commit.22_and_.22optimize.22].
143
+ #
144
+ # Examples:
145
+ #
146
+ # commit
147
+ #
148
+ # <?xml version="1.0" encoding="UTF-8"?>
149
+ # <commit/>
150
+ #
151
+ # commit(softCommit: true)
152
+ #
153
+ # <?xml version="1.0" encoding="UTF-8"?>
154
+ # <commit softCommit="true"/>
155
+ def commit(attributes = {})
156
+ to_xml(:commit, attributes)
157
+ end
158
+
159
+ # See Schema[http://wiki.apache.org/solr/UpdateXmlMessages#A.22commit.22_and_.22optimize.22].
160
+ #
161
+ # Examples:
162
+ #
163
+ # optimize
164
+ #
165
+ # <?xml version="1.0" encoding="UTF-8"?>
166
+ # <optimize/>
167
+ #
168
+ # optimize(maxSegments: 42)
169
+ #
170
+ # <?xml version="1.0" encoding="UTF-8"?>
171
+ # <optimize maxSegments="42"/>
172
+ def optimize(attributes = {})
173
+ to_xml(:optimize, attributes)
174
+ end
175
+
176
+ # See Schema[http://wiki.apache.org/solr/UpdateXmlMessages#A.22rollback.22].
177
+ #
178
+ # Example:
179
+ #
180
+ # rollback
181
+ #
182
+ # <?xml version="1.0" encoding="UTF-8"?>
183
+ # <rollback/>
184
+ def rollback
185
+ to_xml(:rollback)
186
+ end
187
+
188
+ # See Schema[http://wiki.apache.org/solr/UpdateXmlMessages#A.22delete.22_documents_by_ID_and_by_Query].
189
+ #
190
+ # Examples:
191
+ #
192
+ # # single ID
193
+ # delete(id: '05991')
194
+ #
195
+ # <?xml version="1.0" encoding="UTF-8"?>
196
+ # <delete>
197
+ # <id>05991</id>
198
+ # </delete>
199
+ #
200
+ # # multiple IDs
201
+ # delete(id: %w[05991 06000])
202
+ #
203
+ # <?xml version="1.0" encoding="UTF-8"?>
204
+ # <delete>
205
+ # <id>05991</id>
206
+ # <id>06000</id>
207
+ # </delete>
208
+ #
209
+ # # single query
210
+ # delete(query: 'office:Bridgewater')
211
+ #
212
+ # <?xml version="1.0" encoding="UTF-8"?>
213
+ # <delete>
214
+ # <query>office:Bridgewater</query>
215
+ # </delete>
216
+ #
217
+ # # multiple queries
218
+ # delete(query: %w[office:Bridgewater office:Osaka])
219
+ #
220
+ # <?xml version="1.0" encoding="UTF-8"?>
221
+ # <delete>
222
+ # <query>office:Bridgewater</query>
223
+ # <query>office:Osaka</query>
224
+ # </delete>
225
+ #
226
+ # # query hash
227
+ # delete(query: { office: 'Bridgewater', skills: 'Perl' })
228
+ #
229
+ # <?xml version="1.0" encoding="UTF-8"?>
230
+ # <delete>
231
+ # <query>office:Bridgewater</query>
232
+ # <query>skills:Perl</query>
233
+ # </delete>
234
+ #
235
+ # # query hash with array
236
+ # delete(query: { office: %w[Bridgewater Osaka] })
237
+ #
238
+ # <?xml version="1.0" encoding="UTF-8"?>
239
+ # <delete>
240
+ # <query>office:Bridgewater</query>
241
+ # <query>office:Osaka</query>
242
+ # </delete>
243
+ #
244
+ # # both IDs and queries
245
+ # delete(id: %w[05991 06000], query: { office: %w[Bridgewater Osaka] })
246
+ #
247
+ # <?xml version="1.0" encoding="UTF-8"?>
248
+ # <delete>
249
+ # <id>05991</id>
250
+ # <id>06000</id>
251
+ # <query>office:Bridgewater</query>
252
+ # <query>office:Osaka</query>
253
+ # </delete>
254
+ def delete(hash)
255
+ to_xml(:delete) { |delete_node|
256
+ hash.each { |key, values|
257
+ values = [values] unless values.is_a?(Array)
258
+ values.each { |value|
259
+ ary = []
260
+
261
+ if value.is_a?(Hash)
262
+ value.each { |k, v| Array(v).each { |w| ary << "#{k}:#{w}" } }
263
+ else
264
+ ary << value
265
+ end
266
+
267
+ ary.each { |v| delete_node.method_missing(key, v) }
268
+ }
269
+ }
270
+ }
271
+ end
272
+
273
+ def inspect
274
+ '#<%s:0x%x @options=%p>' % [
275
+ self.class, object_id, @options
276
+ ]
277
+ end
278
+
279
+ private
280
+
281
+ def to_xml(name, attributes = {}, &block)
282
+ self.parent = self.doc = @_solr_doc.dup
283
+
284
+ method_missing(name, attributes, &block)
285
+
286
+ super(&nil)
287
+ end
288
+
289
+ end
290
+
291
+ end
@@ -0,0 +1,184 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # solr4r -- A Ruby client for Apache Solr #
7
+ # #
8
+ # Copyright (C) 2014 Jens Wille #
9
+ # #
10
+ # Mir is free software: you can redistribute it and/or modify it under the #
11
+ # terms of the GNU Affero General Public License as published by the Free #
12
+ # Software Foundation, either version 3 of the License, or (at your option) #
13
+ # any later version. #
14
+ # #
15
+ # solr4r is distributed in the hope that it will be useful, but WITHOUT ANY #
16
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
17
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
18
+ # more details. #
19
+ # #
20
+ # You should have received a copy of the GNU Affero General Public License #
21
+ # along with solr4r. If not, see <http://www.gnu.org/licenses/>. #
22
+ # #
23
+ ###############################################################################
24
+ #++
25
+
26
+ require 'uri'
27
+
28
+ module Solr4R
29
+
30
+ class Client
31
+
32
+ DEFAULT_HOST = 'localhost'
33
+ DEFAULT_PATH = 'solr/'
34
+ DEFAULT_PORT = 8983
35
+
36
+ DEFAULT_PARAMS = {
37
+ wt: :json
38
+ }
39
+
40
+ DEFAULT_ENDPOINTS = %w[select query spell suggest terms] <<
41
+ ['ping', path: 'admin/ping', method: :head] <<
42
+ ['dump', path: 'debug/dump']
43
+
44
+ DEFAULT_SELECT_PATH = 'select'
45
+
46
+ DEFAULT_UPDATE_PATH = 'update'
47
+
48
+ MATCH_ALL_QUERY = '*:*'
49
+
50
+ def initialize(options = {})
51
+ if options.is_a?(String)
52
+ url, options = options, {}
53
+ else
54
+ url = options.fetch(:url, default_url(options))
55
+ end
56
+
57
+ self.url, self.options = URI.parse(url), options
58
+
59
+ self.request = options.fetch(:request, Request.new)
60
+ self.builder = options.fetch(:builder, Builder.new)
61
+
62
+ self.default_params = options.fetch(:default_params, DEFAULT_PARAMS)
63
+
64
+ register_endpoints(options.fetch(:endpoints, DEFAULT_ENDPOINTS))
65
+ end
66
+
67
+ attr_accessor :url, :options, :request, :builder, :default_params
68
+
69
+ def register_endpoints(endpoints)
70
+ endpoints.each { |args| register_endpoint(*args) } if endpoints
71
+ self
72
+ end
73
+
74
+ def register_endpoint(path, options = {})
75
+ name, path = path, options.fetch(:path, path)
76
+
77
+ if error = invalid_endpoint?(name.to_s)
78
+ raise ArgumentError, "invalid endpoint: #{name} (#{error})"
79
+ else
80
+ define_singleton_method(name) { |_params = {}, _options = {}, &block|
81
+ send_request(path, options.merge(_options.merge(
82
+ params: options.fetch(:params, {}).merge(_params))), &block)
83
+ }
84
+ end
85
+
86
+ self
87
+ end
88
+
89
+ def get(path, params = {}, options = {}, &block)
90
+ send_request(path, options.merge(method: :get, params: params), &block)
91
+ end
92
+
93
+ def post(path, data = nil, options = {}, &block)
94
+ send_request(path, options.merge(method: :post, data: data), &block)
95
+ end
96
+
97
+ def head(path, params = {}, options = {}, &block)
98
+ send_request(path, options.merge(method: :head, params: params), &block)
99
+ end
100
+
101
+ def update(data, options = {}, path = DEFAULT_UPDATE_PATH, &block)
102
+ options = amend_options(options, :headers, 'Content-Type' => 'text/xml')
103
+ post(path, data, options, &block)
104
+ end
105
+
106
+ # See Builder#add.
107
+ def add(doc, attributes = {}, options = {}, &block)
108
+ update(builder.add(doc, attributes), options, &block)
109
+ end
110
+
111
+ # See Builder#commit.
112
+ def commit(attributes = {}, options = {}, &block)
113
+ update(builder.commit(attributes), options, &block)
114
+ end
115
+
116
+ # See Builder#optimize.
117
+ def optimize(attributes = {}, options = {}, &block)
118
+ update(builder.optimize(attributes), options, &block)
119
+ end
120
+
121
+ # See Builder#rollback.
122
+ def rollback(options = {}, &block)
123
+ update(builder.rollback, options, &block)
124
+ end
125
+
126
+ # See Builder#delete.
127
+ def delete(hash, options = {}, &block)
128
+ update(builder.delete(hash), options, &block)
129
+ end
130
+
131
+ # See #delete.
132
+ def delete_by_id(id, options = {}, &block)
133
+ delete({ id: id }, options, &block)
134
+ end
135
+
136
+ # See #delete.
137
+ def delete_by_query(query, options = {}, &block)
138
+ delete({ query: query }, options, &block)
139
+ end
140
+
141
+ # See #delete_by_query.
142
+ def delete_all!(options = {}, &block)
143
+ delete_by_query(MATCH_ALL_QUERY, options, &block)
144
+ end
145
+
146
+ def count(params = {}, options = {}, path = DEFAULT_SELECT_PATH, &block)
147
+ params = params.merge(rows: 0)
148
+ params[:q] ||= MATCH_ALL_QUERY
149
+ get(path, params, options, &block)
150
+ end
151
+
152
+ def inspect
153
+ '#<%s:0x%x @url=%p, @default_params=%p>' % [
154
+ self.class, object_id, url, default_params
155
+ ]
156
+ end
157
+
158
+ private
159
+
160
+ def default_url(options)
161
+ 'http://%s:%d/%s' % [
162
+ options.fetch(:host, DEFAULT_HOST),
163
+ options.fetch(:port, DEFAULT_PORT),
164
+ options.fetch(:path, DEFAULT_PATH)
165
+ ]
166
+ end
167
+
168
+ def amend_options(options, key, value)
169
+ options.merge(key => value.merge(options.fetch(key, {})))
170
+ end
171
+
172
+ def send_request(path, options, &block)
173
+ options = amend_options(options, :params, default_params)
174
+ request.execute(URI.join(url, path), options, &block)
175
+ end
176
+
177
+ def invalid_endpoint?(name)
178
+ 'method already defined' if respond_to?(name) || (
179
+ respond_to?(name, true) && !DEFAULT_ENDPOINTS.flatten.include?(name))
180
+ end
181
+
182
+ end
183
+
184
+ end
@@ -0,0 +1,122 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # solr4r -- A Ruby client for Apache Solr #
7
+ # #
8
+ # Copyright (C) 2014 Jens Wille #
9
+ # #
10
+ # Mir is free software: you can redistribute it and/or modify it under the #
11
+ # terms of the GNU Affero General Public License as published by the Free #
12
+ # Software Foundation, either version 3 of the License, or (at your option) #
13
+ # any later version. #
14
+ # #
15
+ # solr4r is distributed in the hope that it will be useful, but WITHOUT ANY #
16
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
17
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for #
18
+ # more details. #
19
+ # #
20
+ # You should have received a copy of the GNU Affero General Public License #
21
+ # along with solr4r. If not, see <http://www.gnu.org/licenses/>. #
22
+ # #
23
+ ###############################################################################
24
+ #++
25
+
26
+ require 'curl'
27
+
28
+ module Solr4R
29
+
30
+ class Request < Curl::Easy
31
+
32
+ DEFAULT_VERB = :get
33
+
34
+ DEFAULT_USER_AGENT = "Solr4R/#{VERSION}"
35
+
36
+ RESPONSE_ATTRIBUTES = {
37
+ # request
38
+ headers: :request_headers,
39
+ last_effective_url: :request_url,
40
+ post_body: :post_body,
41
+ params: :request_params,
42
+ verb: :request_verb,
43
+
44
+ # response
45
+ body_str: :response_body,
46
+ content_type: :content_type,
47
+ header_str: :response_header,
48
+ response_code: :response_code
49
+ }
50
+
51
+ def initialize(options = {})
52
+ if block_given?
53
+ raise ArgumentError,
54
+ 'block argument not supported, use options hash instead'
55
+ end
56
+
57
+ super()
58
+
59
+ self.options = options.merge(params: nil, verb: nil)
60
+ set_options
61
+ end
62
+
63
+ attr_accessor :options, :params, :verb
64
+
65
+ def execute(request_url, options = {}, &block)
66
+ prepare_request(request_url, options, &block)
67
+
68
+ send("http_#{verb}")
69
+
70
+ Response.new { |response|
71
+ RESPONSE_ATTRIBUTES.each { |attribute, key|
72
+ response.send("#{key}=", send(attribute))
73
+ }
74
+ }
75
+ end
76
+
77
+ def reset
78
+ super.tap {
79
+ set_options
80
+ headers['User-Agent'] ||= DEFAULT_USER_AGENT
81
+ }
82
+ end
83
+
84
+ def inspect
85
+ '#<%s:0x%x @options=%p, @verb=%p, @url=%p, @response_code=%p>' % [
86
+ self.class, object_id, options, verb, url, response_code
87
+ ]
88
+ end
89
+
90
+ private
91
+
92
+ def set_options
93
+ options.each { |key, value| send("#{key}=", value) }
94
+ end
95
+
96
+ def prepare_request(request_url, options)
97
+ reset
98
+
99
+ self.url = Curl.urlalize(request_url.to_s,
100
+ self.params = options.fetch(:params, {}))
101
+
102
+ case self.verb = options.fetch(:method, DEFAULT_VERB)
103
+ when :get, :head
104
+ # ok
105
+ when :post, :delete, :patch
106
+ self.post_body = Curl.postalize(options[:data])
107
+ when :put
108
+ self.put_data = Curl.postalize(options[:data])
109
+ else
110
+ raise ArgumentError, "verb not supported: #{verb}"
111
+ end
112
+
113
+ headers.update(options[:headers]) if options[:headers]
114
+
115
+ yield self if block_given?
116
+
117
+ self
118
+ end
119
+
120
+ end
121
+
122
+ end