rsolr 2.6.0 → 3.0.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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +19 -17
- data/CHANGES.txt +10 -0
- data/Gemfile +0 -6
- data/README.md +370 -0
- data/Rakefile +1 -1
- data/docker-compose.yaml +32 -0
- data/lib/rsolr/client.rb +68 -16
- data/lib/rsolr/version.rb +1 -1
- data/rsolr.gemspec +1 -2
- data/spec/api/client_spec.rb +133 -1
- data/spec/api/error_spec.rb +4 -2
- data/spec/api/json_spec.rb +3 -25
- data/spec/fixtures/basic_configs/schema.xml +1 -1
- data/spec/fixtures/basic_configs/solrconfig.xml +2 -2
- data/spec/fixtures/security/security.json +16 -0
- data/spec/integration/basic_auth_spec.rb +29 -0
- data/spec/integration/solr5_spec.rb +2 -15
- data/spec/spec_helper.rb +1 -1
- metadata +9 -29
- data/README.rdoc +0 -227
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7cd06ee9f0201b75d69b9f7d02f5ad6641272e5ec8d6d1eaa1b57aaedc90daef
|
|
4
|
+
data.tar.gz: 277248ac909e888160f8addac51fa01d44dc26cb089b8bfcbdb3ed93263015c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e095fbdf8f63f68eeb5b9919b6cf61d745b1dd044d819f2a762dbf82bf3b9bf813b0b022c61709f57e3f080ce96104e453d22db120d2d62f1a94453cf58b145c
|
|
7
|
+
data.tar.gz: 8ff5849b11a1bb25f315cd5373354dd2479d7c0725d8092b76e8360fb31886f3d406f4d2cecf898003d11124af0566528aa6f9dd860680510875008a10afe046
|
data/.github/workflows/ruby.yml
CHANGED
|
@@ -2,28 +2,30 @@ name: CI
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
branches: [
|
|
5
|
+
branches: [main]
|
|
6
6
|
pull_request:
|
|
7
|
-
branches: [
|
|
7
|
+
branches: [main]
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
tests:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
strategy:
|
|
13
13
|
matrix:
|
|
14
|
-
ruby: [jruby-
|
|
15
|
-
faraday: [
|
|
14
|
+
ruby: [jruby-10.0.3.0, "3.2", "3.3", "3.4", "4.0"]
|
|
15
|
+
faraday: ["~> 1", "~>2"]
|
|
16
16
|
steps:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
- name: Set up Ruby
|
|
19
|
+
uses: ruby/setup-ruby@v1
|
|
20
|
+
with:
|
|
21
|
+
ruby-version: ${{ matrix.ruby }}
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: bundle install
|
|
24
|
+
env:
|
|
25
|
+
FARADAY_VERSION: ${{ matrix.faraday}}
|
|
26
|
+
- name: Start Solr
|
|
27
|
+
run: docker compose up -d --wait --wait-timeout 60
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: bundle exec rake
|
|
30
|
+
env:
|
|
31
|
+
FARADAY_VERSION: ${{ matrix.faraday}}
|
data/CHANGES.txt
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
3.0.0
|
|
2
|
+
|
|
3
|
+
- Drop faraday 0.x support by @jcoyne in https://github.com/rsolr/rsolr/pull/255
|
|
4
|
+
- Allow for setting a default http method by @cjcolvar in https://github.com/rsolr/rsolr/pull/252
|
|
5
|
+
- Move query params into data hash when using post and no data is provided by @cjcolvar in https://github.com/rsolr/rsolr/pull/253
|
|
6
|
+
- Support HTTP Basic Auth via a basic_auth: option (in addition to URL-embedded) by @maxkadel in https://github.com/rsolr/rsolr/pull/259
|
|
7
|
+
- Redact only basic auth credentials from Faraday's cause exception by @maxkadel in https://github.com/rsolr/rsolr/pull/261
|
|
8
|
+
|
|
9
|
+
**Full Changelog**: https://github.com/rsolr/rsolr/compare/v2.6.0...v3.0.0
|
|
10
|
+
|
|
1
11
|
2.6.0
|
|
2
12
|
|
|
3
13
|
- Stop testing on Ruby 2. https://github.com/rsolr/rsolr/pull/237
|
data/Gemfile
CHANGED
|
@@ -4,10 +4,4 @@ gemspec
|
|
|
4
4
|
|
|
5
5
|
gem "builder", ">= 2.1.2"
|
|
6
6
|
|
|
7
|
-
if defined? JRUBY_VERSION
|
|
8
|
-
# HTTP.rb (used by solr_wrapper to download solr for integration testing) fails
|
|
9
|
-
# to download the full contents of files (under jruby)?
|
|
10
|
-
gem "http", '< 5', platforms: :jruby
|
|
11
|
-
end
|
|
12
|
-
|
|
13
7
|
gem 'faraday', ENV['FARADAY_VERSION'] if ENV['FARADAY_VERSION']
|
data/README.md
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# RSolr
|
|
2
|
+
|
|
3
|
+
A simple, extensible Ruby client for Apache Solr.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
The code docs: http://www.rubydoc.info/gems/rsolr
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
gem install rsolr
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require 'rsolr'
|
|
19
|
+
|
|
20
|
+
# Direct connection
|
|
21
|
+
solr = RSolr.connect :url => 'http://solrserver.com'
|
|
22
|
+
|
|
23
|
+
# Connecting over a proxy server
|
|
24
|
+
solr = RSolr.connect :url => 'http://solrserver.com', :proxy=>'http://user:pass@proxy.example.com:8080'
|
|
25
|
+
|
|
26
|
+
# Using an alternate Faraday adapter
|
|
27
|
+
solr = RSolr.connect :url => 'http://solrserver.com', :adapter => :em_http
|
|
28
|
+
|
|
29
|
+
# Using a custom Faraday connection
|
|
30
|
+
conn = Faraday.new do |faraday|
|
|
31
|
+
faraday.response :logger # log requests to STDOUT
|
|
32
|
+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
|
33
|
+
end
|
|
34
|
+
solr = RSolr.connect conn, :url => 'http://solrserver.com'
|
|
35
|
+
|
|
36
|
+
# send a request to /select
|
|
37
|
+
response = solr.get 'select', :params => {:q => '*:*'}
|
|
38
|
+
|
|
39
|
+
# send a request to /catalog
|
|
40
|
+
response = solr.get 'catalog', :params => {:q => '*:*'}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
When the Solr `:wt` is `:ruby`, then the response will be a Hash. This Hash is the same object returned by Solr, but evaluated as Ruby. If the `:wt` is not `:ruby`, then the response will be a String.
|
|
44
|
+
|
|
45
|
+
The response also exposes 2 attribute readers (for any `:wt` value), `:request` and `:response`. Both are Hash objects with symbolized keys.
|
|
46
|
+
|
|
47
|
+
The `:request` attribute contains the original request context. You can use this for debugging or logging. Some of the keys this object contains are `:uri`, `:query`, `:method` etc..
|
|
48
|
+
|
|
49
|
+
The `:response` attribute contains the original response. This object contains the `:status`, `:body` and `:headers` keys.
|
|
50
|
+
|
|
51
|
+
## Basic Auth
|
|
52
|
+
|
|
53
|
+
Credentials can be embedded in the URL:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
solr = RSolr.connect :url => 'http://user:pass@solrserver.com'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
or passed explicitly via `:basic_auth`, which is useful if you'd rather keep
|
|
60
|
+
secrets out of a URL string (logs, error messages, etc. tend to capture URLs
|
|
61
|
+
more casually than headers):
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
solr = RSolr.connect :url => 'http://solrserver.com', :basic_auth => { :user => 'user', :password => 'pass' }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`:basic_auth` accepts string or symbol keys (`'user'`/`'password'`
|
|
68
|
+
work the same as `:user`/`:password`), so a hash loaded straight
|
|
69
|
+
from YAML config works without re-symbolizing it yourself.
|
|
70
|
+
|
|
71
|
+
If both are given, `:basic_auth` wins. Either way, this only protects the
|
|
72
|
+
credential from casual logging - it doesn't encrypt anything in transit, so
|
|
73
|
+
use `https://` if the connection isn't otherwise trusted (e.g. firewalled).
|
|
74
|
+
|
|
75
|
+
## Request formats
|
|
76
|
+
|
|
77
|
+
By default, RSolr uses the Solr JSON command format for all requests.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
RSolr.connect :url => 'http://solrserver.com', update_format: :json # the default
|
|
81
|
+
# or
|
|
82
|
+
RSolr.connect :url => 'http://solrserver.com', update_format: :xml
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Timeouts
|
|
86
|
+
|
|
87
|
+
The read and connect timeout settings can be set when creating a new instance of RSolr, and will
|
|
88
|
+
be passed on to underlying Faraday instance:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
solr = RSolr.connect(:timeout => 120, :open_timeout => 120)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Retry 503s
|
|
95
|
+
|
|
96
|
+
A 503 is usually a temporary error which RSolr may retry if requested. You may specify the number of retry attempts with the `:retry_503` option.
|
|
97
|
+
|
|
98
|
+
Only requests which specify a Retry-After header will be retried, after waiting the indicated retry interval, otherwise RSolr will treat the request as a 500. You may specify a maximum Retry-After interval to wait with the `:retry_after_limit` option (default: one second).
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
solr = RSolr.connect(:retry_503 => 1, :retry_after_limit => 1)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For additional control, consider using a custom Faraday connection (see above) using its `retry` middleware.
|
|
105
|
+
|
|
106
|
+
## Querying
|
|
107
|
+
|
|
108
|
+
Use the `#get` / `#post` method to send search requests to the /select handler:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
response = solr.get 'select', :params => {
|
|
112
|
+
:q=>'washington',
|
|
113
|
+
:start=>0,
|
|
114
|
+
:rows=>10
|
|
115
|
+
}
|
|
116
|
+
response["response"]["docs"].each{|doc| puts doc["id"] }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The `:params` sent into the method are sent to Solr as-is, which is to say they are converted to Solr url style, but no special mapping is used.
|
|
120
|
+
When an array is used, multiple parameters **with the same name** are generated for the Solr query. Example:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
solr.get 'select', :params => {:q=>'roses', :fq=>['red', 'violet']}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The above statement generates this Solr query:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
select?q=roses&fq=red&fq=violet
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Pagination
|
|
133
|
+
|
|
134
|
+
To paginate through a set of Solr documents, use the paginate method:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
solr.paginate 1, 10, "select", :params => {:q => "test"}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The first argument is the current page, the second is how many documents to return for each page. In other words, "page" is the "start" Solr param and "per-page" is the "rows" Solr param.
|
|
141
|
+
|
|
142
|
+
The paginate method returns WillPaginate ready "docs" objects, so for example in a Rails application, paginating is as simple as:
|
|
143
|
+
|
|
144
|
+
```erb
|
|
145
|
+
<%= will_paginate @solr_response["response"]["docs"] %>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Method Missing
|
|
149
|
+
|
|
150
|
+
The `RSolr::Client` class also uses `method_missing` for setting the request handler/path:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
solr.paintings :params => {:q=>'roses', :fq=>['red', 'violet']}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
This is sent to Solr as:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
paintings?q=roses&fq=red&fq=violet
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This works with pagination as well:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
solr.paginate_paintings 1, 10, {:q=>'roses', :fq=>['red', 'violet']}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Using POST for Search Queries
|
|
169
|
+
|
|
170
|
+
There may be cases where the query string is too long for a GET request. RSolr solves this issue by converting hash objects into form-encoded strings:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
response = solr.music :data => {:q => "*:*"}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The `:data` hash is serialized as a form-encoded query string, and the correct content-type headers are sent along to Solr.
|
|
177
|
+
|
|
178
|
+
### Sending HEAD Requests
|
|
179
|
+
|
|
180
|
+
There may be cases where you'd like to send a HEAD request to Solr:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
solr.head("admin/ping").response[:status] == 200
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Sending HTTP Headers
|
|
187
|
+
|
|
188
|
+
Solr responds to the request headers listed here: http://wiki.apache.org/solr/SolrAndHTTPCaches
|
|
189
|
+
To send header information to Solr using RSolr, just use the `:headers` option:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
response = solr.head "admin/ping", :headers => {"Cache-Control" => "If-None-Match"}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Building a Request
|
|
196
|
+
|
|
197
|
+
`RSolr::Client` provides a method for building a request context, which can be useful for debugging or logging etc.:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
request_context = solr.build_request "select", :data => {:q => "*:*"}, :method => :post, :headers => {}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
To build a paginated request use build_paginated_request:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
request_context = solr.build_paginated_request 1, 10, "select", ...
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Updating Solr
|
|
210
|
+
|
|
211
|
+
Updating is done using native Ruby objects. Hashes are used for single documents and arrays are used for a collection of documents (hashes). These objects get turned into simple XML "messages". Raw XML strings can also be used.
|
|
212
|
+
|
|
213
|
+
Single document via `#add`
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
solr.add :id=>1, :price=>1.00
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Multiple documents via `#add`
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
documents = [{:id=>1, :price=>1.00}, {:id=>2, :price=>10.50}]
|
|
223
|
+
solr.add documents
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The optional `:add_attributes` hash can also be used to set Solr "add" document attributes:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
solr.add documents, :add_attributes => {:commitWithin => 10}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Raw commands via `#update`
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
solr.update data: '<commit/>', headers: { 'Content-Type' => 'text/xml' }
|
|
236
|
+
solr.update data: { optimize: true }.to_json, headers: { 'Content-Type' => 'application/json' }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
When adding, you can also supply "add" xml element attributes and/or a block for manipulating other "add" related elements (docs and fields) by calling the `xml` method directly:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
doc = {:id=>1, :price=>1.00}
|
|
243
|
+
add_attributes = {:allowDups=>false, :commitWithin=>10}
|
|
244
|
+
add_xml = solr.xml.add(doc, add_attributes) do |doc|
|
|
245
|
+
# boost each document
|
|
246
|
+
doc.attrs[:boost] = 1.5
|
|
247
|
+
# boost the price field:
|
|
248
|
+
doc.field_by_name(:price).attrs[:boost] = 2.0
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Now the "add_xml" object can be sent to Solr like:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
solr.update :data => add_xml
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Deleting
|
|
259
|
+
|
|
260
|
+
Delete by id
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
solr.delete_by_id 1
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
or an array of ids
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
solr.delete_by_id [1, 2, 3, 4]
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Delete by query:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
solr.delete_by_query 'price:1.00'
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Delete by array of queries
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
solr.delete_by_query ['price:1.00', 'price:10.00']
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Commit / Optimize
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
solr.commit, :commit_attributes => {}
|
|
288
|
+
solr.optimize, :optimize_attributes => {}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Response Formats
|
|
292
|
+
|
|
293
|
+
The default response format is Ruby. When the `:wt` param is set to `:ruby`, the response is eval'd resulting in a Hash. You can get a raw response by setting the `:wt` to `"ruby"` - notice, the string -- not a symbol. RSolr will eval the Ruby string ONLY if the :wt value is :ruby. All other response formats are available as expected, `:wt=>'xml'` etc..
|
|
294
|
+
|
|
295
|
+
### Evaluated Ruby:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
solr.get 'select', :params => {:wt => :ruby} # notice :ruby is a Symbol
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Raw Ruby:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
solr.get 'select', :params => {:wt => 'ruby'} # notice 'ruby' is a String
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### XML:
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
solr.get 'select', :params => {:wt => :xml}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### JSON (default):
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
solr.get 'select', :params => {:wt => :json}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Related Resources & Projects
|
|
320
|
+
|
|
321
|
+
* [RSolr Google Group](http://groups.google.com/group/rsolr) -- The RSolr discussion group
|
|
322
|
+
* [rsolr-ext](http://github.com/mwmitchell/rsolr-ext) -- An extension kit for RSolr
|
|
323
|
+
* [rsolr-direct](http://github.com/mwmitchell/rsolr-direct) -- JRuby direct connection for RSolr
|
|
324
|
+
* [rsolr-nokogiri](http://github.com/mwmitchell/rsolr-nokogiri) -- Gives RSolr Nokogiri for XML generation.
|
|
325
|
+
* [SunSpot](http://github.com/sunspot/sunspot) -- An awesome Solr DSL, built with RSolr
|
|
326
|
+
* [Blacklight](http://blacklightopac.org) -- A "next generation" Library OPAC, built with RSolr
|
|
327
|
+
* [java_bin](http://github.com/kennyj/java_bin) -- Provides javabin/binary parsing for RSolr
|
|
328
|
+
* [Solr](http://lucene.apache.org/solr/) -- The Apache Solr project
|
|
329
|
+
* [solr-ruby](http://wiki.apache.org/solr/solr-ruby) -- The original Solr Ruby Gem!
|
|
330
|
+
|
|
331
|
+
## Running the tests
|
|
332
|
+
|
|
333
|
+
The integration specs (`spec/integration`) need a Solr instance;
|
|
334
|
+
run `docker compose up -d` before `bundle exec rspec` or
|
|
335
|
+
`bundle exec rake`.
|
|
336
|
+
|
|
337
|
+
## Note on Patches/Pull Requests
|
|
338
|
+
|
|
339
|
+
* Fork the project.
|
|
340
|
+
* Make your feature addition or bug fix.
|
|
341
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
|
342
|
+
* Commit, do not mess with rakefile, version, or history
|
|
343
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
|
344
|
+
* Send me a pull request. Bonus points for topic branches.
|
|
345
|
+
|
|
346
|
+
## Contributors
|
|
347
|
+
|
|
348
|
+
* Nathan Witmer
|
|
349
|
+
* Magnus Bergmark
|
|
350
|
+
* shima
|
|
351
|
+
* Randy Souza
|
|
352
|
+
* Mat Brown
|
|
353
|
+
* Jeremy Hinegardner
|
|
354
|
+
* Denis Goeury
|
|
355
|
+
* shairon toledo
|
|
356
|
+
* Rob Di Marco
|
|
357
|
+
* Peter Kieltyka
|
|
358
|
+
* Mike Perham
|
|
359
|
+
* Lucas Souza
|
|
360
|
+
* Dmitry Lihachev
|
|
361
|
+
* Antoine Latter
|
|
362
|
+
* Naomi Dushay
|
|
363
|
+
|
|
364
|
+
## Author
|
|
365
|
+
|
|
366
|
+
Matt Mitchell <goodieboy@gmail.com>
|
|
367
|
+
|
|
368
|
+
## Copyright
|
|
369
|
+
|
|
370
|
+
Copyright (c) 2008-2010 Matt Mitchell. See LICENSE for details.
|
data/Rakefile
CHANGED
|
@@ -14,6 +14,6 @@ RDoc::Task.new(:doc) do |rdoc|
|
|
|
14
14
|
rdoc.rdoc_dir = 'doc'
|
|
15
15
|
rdoc.title = 'RSolr'
|
|
16
16
|
rdoc.options << '--line-numbers' << '--inline-source'
|
|
17
|
-
rdoc.rdoc_files.include('README.
|
|
17
|
+
rdoc.rdoc_files.include('README.md')
|
|
18
18
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
19
19
|
end
|
data/docker-compose.yaml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
services:
|
|
2
|
+
solr:
|
|
3
|
+
image: solr:9
|
|
4
|
+
ports:
|
|
5
|
+
- "8983:8983"
|
|
6
|
+
volumes:
|
|
7
|
+
- ./spec/fixtures/basic_configs:/basic_configs_config/conf:ro
|
|
8
|
+
command: ["solr-precreate", "basic_configs", "/basic_configs_config"]
|
|
9
|
+
healthcheck:
|
|
10
|
+
test: ["CMD", "curl", "-sf", "http://localhost:8983/solr/basic_configs/admin/ping"]
|
|
11
|
+
interval: 2s
|
|
12
|
+
timeout: 5s
|
|
13
|
+
retries: 30
|
|
14
|
+
|
|
15
|
+
solr_auth:
|
|
16
|
+
image: solr:9
|
|
17
|
+
ports:
|
|
18
|
+
- "8984:8983"
|
|
19
|
+
volumes:
|
|
20
|
+
- ./spec/fixtures/basic_configs:/basic_configs_config/conf:ro
|
|
21
|
+
- ./spec/fixtures/security/security.json:/security-fixture/security.json:ro
|
|
22
|
+
entrypoint: ["bash", "-c"]
|
|
23
|
+
command:
|
|
24
|
+
- |
|
|
25
|
+
mkdir -p /var/solr/data &&
|
|
26
|
+
cp /security-fixture/security.json /var/solr/data/security.json &&
|
|
27
|
+
exec /opt/solr/docker/scripts/solr-precreate basic_configs /basic_configs_config
|
|
28
|
+
healthcheck:
|
|
29
|
+
test: ["CMD", "curl", "-sf", "-u", "rsolr:RSolrTest123", "http://localhost:8983/solr/basic_configs/admin/ping"]
|
|
30
|
+
interval: 2s
|
|
31
|
+
timeout: 5s
|
|
32
|
+
retries: 30
|
data/lib/rsolr/client.rb
CHANGED
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'faraday'
|
|
5
5
|
require 'uri'
|
|
6
|
+
require 'base64'
|
|
6
7
|
|
|
7
8
|
class RSolr::Client
|
|
9
|
+
include RSolr::Error::URICleanup
|
|
10
|
+
|
|
8
11
|
DEFAULT_URL = 'http://127.0.0.1:8983/solr/'
|
|
9
12
|
|
|
13
|
+
# HTTP Basic Auth credentials, and how to render them as an Authorization header.
|
|
14
|
+
BasicAuth = Struct.new(:user, :password) do
|
|
15
|
+
def present?
|
|
16
|
+
!!(user && password)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def header_value
|
|
20
|
+
"Basic #{Base64.strict_encode64("#{user}:#{password}")}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
10
24
|
class << self
|
|
11
25
|
def default_wt
|
|
12
26
|
@default_wt ||= :json
|
|
@@ -15,6 +29,14 @@ class RSolr::Client
|
|
|
15
29
|
def default_wt= value
|
|
16
30
|
@default_wt = value
|
|
17
31
|
end
|
|
32
|
+
|
|
33
|
+
def default_http_method
|
|
34
|
+
@default_http_method ||= :get
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def default_http_method= method
|
|
38
|
+
@default_http_method = method
|
|
39
|
+
end
|
|
18
40
|
end
|
|
19
41
|
|
|
20
42
|
attr_reader :uri, :proxy, :update_format, :options, :update_path
|
|
@@ -213,11 +235,11 @@ class RSolr::Client
|
|
|
213
235
|
|
|
214
236
|
{ status: response.status.to_i, headers: response.headers, body: response.body.force_encoding('utf-8') }
|
|
215
237
|
rescue Faraday::TimeoutError => e
|
|
216
|
-
raise RSolr::Error::Timeout.new(request_context, e.response)
|
|
238
|
+
raise RSolr::Error::Timeout.new(request_context, e.response), cause: redact_basic_auth(e)
|
|
217
239
|
rescue Errno::ECONNREFUSED, defined?(Faraday::ConnectionFailed) ? Faraday::ConnectionFailed : Faraday::Error::ConnectionFailed
|
|
218
240
|
raise RSolr::Error::ConnectionRefused.new(request_context)
|
|
219
241
|
rescue Faraday::Error => e
|
|
220
|
-
raise RSolr::Error::Http.new(request_context, e.response)
|
|
242
|
+
raise RSolr::Error::Http.new(request_context, e.response), cause: redact_basic_auth(e)
|
|
221
243
|
end
|
|
222
244
|
adapt_response(request_context, raw_response) unless raw_response.nil?
|
|
223
245
|
end
|
|
@@ -240,11 +262,17 @@ class RSolr::Client
|
|
|
240
262
|
raise "path must be a string or symbol, not #{path.inspect}" unless [String,Symbol].include?(path.class)
|
|
241
263
|
path = path.to_s
|
|
242
264
|
opts[:proxy] = proxy unless proxy.nil?
|
|
243
|
-
opts[:method] ||=
|
|
265
|
+
opts[:method] ||= default_http_method
|
|
244
266
|
raise "The :data option can only be used if :method => :post" if opts[:method] != :post and opts[:data]
|
|
245
267
|
opts[:params] = params_with_wt(opts[:params])
|
|
246
|
-
|
|
247
|
-
|
|
268
|
+
if opts[:method] == :post && (opts[:data].nil? || opts[:data].empty?)
|
|
269
|
+
# If no data hash is provided move query into data hash
|
|
270
|
+
# This is useful if the query string is larger than the max url length
|
|
271
|
+
opts[:data] = opts[:params]
|
|
272
|
+
else
|
|
273
|
+
query = RSolr::Uri.params_to_solr(opts[:params]) unless opts[:params].empty?
|
|
274
|
+
opts[:query] = query
|
|
275
|
+
end
|
|
248
276
|
if opts[:data].is_a? Hash
|
|
249
277
|
opts[:data] = RSolr::Uri.params_to_solr opts[:data]
|
|
250
278
|
opts[:headers] ||= {}
|
|
@@ -314,28 +342,48 @@ class RSolr::Client
|
|
|
314
342
|
conn_opts[:request][:params_encoder] = Faraday::FlatParamsEncoder
|
|
315
343
|
|
|
316
344
|
Faraday.new(conn_opts) do |conn|
|
|
317
|
-
|
|
318
|
-
case Faraday::VERSION
|
|
319
|
-
when /^0/
|
|
320
|
-
conn.basic_auth uri.user, uri.password
|
|
321
|
-
when /^1/
|
|
322
|
-
conn.request :basic_auth, uri.user, uri.password
|
|
323
|
-
else
|
|
324
|
-
conn.request :authorization, :basic_auth, uri.user, uri.password
|
|
325
|
-
end
|
|
326
|
-
end
|
|
345
|
+
conn.headers['Authorization'] = basic_auth.header_value if basic_auth.present?
|
|
327
346
|
|
|
328
347
|
conn.response :raise_error
|
|
329
348
|
conn.request :retry, max: options[:retry_after_limit], interval: 0.05,
|
|
330
349
|
interval_randomness: 0.5, backoff_factor: 2,
|
|
331
350
|
exceptions: ['Faraday::Error', 'Timeout::Error'] if options[:retry_503]
|
|
332
|
-
conn.adapter
|
|
351
|
+
conn.adapter(*Array(options[:adapter] || Faraday.default_adapter || :net_http))
|
|
333
352
|
end
|
|
334
353
|
end
|
|
335
354
|
end
|
|
336
355
|
|
|
337
356
|
protected
|
|
338
357
|
|
|
358
|
+
def basic_auth
|
|
359
|
+
if options[:basic_auth]
|
|
360
|
+
auth = options[:basic_auth]
|
|
361
|
+
BasicAuth.new(auth[:user] || auth['user'], auth[:password] || auth['password'])
|
|
362
|
+
else
|
|
363
|
+
BasicAuth.new(uri.user, uri.password)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Faraday's own exception (kept as +cause+) embeds the request unredacted; rebuild it with just the credentials scrubbed.
|
|
368
|
+
def redact_basic_auth(error)
|
|
369
|
+
response = error.response
|
|
370
|
+
return error unless response.is_a?(Hash) && response[:request].is_a?(Hash)
|
|
371
|
+
|
|
372
|
+
request = response[:request].dup
|
|
373
|
+
request[:url] = clean_uri(request[:url]) if request[:url].respond_to?(:user)
|
|
374
|
+
request[:headers] = redact_authorization_header(request[:headers]) if request[:headers]
|
|
375
|
+
|
|
376
|
+
error.class.new(response.merge(request: request))
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def redact_authorization_header(headers)
|
|
380
|
+
return headers unless headers['Authorization']
|
|
381
|
+
|
|
382
|
+
headers = headers.dup
|
|
383
|
+
headers['Authorization'] = 'REDACTED'
|
|
384
|
+
headers
|
|
385
|
+
end
|
|
386
|
+
|
|
339
387
|
# converts the method name for the solr request handler path.
|
|
340
388
|
def method_missing name, *args
|
|
341
389
|
if name.to_s =~ /^paginated?_(.+)$/
|
|
@@ -369,4 +417,8 @@ class RSolr::Client
|
|
|
369
417
|
def default_wt
|
|
370
418
|
self.options[:default_wt] || self.class.default_wt
|
|
371
419
|
end
|
|
420
|
+
|
|
421
|
+
def default_http_method
|
|
422
|
+
self.options[:default_http_method] || self.class.default_http_method
|
|
423
|
+
end
|
|
372
424
|
end
|
data/lib/rsolr/version.rb
CHANGED
data/rsolr.gemspec
CHANGED
|
@@ -33,7 +33,7 @@ Gem::Specification.new do |s|
|
|
|
33
33
|
s.requirements << 'Apache Solr'
|
|
34
34
|
|
|
35
35
|
s.add_dependency 'builder', '>= 2.1.2'
|
|
36
|
-
s.add_dependency 'faraday', '>=
|
|
36
|
+
s.add_dependency 'faraday', '>= 1.0', '< 3'
|
|
37
37
|
|
|
38
38
|
s.add_development_dependency 'activesupport'
|
|
39
39
|
s.add_development_dependency 'nokogiri', '>= 1.4.0'
|
|
@@ -41,5 +41,4 @@ Gem::Specification.new do |s|
|
|
|
41
41
|
s.add_development_dependency 'rdoc', '>= 4.0'
|
|
42
42
|
s.add_development_dependency 'rspec', '~> 3.0'
|
|
43
43
|
s.add_development_dependency 'simplecov'
|
|
44
|
-
s.add_development_dependency 'solr_wrapper'
|
|
45
44
|
end
|
data/spec/api/client_spec.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
|
+
require 'base64'
|
|
2
3
|
|
|
3
4
|
RSpec.describe RSolr::Client do
|
|
4
5
|
let(:connection) { nil }
|
|
@@ -13,6 +14,106 @@ RSpec.describe RSolr::Client do
|
|
|
13
14
|
RSolr::Client.new connection, connection_options.merge(proxy: 'http://localhost:8080')
|
|
14
15
|
end
|
|
15
16
|
|
|
17
|
+
context "basic auth" do
|
|
18
|
+
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
|
|
19
|
+
|
|
20
|
+
def authorization_header_for(url:, **client_opts)
|
|
21
|
+
seen_header = nil
|
|
22
|
+
stubs.get('/solr/admin/ping') do |env|
|
|
23
|
+
seen_header = env.request_headers['Authorization']
|
|
24
|
+
[200, {}, '']
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
RSolr::Client.new(nil, { url: url, adapter: [:test, stubs] }.merge(client_opts)).get('admin/ping')
|
|
28
|
+
stubs.verify_stubbed_calls
|
|
29
|
+
seen_header
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "sends credentials embedded in the URL as a Basic Authorization header" do
|
|
33
|
+
header = authorization_header_for(url: "http://someuser:somepass@localhost:9999/solr")
|
|
34
|
+
expect(header).to eq("Basic #{Base64.strict_encode64('someuser:somepass')}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "sends no Authorization header when the URL has no credentials" do
|
|
38
|
+
header = authorization_header_for(url: "http://localhost:9999/solr")
|
|
39
|
+
expect(header).to be_nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "sends credentials given via the basic_auth: option" do
|
|
43
|
+
header = authorization_header_for(
|
|
44
|
+
url: "http://localhost:9999/solr",
|
|
45
|
+
basic_auth: { user: 'optuser', password: 'optpass' }
|
|
46
|
+
)
|
|
47
|
+
expect(header).to eq("Basic #{Base64.strict_encode64('optuser:optpass')}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "prefers the basic_auth: option over credentials embedded in the URL" do
|
|
51
|
+
header = authorization_header_for(
|
|
52
|
+
url: "http://urluser:urlpass@localhost:9999/solr",
|
|
53
|
+
basic_auth: { user: 'optuser', password: 'optpass' }
|
|
54
|
+
)
|
|
55
|
+
expect(header).to eq("Basic #{Base64.strict_encode64('optuser:optpass')}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "accepts a basic_auth: option with string keys, as loaded from YAML" do
|
|
59
|
+
header = authorization_header_for(
|
|
60
|
+
url: "http://localhost:9999/solr",
|
|
61
|
+
basic_auth: { 'user' => 'optuser', 'password' => 'optpass' }
|
|
62
|
+
)
|
|
63
|
+
expect(header).to eq("Basic #{Base64.strict_encode64('optuser:optpass')}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'when a connection error is raised' do
|
|
67
|
+
def error_raised_for(url:, **client_opts)
|
|
68
|
+
stubs.get('/solr/admin/ping') { |_env| [500, {}, 'boom'] }
|
|
69
|
+
RSolr::Client.new(nil, { url: url, adapter: [:test, stubs] }.merge(client_opts)).get('admin/ping')
|
|
70
|
+
nil
|
|
71
|
+
rescue RSolr::Error::Http => e
|
|
72
|
+
e
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'with credentials in url' do
|
|
76
|
+
it 'already redacts the url in RSolr::Error::Http#message' do
|
|
77
|
+
error = error_raised_for(url: "http://someuser:somepass@localhost:9999/solr")
|
|
78
|
+
|
|
79
|
+
expect(error.message).not_to include('somepass')
|
|
80
|
+
expect(error.message).to include('REDACTED')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'does not leak basic auth info' do
|
|
84
|
+
error = error_raised_for(url: "http://someuser:somepass@localhost:9999/solr")
|
|
85
|
+
|
|
86
|
+
expect(error.cause.message).not_to include('somepass')
|
|
87
|
+
expect(error.cause.inspect).not_to include('somepass')
|
|
88
|
+
expect(error.cause.inspect).to include('REDACTED')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context 'with credentials in header' do
|
|
93
|
+
it 'already omits credentials from RSolr::Error::Http#message' do
|
|
94
|
+
error = error_raised_for(
|
|
95
|
+
url: "http://localhost:9999/solr",
|
|
96
|
+
basic_auth: { user: 'optuser', password: 'optpass' }
|
|
97
|
+
)
|
|
98
|
+
encoded_credentials = Base64.strict_encode64('optuser:optpass')
|
|
99
|
+
|
|
100
|
+
expect(error.message).not_to include(encoded_credentials)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'does not leak basic auth info' do
|
|
104
|
+
error = error_raised_for(
|
|
105
|
+
url: "http://localhost:9999/solr",
|
|
106
|
+
basic_auth: { user: 'optuser', password: 'optpass' }
|
|
107
|
+
)
|
|
108
|
+
encoded_credentials = Base64.strict_encode64('optuser:optpass')
|
|
109
|
+
|
|
110
|
+
expect(error.cause.inspect).not_to include(encoded_credentials)
|
|
111
|
+
expect(error.cause.inspect).to include('REDACTED')
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
16
117
|
context "initialize" do
|
|
17
118
|
it "should accept whatevs and set it as the @connection" do
|
|
18
119
|
expect(RSolr::Client.new(:whatevs).connection).to eq(:whatevs)
|
|
@@ -92,7 +193,7 @@ RSpec.describe RSolr::Client do
|
|
|
92
193
|
it "removes credentials from uri" do
|
|
93
194
|
expect {
|
|
94
195
|
client.execute({ uri: uri })
|
|
95
|
-
}.to raise_error(RSolr::Error::ConnectionRefused, /http:\/\/REDACTED:REDACTED@hostname\.local:8983/)
|
|
196
|
+
}.to raise_error(RSolr::Error::ConnectionRefused, /http:\/\/(?:REDACTED:)?REDACTED@hostname\.local:8983/)
|
|
96
197
|
end
|
|
97
198
|
end
|
|
98
199
|
end
|
|
@@ -411,6 +512,18 @@ RSpec.describe RSolr::Client do
|
|
|
411
512
|
end
|
|
412
513
|
end
|
|
413
514
|
|
|
515
|
+
context "when post but no hash is passed in as data" do
|
|
516
|
+
let(:data) { nil }
|
|
517
|
+
|
|
518
|
+
it "sets the Content-Type header to application/x-www-form-urlencoded; charset=UTF-8" do
|
|
519
|
+
expect(subject[:query]).to be_nil
|
|
520
|
+
[/fq=0/, /fq=1/, /q=test/, /wt=json/].each do |pattern|
|
|
521
|
+
expect(subject[:data]).to match pattern
|
|
522
|
+
end
|
|
523
|
+
expect(subject[:headers]).to eq({"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8"})
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
414
527
|
it "should properly handle proxy configuration" do
|
|
415
528
|
result = client_with_proxy.build_request('select',
|
|
416
529
|
:method => :post,
|
|
@@ -419,5 +532,24 @@ RSpec.describe RSolr::Client do
|
|
|
419
532
|
)
|
|
420
533
|
expect(result[:uri].to_s).to match %r{^http://localhost:9999/solr/}
|
|
421
534
|
end
|
|
535
|
+
|
|
536
|
+
context "http method" do
|
|
537
|
+
it "uses get by default" do
|
|
538
|
+
expect(client.build_request('select', { params: params })[:method]).to eq :get
|
|
539
|
+
end
|
|
540
|
+
it "uses the instance default if set" do
|
|
541
|
+
client = RSolr::Client.new connection, connection_options.merge(default_http_method: :post)
|
|
542
|
+
expect(client.build_request('select', { data: params })[:method]).to eq :post
|
|
543
|
+
end
|
|
544
|
+
it "uses the class default if set" do
|
|
545
|
+
RSolr::Client.default_http_method = :post
|
|
546
|
+
expect(client.build_request('select', { data: params })[:method]).to eq :post
|
|
547
|
+
RSolr::Client.default_http_method = nil
|
|
548
|
+
end
|
|
549
|
+
it "uses the method passed in opts" do
|
|
550
|
+
expect(client.build_request('select', { params: params, method: :get })[:method]).to eq :get
|
|
551
|
+
expect(client.build_request('select', { data: params, method: :post })[:method]).to eq :post
|
|
552
|
+
end
|
|
553
|
+
end
|
|
422
554
|
end
|
|
423
555
|
end
|
data/spec/api/error_spec.rb
CHANGED
|
@@ -148,11 +148,13 @@ RSpec.describe RSolr::Error do
|
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
context "when request uri contains credentials" do
|
|
151
|
-
let(:request) { { uri: URI.parse('http://
|
|
151
|
+
let(:request) { { uri: URI.parse('http://user:pass@hostname.local:8983/solr/admin/update?wt=json&q=test') } }
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
it 'includes redacted url' do
|
|
155
|
-
expect(subject).to include '
|
|
155
|
+
expect(subject).to include 'REDACTED@hostname.local:8983/solr/admin/update?wt=json&q=test'
|
|
156
|
+
expect(subject).not_to include 'user'
|
|
157
|
+
expect(subject).not_to include 'pass'
|
|
156
158
|
end
|
|
157
159
|
end
|
|
158
160
|
end
|
data/spec/api/json_spec.rb
CHANGED
|
@@ -68,32 +68,10 @@ RSpec.describe RSolr::JSON do
|
|
|
68
68
|
},
|
|
69
69
|
]
|
|
70
70
|
|
|
71
|
-
# custom JSON object class to handle Solr's non-standard JSON command format
|
|
72
|
-
tmp = Class.new do
|
|
73
|
-
def initialize
|
|
74
|
-
@source ||= {}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def []=(k, v)
|
|
78
|
-
if k == :add
|
|
79
|
-
@source[k] ||= []
|
|
80
|
-
@source[k] << v.to_h
|
|
81
|
-
elsif v.class == self.class
|
|
82
|
-
@source[k] = v.to_h
|
|
83
|
-
else
|
|
84
|
-
@source[k] = v
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def to_h
|
|
89
|
-
@source
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
71
|
request = generator.add(data, boost: 1)
|
|
94
|
-
|
|
95
|
-
expect(
|
|
96
|
-
expect(
|
|
72
|
+
|
|
73
|
+
expect(request).to match(/"add":{"boost":1,"doc":{"id":"1","name":"matt"}}/)
|
|
74
|
+
expect(request).to match(/"add":{"boost":1,"doc":{"id":"2","name":"sam"}}/)
|
|
97
75
|
end
|
|
98
76
|
end
|
|
99
77
|
|
|
@@ -498,7 +498,7 @@
|
|
|
498
498
|
<fieldType name="point" class="solr.PointType" dimension="2" subFieldSuffix="_d"/>
|
|
499
499
|
|
|
500
500
|
<!-- A specialized field for geospatial search. If indexed, this fieldType must not be multivalued. -->
|
|
501
|
-
<fieldType name="location" class="solr.
|
|
501
|
+
<fieldType name="location" class="solr.LatLonPointSpatialField"/>
|
|
502
502
|
|
|
503
503
|
<!-- An alternative geospatial field type new to Solr 4. It supports multiValued and polygon shapes.
|
|
504
504
|
For more information about this and other Spatial fields new to Solr 4, see:
|
|
@@ -315,8 +315,8 @@
|
|
|
315
315
|
background concurrently. An error is returned if this limit
|
|
316
316
|
is exceeded.
|
|
317
317
|
|
|
318
|
-
Recommend values of 1-2 for read-only
|
|
319
|
-
|
|
318
|
+
Recommend values of 1-2 for read-only followers, higher for
|
|
319
|
+
leaders w/o cache warming.
|
|
320
320
|
-->
|
|
321
321
|
<maxWarmingSearchers>2</maxWarmingSearchers>
|
|
322
322
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"authentication": {
|
|
3
|
+
"blockUnknown": true,
|
|
4
|
+
"class": "solr.BasicAuthPlugin",
|
|
5
|
+
"credentials": {
|
|
6
|
+
"rsolr": "yBxMT8Js4iwRuqWHqW7eQcF1iNDaSbI8ZQtQc5nZyYg= 57WDp52Vgp2LrB9BT5/iFnGY7txUMdTlyM9rVT9XwcA="
|
|
7
|
+
},
|
|
8
|
+
"realm": "rsolr test",
|
|
9
|
+
"forwardCredentials": false
|
|
10
|
+
},
|
|
11
|
+
"authorization": {
|
|
12
|
+
"class": "solr.RuleBasedAuthorizationPlugin",
|
|
13
|
+
"permissions": [{ "name": "security-edit", "role": "admin" }],
|
|
14
|
+
"user-role": { "rsolr": "admin" }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe "Solr basic auth" do
|
|
4
|
+
let(:port) { ENV.fetch('SOLR_AUTH_TEST_PORT', 8984) }
|
|
5
|
+
let(:base_url) { "http://localhost:#{port}/solr/basic_configs/" }
|
|
6
|
+
let(:user) { 'rsolr' }
|
|
7
|
+
let(:password) { 'RSolrTest123' }
|
|
8
|
+
|
|
9
|
+
it "succeeds using the basic_auth: option" do
|
|
10
|
+
solr = RSolr.connect(url: base_url, basic_auth: { user: user, password: password })
|
|
11
|
+
expect { solr.head('admin/ping') }.not_to raise_error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "succeeds using credentials embedded in the URL" do
|
|
15
|
+
url = "http://#{user}:#{password}@localhost:#{port}/solr/basic_configs/"
|
|
16
|
+
solr = RSolr.connect(url: url)
|
|
17
|
+
expect { solr.head('admin/ping') }.not_to raise_error
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "raises RSolr::Error::Http with no credentials" do
|
|
21
|
+
solr = RSolr.connect(url: base_url)
|
|
22
|
+
expect { solr.head('admin/ping') }.to raise_error(RSolr::Error::Http, /401/)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "raises RSolr::Error::Http with the wrong credentials" do
|
|
26
|
+
solr = RSolr.connect(url: base_url, basic_auth: { user: user, password: 'wrong' })
|
|
27
|
+
expect { solr.head('admin/ping') }.to raise_error(RSolr::Error::Http, /401/)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
|
-
require 'solr_wrapper'
|
|
3
2
|
|
|
4
3
|
RSpec.describe "Solr basic_configs" do
|
|
5
|
-
SolrWrapper.default_instance_options = {
|
|
6
|
-
port: SolrWrapper.default_solr_port,
|
|
7
|
-
version: '8.11.3'
|
|
8
|
-
}
|
|
9
|
-
SOLR_INSTANCE = SolrWrapper.default_instance({})
|
|
10
|
-
before(:all) { SOLR_INSTANCE.start }
|
|
11
|
-
after(:all) { SOLR_INSTANCE.stop }
|
|
12
|
-
|
|
13
4
|
context "basic configs" do
|
|
14
|
-
subject { RSolr.connect url: "http://localhost:#{
|
|
15
|
-
|
|
16
|
-
SOLR_INSTANCE.with_collection(name: "basic_configs", dir: File.join(FIXTURES_DIR, "basic_configs")) do |coll|
|
|
17
|
-
example.run
|
|
18
|
-
end
|
|
19
|
-
end
|
|
5
|
+
subject { RSolr.connect url: "http://localhost:#{ENV.fetch('SOLR_TEST_PORT', 8983)}/solr/basic_configs/" }
|
|
6
|
+
|
|
20
7
|
describe "HEAD admin/ping" do
|
|
21
8
|
it "should not raise an exception" do
|
|
22
9
|
expect { subject.head('admin/ping') }.not_to raise_error
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rsolr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antoine Latter
|
|
@@ -26,10 +26,9 @@ authors:
|
|
|
26
26
|
- Nathan Witmer
|
|
27
27
|
- Naomi Dushay
|
|
28
28
|
- '"shima"'
|
|
29
|
-
autorequire:
|
|
30
29
|
bindir: bin
|
|
31
30
|
cert_chain: []
|
|
32
|
-
date:
|
|
31
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
33
32
|
dependencies:
|
|
34
33
|
- !ruby/object:Gem::Dependency
|
|
35
34
|
name: builder
|
|
@@ -51,10 +50,7 @@ dependencies:
|
|
|
51
50
|
requirements:
|
|
52
51
|
- - ">="
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '0
|
|
55
|
-
- - "!="
|
|
56
|
-
- !ruby/object:Gem::Version
|
|
57
|
-
version: 2.0.0
|
|
53
|
+
version: '1.0'
|
|
58
54
|
- - "<"
|
|
59
55
|
- !ruby/object:Gem::Version
|
|
60
56
|
version: '3'
|
|
@@ -64,10 +60,7 @@ dependencies:
|
|
|
64
60
|
requirements:
|
|
65
61
|
- - ">="
|
|
66
62
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0
|
|
68
|
-
- - "!="
|
|
69
|
-
- !ruby/object:Gem::Version
|
|
70
|
-
version: 2.0.0
|
|
63
|
+
version: '1.0'
|
|
71
64
|
- - "<"
|
|
72
65
|
- !ruby/object:Gem::Version
|
|
73
66
|
version: '3'
|
|
@@ -155,20 +148,6 @@ dependencies:
|
|
|
155
148
|
- - ">="
|
|
156
149
|
- !ruby/object:Gem::Version
|
|
157
150
|
version: '0'
|
|
158
|
-
- !ruby/object:Gem::Dependency
|
|
159
|
-
name: solr_wrapper
|
|
160
|
-
requirement: !ruby/object:Gem::Requirement
|
|
161
|
-
requirements:
|
|
162
|
-
- - ">="
|
|
163
|
-
- !ruby/object:Gem::Version
|
|
164
|
-
version: '0'
|
|
165
|
-
type: :development
|
|
166
|
-
prerelease: false
|
|
167
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
168
|
-
requirements:
|
|
169
|
-
- - ">="
|
|
170
|
-
- !ruby/object:Gem::Version
|
|
171
|
-
version: '0'
|
|
172
151
|
description: RSolr aims to provide a simple and extensible library for working with
|
|
173
152
|
Solr
|
|
174
153
|
email:
|
|
@@ -183,8 +162,9 @@ files:
|
|
|
183
162
|
- CHANGES.txt
|
|
184
163
|
- Gemfile
|
|
185
164
|
- LICENSE
|
|
186
|
-
- README.
|
|
165
|
+
- README.md
|
|
187
166
|
- Rakefile
|
|
167
|
+
- docker-compose.yaml
|
|
188
168
|
- lib/rsolr.rb
|
|
189
169
|
- lib/rsolr/char.rb
|
|
190
170
|
- lib/rsolr/client.rb
|
|
@@ -214,6 +194,8 @@ files:
|
|
|
214
194
|
- spec/fixtures/basic_configs/solrconfig.xml
|
|
215
195
|
- spec/fixtures/basic_configs/stopwords.txt
|
|
216
196
|
- spec/fixtures/basic_configs/synonyms.txt
|
|
197
|
+
- spec/fixtures/security/security.json
|
|
198
|
+
- spec/integration/basic_auth_spec.rb
|
|
217
199
|
- spec/integration/solr5_spec.rb
|
|
218
200
|
- spec/lib/rsolr/client_spec.rb
|
|
219
201
|
- spec/spec_helper.rb
|
|
@@ -221,7 +203,6 @@ homepage: https://github.com/rsolr/rsolr
|
|
|
221
203
|
licenses:
|
|
222
204
|
- Apache-2.0
|
|
223
205
|
metadata: {}
|
|
224
|
-
post_install_message:
|
|
225
206
|
rdoc_options: []
|
|
226
207
|
require_paths:
|
|
227
208
|
- lib
|
|
@@ -237,8 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
237
218
|
version: '0'
|
|
238
219
|
requirements:
|
|
239
220
|
- Apache Solr
|
|
240
|
-
rubygems_version:
|
|
241
|
-
signing_key:
|
|
221
|
+
rubygems_version: 4.0.18
|
|
242
222
|
specification_version: 4
|
|
243
223
|
summary: A Ruby client for Apache Solr
|
|
244
224
|
test_files: []
|
data/README.rdoc
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
=RSolr
|
|
2
|
-
|
|
3
|
-
A simple, extensible Ruby client for Apache Solr.
|
|
4
|
-
|
|
5
|
-
==Documentation
|
|
6
|
-
The code docs http://www.rubydoc.info/gems/rsolr
|
|
7
|
-
|
|
8
|
-
== Installation:
|
|
9
|
-
gem install rsolr
|
|
10
|
-
|
|
11
|
-
== Example:
|
|
12
|
-
require 'rsolr'
|
|
13
|
-
|
|
14
|
-
# Direct connection
|
|
15
|
-
solr = RSolr.connect :url => 'http://solrserver.com'
|
|
16
|
-
|
|
17
|
-
# Connecting over a proxy server
|
|
18
|
-
solr = RSolr.connect :url => 'http://solrserver.com', :proxy=>'http://user:pass@proxy.example.com:8080'
|
|
19
|
-
|
|
20
|
-
# Using an alternate Faraday adapter
|
|
21
|
-
solr = RSolr.connect :url => 'http://solrserver.com', :adapter => :em_http
|
|
22
|
-
|
|
23
|
-
# Using a custom Faraday connection
|
|
24
|
-
conn = Faraday.new do |faraday|
|
|
25
|
-
faraday.response :logger # log requests to STDOUT
|
|
26
|
-
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
|
27
|
-
end
|
|
28
|
-
solr = RSolr.connect conn, :url => 'http://solrserver.com'
|
|
29
|
-
|
|
30
|
-
# send a request to /select
|
|
31
|
-
response = solr.get 'select', :params => {:q => '*:*'}
|
|
32
|
-
|
|
33
|
-
# send a request to /catalog
|
|
34
|
-
response = solr.get 'catalog', :params => {:q => '*:*'}
|
|
35
|
-
|
|
36
|
-
When the Solr +:wt+ is +:ruby+, then the response will be a Hash. This Hash is the same object returned by Solr, but evaluated as Ruby. If the +:wt+ is not +:ruby+, then the response will be a String.
|
|
37
|
-
|
|
38
|
-
The response also exposes 2 attribute readers (for any +:wt+ value), +:request+ and +:response+. Both are Hash objects with symbolized keys.
|
|
39
|
-
|
|
40
|
-
The +:request+ attribute contains the original request context. You can use this for debugging or logging. Some of the keys this object contains are +:uri+, +:query+, +:method+ etc..
|
|
41
|
-
|
|
42
|
-
The +:response+ attribute contains the original response. This object contains the +:status+, +:body+ and +:headers+ keys.
|
|
43
|
-
|
|
44
|
-
== Request formats
|
|
45
|
-
|
|
46
|
-
By default, RSolr uses the Solr JSON command format for all requests.
|
|
47
|
-
|
|
48
|
-
RSolr.connect :url => 'http://solrserver.com', update_format: :json # the default
|
|
49
|
-
# or
|
|
50
|
-
RSolr.connect :url => 'http://solrserver.com', update_format: :xml
|
|
51
|
-
|
|
52
|
-
== Timeouts
|
|
53
|
-
The read and connect timeout settings can be set when creating a new instance of RSolr, and will
|
|
54
|
-
be passed on to underlying Faraday instance:
|
|
55
|
-
|
|
56
|
-
solr = RSolr.connect(:timeout => 120, :open_timeout => 120)
|
|
57
|
-
|
|
58
|
-
== Retry 503s
|
|
59
|
-
A 503 is usually a temporary error which RSolr may retry if requested. You may specify the number of retry attempts with the +:retry_503+ option.
|
|
60
|
-
|
|
61
|
-
Only requests which specify a Retry-After header will be retried, after waiting the indicated retry interval, otherwise RSolr will treat the request as a 500. You may specify a maximum Retry-After interval to wait with the +:retry_after_limit+ option (default: one second).
|
|
62
|
-
solr = RSolr.connect(:retry_503 => 1, :retry_after_limit => 1)
|
|
63
|
-
|
|
64
|
-
For additional control, consider using a custom Faraday connection (see above) using its `retry` middleware.
|
|
65
|
-
|
|
66
|
-
== Querying
|
|
67
|
-
Use the #get / #post method to send search requests to the /select handler:
|
|
68
|
-
response = solr.get 'select', :params => {
|
|
69
|
-
:q=>'washington',
|
|
70
|
-
:start=>0,
|
|
71
|
-
:rows=>10
|
|
72
|
-
}
|
|
73
|
-
response["response"]["docs"].each{|doc| puts doc["id"] }
|
|
74
|
-
|
|
75
|
-
The +:params+ sent into the method are sent to Solr as-is, which is to say they are converted to Solr url style, but no special mapping is used.
|
|
76
|
-
When an array is used, multiple parameters *with the same name* are generated for the Solr query. Example:
|
|
77
|
-
|
|
78
|
-
solr.get 'select', :params => {:q=>'roses', :fq=>['red', 'violet']}
|
|
79
|
-
|
|
80
|
-
The above statement generates this Solr query:
|
|
81
|
-
|
|
82
|
-
select?q=roses&fq=red&fq=violet
|
|
83
|
-
|
|
84
|
-
===Pagination
|
|
85
|
-
To paginate through a set of Solr documents, use the paginate method:
|
|
86
|
-
solr.paginate 1, 10, "select", :params => {:q => "test"}
|
|
87
|
-
|
|
88
|
-
The first argument is the current page, the second is how many documents to return for each page. In other words, "page" is the "start" Solr param and "per-page" is the "rows" Solr param.
|
|
89
|
-
|
|
90
|
-
The paginate method returns WillPaginate ready "docs" objects, so for example in a Rails application, paginating is as simple as:
|
|
91
|
-
<%= will_paginate @solr_response["response"]["docs"] %>
|
|
92
|
-
|
|
93
|
-
===Method Missing
|
|
94
|
-
The +RSolr::Client+ class also uses +method_missing+ for setting the request handler/path:
|
|
95
|
-
|
|
96
|
-
solr.paintings :params => {:q=>'roses', :fq=>['red', 'violet']}
|
|
97
|
-
|
|
98
|
-
This is sent to Solr as:
|
|
99
|
-
paintings?q=roses&fq=red&fq=violet
|
|
100
|
-
|
|
101
|
-
This works with pagination as well:
|
|
102
|
-
|
|
103
|
-
solr.paginate_paintings 1, 10, {:q=>'roses', :fq=>['red', 'violet']}
|
|
104
|
-
|
|
105
|
-
===Using POST for Search Queries
|
|
106
|
-
There may be cases where the query string is too long for a GET request. RSolr solves this issue by converting hash objects into form-encoded strings:
|
|
107
|
-
response = solr.music :data => {:q => "*:*"}
|
|
108
|
-
|
|
109
|
-
The +:data+ hash is serialized as a form-encoded query string, and the correct content-type headers are sent along to Solr.
|
|
110
|
-
|
|
111
|
-
===Sending HEAD Requests
|
|
112
|
-
There may be cases where you'd like to send a HEAD request to Solr:
|
|
113
|
-
solr.head("admin/ping").response[:status] == 200
|
|
114
|
-
|
|
115
|
-
==Sending HTTP Headers
|
|
116
|
-
Solr responds to the request headers listed here: http://wiki.apache.org/solr/SolrAndHTTPCaches
|
|
117
|
-
To send header information to Solr using RSolr, just use the +:headers+ option:
|
|
118
|
-
response = solr.head "admin/ping", :headers => {"Cache-Control" => "If-None-Match"}
|
|
119
|
-
|
|
120
|
-
===Building a Request
|
|
121
|
-
+RSolr::Client+ provides a method for building a request context, which can be useful for debugging or logging etc.:
|
|
122
|
-
request_context = solr.build_request "select", :data => {:q => "*:*"}, :method => :post, :headers => {}
|
|
123
|
-
|
|
124
|
-
To build a paginated request use build_paginated_request:
|
|
125
|
-
request_context = solr.build_paginated_request 1, 10, "select", ...
|
|
126
|
-
|
|
127
|
-
== Updating Solr
|
|
128
|
-
Updating is done using native Ruby objects. Hashes are used for single documents and arrays are used for a collection of documents (hashes). These objects get turned into simple XML "messages". Raw XML strings can also be used.
|
|
129
|
-
|
|
130
|
-
Single document via #add
|
|
131
|
-
solr.add :id=>1, :price=>1.00
|
|
132
|
-
|
|
133
|
-
Multiple documents via #add
|
|
134
|
-
documents = [{:id=>1, :price=>1.00}, {:id=>2, :price=>10.50}]
|
|
135
|
-
solr.add documents
|
|
136
|
-
|
|
137
|
-
The optional +:add_attributes+ hash can also be used to set Solr "add" document attributes:
|
|
138
|
-
solr.add documents, :add_attributes => {:commitWithin => 10}
|
|
139
|
-
|
|
140
|
-
Raw commands via #update
|
|
141
|
-
solr.update data: '<commit/>', headers: { 'Content-Type' => 'text/xml' }
|
|
142
|
-
solr.update data: { optimize: true }.to_json, headers: { 'Content-Type' => 'application/json' }
|
|
143
|
-
|
|
144
|
-
When adding, you can also supply "add" xml element attributes and/or a block for manipulating other "add" related elements (docs and fields) by calling the +xml+ method directly:
|
|
145
|
-
|
|
146
|
-
doc = {:id=>1, :price=>1.00}
|
|
147
|
-
add_attributes = {:allowDups=>false, :commitWithin=>10}
|
|
148
|
-
add_xml = solr.xml.add(doc, add_attributes) do |doc|
|
|
149
|
-
# boost each document
|
|
150
|
-
doc.attrs[:boost] = 1.5
|
|
151
|
-
# boost the price field:
|
|
152
|
-
doc.field_by_name(:price).attrs[:boost] = 2.0
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
Now the "add_xml" object can be sent to Solr like:
|
|
156
|
-
solr.update :data => add_xml
|
|
157
|
-
|
|
158
|
-
===Deleting
|
|
159
|
-
Delete by id
|
|
160
|
-
solr.delete_by_id 1
|
|
161
|
-
or an array of ids
|
|
162
|
-
solr.delete_by_id [1, 2, 3, 4]
|
|
163
|
-
|
|
164
|
-
Delete by query:
|
|
165
|
-
solr.delete_by_query 'price:1.00'
|
|
166
|
-
Delete by array of queries
|
|
167
|
-
solr.delete_by_query ['price:1.00', 'price:10.00']
|
|
168
|
-
|
|
169
|
-
===Commit / Optimize
|
|
170
|
-
solr.commit, :commit_attributes => {}
|
|
171
|
-
solr.optimize, :optimize_attributes => {}
|
|
172
|
-
|
|
173
|
-
== Response Formats
|
|
174
|
-
The default response format is Ruby. When the +:wt+ param is set to +:ruby+, the response is eval'd resulting in a Hash. You can get a raw response by setting the +:wt+ to +"ruby"+ - notice, the string -- not a symbol. RSolr will eval the Ruby string ONLY if the :wt value is :ruby. All other response formats are available as expected, +:wt=>'xml'+ etc..
|
|
175
|
-
|
|
176
|
-
===Evaluated Ruby:
|
|
177
|
-
solr.get 'select', :params => {:wt => :ruby} # notice :ruby is a Symbol
|
|
178
|
-
===Raw Ruby:
|
|
179
|
-
solr.get 'select', :params => {:wt => 'ruby'} # notice 'ruby' is a String
|
|
180
|
-
===XML:
|
|
181
|
-
solr.get 'select', :params => {:wt => :xml}
|
|
182
|
-
===JSON (default):
|
|
183
|
-
solr.get 'select', :params => {:wt => :json}
|
|
184
|
-
|
|
185
|
-
==Related Resources & Projects
|
|
186
|
-
* {RSolr Google Group}[http://groups.google.com/group/rsolr] -- The RSolr discussion group
|
|
187
|
-
* {rsolr-ext}[http://github.com/mwmitchell/rsolr-ext] -- An extension kit for RSolr
|
|
188
|
-
* {rsolr-direct}[http://github.com/mwmitchell/rsolr-direct] -- JRuby direct connection for RSolr
|
|
189
|
-
* {rsolr-nokogiri}[http://github.com/mwmitchell/rsolr-nokogiri] -- Gives RSolr Nokogiri for XML generation.
|
|
190
|
-
* {SunSpot}[http://github.com/sunspot/sunspot] -- An awesome Solr DSL, built with RSolr
|
|
191
|
-
* {Blacklight}[http://blacklightopac.org] -- A "next generation" Library OPAC, built with RSolr
|
|
192
|
-
* {java_bin}[http://github.com/kennyj/java_bin] -- Provides javabin/binary parsing for RSolr
|
|
193
|
-
* {Solr}[http://lucene.apache.org/solr/] -- The Apache Solr project
|
|
194
|
-
* {solr-ruby}[http://wiki.apache.org/solr/solr-ruby] -- The original Solr Ruby Gem!
|
|
195
|
-
|
|
196
|
-
== Note on Patches/Pull Requests
|
|
197
|
-
* Fork the project.
|
|
198
|
-
* Make your feature addition or bug fix.
|
|
199
|
-
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
|
200
|
-
* Commit, do not mess with rakefile, version, or history
|
|
201
|
-
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
|
202
|
-
* Send me a pull request. Bonus points for topic branches.
|
|
203
|
-
|
|
204
|
-
==Contributors
|
|
205
|
-
* Nathan Witmer
|
|
206
|
-
* Magnus Bergmark
|
|
207
|
-
* shima
|
|
208
|
-
* Randy Souza
|
|
209
|
-
* Mat Brown
|
|
210
|
-
* Jeremy Hinegardner
|
|
211
|
-
* Denis Goeury
|
|
212
|
-
* shairon toledo
|
|
213
|
-
* Rob Di Marco
|
|
214
|
-
* Peter Kieltyka
|
|
215
|
-
* Mike Perham
|
|
216
|
-
* Lucas Souza
|
|
217
|
-
* Dmitry Lihachev
|
|
218
|
-
* Antoine Latter
|
|
219
|
-
* Naomi Dushay
|
|
220
|
-
|
|
221
|
-
==Author
|
|
222
|
-
|
|
223
|
-
Matt Mitchell <mailto:goodieboy@gmail.com>
|
|
224
|
-
|
|
225
|
-
==Copyright
|
|
226
|
-
|
|
227
|
-
Copyright (c) 2008-2010 Matt Mitchell. See LICENSE for details.
|