better-riak-client 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/LICENSE +16 -0
  2. data/README.markdown +198 -0
  3. data/RELEASE_NOTES.md +211 -0
  4. data/better-riak-client.gemspec +61 -0
  5. data/erl_src/riak_kv_test014_backend.beam +0 -0
  6. data/erl_src/riak_kv_test014_backend.erl +189 -0
  7. data/erl_src/riak_kv_test_backend.beam +0 -0
  8. data/erl_src/riak_kv_test_backend.erl +697 -0
  9. data/erl_src/riak_search_test_backend.beam +0 -0
  10. data/erl_src/riak_search_test_backend.erl +175 -0
  11. data/lib/riak/bucket.rb +221 -0
  12. data/lib/riak/client/beefcake/messages.rb +213 -0
  13. data/lib/riak/client/beefcake/object_methods.rb +111 -0
  14. data/lib/riak/client/beefcake_protobuffs_backend.rb +226 -0
  15. data/lib/riak/client/decaying.rb +36 -0
  16. data/lib/riak/client/excon_backend.rb +162 -0
  17. data/lib/riak/client/feature_detection.rb +88 -0
  18. data/lib/riak/client/http_backend/configuration.rb +211 -0
  19. data/lib/riak/client/http_backend/key_streamer.rb +43 -0
  20. data/lib/riak/client/http_backend/object_methods.rb +106 -0
  21. data/lib/riak/client/http_backend/request_headers.rb +34 -0
  22. data/lib/riak/client/http_backend/transport_methods.rb +201 -0
  23. data/lib/riak/client/http_backend.rb +340 -0
  24. data/lib/riak/client/net_http_backend.rb +82 -0
  25. data/lib/riak/client/node.rb +115 -0
  26. data/lib/riak/client/protobuffs_backend.rb +173 -0
  27. data/lib/riak/client/search.rb +91 -0
  28. data/lib/riak/client.rb +540 -0
  29. data/lib/riak/cluster.rb +151 -0
  30. data/lib/riak/core_ext/blank.rb +53 -0
  31. data/lib/riak/core_ext/deep_dup.rb +13 -0
  32. data/lib/riak/core_ext/extract_options.rb +7 -0
  33. data/lib/riak/core_ext/json.rb +15 -0
  34. data/lib/riak/core_ext/slice.rb +18 -0
  35. data/lib/riak/core_ext/stringify_keys.rb +10 -0
  36. data/lib/riak/core_ext/symbolize_keys.rb +10 -0
  37. data/lib/riak/core_ext/to_param.rb +31 -0
  38. data/lib/riak/core_ext.rb +7 -0
  39. data/lib/riak/encoding.rb +6 -0
  40. data/lib/riak/failed_request.rb +81 -0
  41. data/lib/riak/i18n.rb +5 -0
  42. data/lib/riak/json.rb +52 -0
  43. data/lib/riak/link.rb +94 -0
  44. data/lib/riak/locale/en.yml +53 -0
  45. data/lib/riak/locale/fr.yml +52 -0
  46. data/lib/riak/map_reduce/filter_builder.rb +103 -0
  47. data/lib/riak/map_reduce/phase.rb +98 -0
  48. data/lib/riak/map_reduce.rb +225 -0
  49. data/lib/riak/map_reduce_error.rb +7 -0
  50. data/lib/riak/node/configuration.rb +293 -0
  51. data/lib/riak/node/console.rb +133 -0
  52. data/lib/riak/node/control.rb +207 -0
  53. data/lib/riak/node/defaults.rb +83 -0
  54. data/lib/riak/node/generation.rb +106 -0
  55. data/lib/riak/node/log.rb +34 -0
  56. data/lib/riak/node/version.rb +43 -0
  57. data/lib/riak/node.rb +38 -0
  58. data/lib/riak/robject.rb +318 -0
  59. data/lib/riak/search.rb +3 -0
  60. data/lib/riak/serializers.rb +74 -0
  61. data/lib/riak/stamp.rb +77 -0
  62. data/lib/riak/test_server.rb +89 -0
  63. data/lib/riak/util/escape.rb +76 -0
  64. data/lib/riak/util/headers.rb +53 -0
  65. data/lib/riak/util/multipart/stream_parser.rb +62 -0
  66. data/lib/riak/util/multipart.rb +52 -0
  67. data/lib/riak/util/tcp_socket_extensions.rb +58 -0
  68. data/lib/riak/util/translation.rb +19 -0
  69. data/lib/riak/version.rb +3 -0
  70. data/lib/riak/walk_spec.rb +105 -0
  71. data/lib/riak.rb +21 -0
  72. metadata +348 -0
@@ -0,0 +1,34 @@
1
+
2
+ require 'riak/util/headers'
3
+
4
+ module Riak
5
+ class Client
6
+ class HTTPBackend
7
+ # @private
8
+ class RequestHeaders < Riak::Util::Headers
9
+ alias each each_capitalized
10
+
11
+ def initialize(hash)
12
+ initialize_http_header(hash)
13
+ end
14
+
15
+ def to_a
16
+ [].tap do |arr|
17
+ each_capitalized do |k,v|
18
+ arr << "#{k}: #{v}"
19
+ end
20
+ end
21
+ end
22
+
23
+ def to_hash
24
+ {}.tap do |hash|
25
+ each_capitalized do |k,v|
26
+ hash[k] ||= []
27
+ hash[k] << v
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,201 @@
1
+
2
+ require 'base64'
3
+ require 'uri'
4
+ require 'riak/client'
5
+ require 'riak/util/headers'
6
+
7
+ module Riak
8
+ class Client
9
+ class HTTPBackend
10
+ # Methods related to performing HTTP requests in a consistent
11
+ # fashion across multiple client libraries. HTTP/1.1 verbs are
12
+ # presented as methods.
13
+ module TransportMethods
14
+ # Performs a HEAD request to the specified resource on the Riak server.
15
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
16
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
17
+ # @overload head(expect, *resource)
18
+ # @overload head(expect, *resource, headers)
19
+ # Send the request with custom headers
20
+ # @param [Hash] headers custom headers to send with the request
21
+ # @return [Hash] response data, containing only the :headers and :code keys
22
+ # @raise [FailedRequest] if the response code doesn't match the expected response
23
+ def head(expect, resource, headers={})
24
+ headers = default_headers.merge(headers)
25
+ perform(:head, resource, headers, expect)
26
+ end
27
+
28
+ # Performs a GET request to the specified resource on the Riak server.
29
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
30
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
31
+ # @overload get(expect, *resource)
32
+ # @overload get(expect, *resource, headers)
33
+ # Send the request with custom headers
34
+ # @param [Hash] headers custom headers to send with the request
35
+ # @overload get(expect, *resource, headers={})
36
+ # Stream the response body through the supplied block
37
+ # @param [Hash] headers custom headers to send with the request
38
+ # @yield [chunk] yields successive chunks of the response body as strings
39
+ # @return [Hash] response data, containing only the :headers and :code keys
40
+ # @return [Hash] response data, containing :headers, :body, and :code keys
41
+ # @raise [FailedRequest] if the response code doesn't match the expected response
42
+ def get(expect, resource, headers={}, &block)
43
+ headers = default_headers.merge(headers)
44
+ perform(:get, resource, headers, expect, &block)
45
+ end
46
+
47
+ # Performs a PUT request to the specified resource on the Riak server.
48
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
49
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
50
+ # @param [String] body the request body to send to the server
51
+ # @overload put(expect, *resource, body)
52
+ # @overload put(expect, *resource, body, headers)
53
+ # Send the request with custom headers
54
+ # @param [Hash] headers custom headers to send with the request
55
+ # @overload put(expect, *resource, body, headers={})
56
+ # Stream the response body through the supplied block
57
+ # @param [Hash] headers custom headers to send with the request
58
+ # @yield [chunk] yields successive chunks of the response body as strings
59
+ # @return [Hash] response data, containing only the :headers and :code keys
60
+ # @return [Hash] response data, containing :headers, :code, and :body keys
61
+ # @raise [FailedRequest] if the response code doesn't match the expected response
62
+ def put(expect, resource, body, headers={}, &block)
63
+ headers = default_headers.merge(headers)
64
+ verify_body!(body)
65
+ perform(:put, resource, headers, expect, body, &block)
66
+ end
67
+
68
+ # Performs a POST request to the specified resource on the Riak server.
69
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
70
+ # @param [String, Array<String>] resource a relative path or array of path segments that will be joined to the root URI
71
+ # @param [String] body the request body to send to the server
72
+ # @overload post(expect, *resource, body)
73
+ # @overload post(expect, *resource, body, headers)
74
+ # Send the request with custom headers
75
+ # @param [Hash] headers custom headers to send with the request
76
+ # @overload post(expect, *resource, body, headers={})
77
+ # Stream the response body through the supplied block
78
+ # @param [Hash] headers custom headers to send with the request
79
+ # @yield [chunk] yields successive chunks of the response body as strings
80
+ # @return [Hash] response data, containing only the :headers and :code keys
81
+ # @return [Hash] response data, containing :headers, :code and :body keys
82
+ # @raise [FailedRequest] if the response code doesn't match the expected response
83
+ def post(expect, resource, body, headers={}, &block)
84
+ headers = default_headers.merge(headers)
85
+ verify_body!(body)
86
+ perform(:post, resource, headers, expect, body, &block)
87
+ end
88
+
89
+ # Performs a DELETE request to the specified resource on the Riak server.
90
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
91
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
92
+ # @overload delete(expect, *resource)
93
+ # @overload delete(expect, *resource, headers)
94
+ # Send the request with custom headers
95
+ # @param [Hash] headers custom headers to send with the request
96
+ # @overload delete(expect, *resource, headers={})
97
+ # Stream the response body through the supplied block
98
+ # @param [Hash] headers custom headers to send with the request
99
+ # @yield [chunk] yields successive chunks of the response body as strings
100
+ # @return [Hash] response data, containing only the :headers and :code keys
101
+ # @return [Hash] response data, containing :headers, :code and :body keys
102
+ # @raise [FailedRequest] if the response code doesn't match the expected response
103
+ def delete(expect, resource, headers={}, &block)
104
+ headers = default_headers.merge(headers)
105
+ perform(:delete, resource, headers, expect, &block)
106
+ end
107
+
108
+ # Executes requests according to the underlying HTTP client library semantics.
109
+ # @abstract Subclasses must implement this internal method to perform HTTP requests
110
+ # according to the API of their HTTP libraries.
111
+ # @param [Symbol] method one of :head, :get, :post, :put, :delete
112
+ # @param [URI] uri the HTTP URI to request
113
+ # @param [Hash] headers headers to send along with the request
114
+ # @param [Fixnum, Array] expect the expected response code(s)
115
+ # @param [String, #read] body the PUT or POST request body
116
+ # @return [Hash] response data, containing :headers, :code and :body keys. Only :headers and :code should be present when the body is streamed or the method is :head.
117
+ # @yield [chunk] if the method is not :head, successive chunks of the response body will be yielded as strings
118
+ # @raise [NotImplementedError] if a subclass does not implement this method
119
+ def perform(method, uri, headers, expect, body=nil)
120
+ raise NotImplementedError
121
+ end
122
+
123
+ # Default header hash sent with every request, based on settings in the client
124
+ # @return [Hash] headers that will be merged with user-specified headers on every request
125
+ def default_headers
126
+ {
127
+ "Accept" => "multipart/mixed, application/json;q=0.7, */*;q=0.5",
128
+ "X-Riak-ClientId" => client_id
129
+ }.merge(basic_auth_header)
130
+ end
131
+
132
+ def client_id
133
+ value = @client.client_id
134
+ case value
135
+ when Integer
136
+ b64encode(value)
137
+ when String
138
+ value
139
+ end
140
+ end
141
+
142
+ def basic_auth_header
143
+ @node.basic_auth ? {"Authorization" => "Basic #{Base64::encode64(@node.basic_auth)}"} : {}
144
+ end
145
+
146
+ # @return [URI] The calculated root URI for the Riak HTTP endpoint
147
+ def root_uri
148
+ protocol = node.ssl_enabled? ? "https" : "http"
149
+ URI.parse("#{protocol}://#{node.host}:#{node.http_port}")
150
+ end
151
+
152
+ # Calculates an absolute URI from a relative path specification
153
+ # @param [Array<String,Hash>] segments a relative path or sequence of path segments and optional query params Hash that will be joined to the root URI
154
+ # @return [URI] an absolute URI for the resource
155
+ def path(*segments)
156
+ segments = segments.flatten
157
+ query = segments.extract_options!.to_param
158
+ root_uri.merge(segments.join("/").gsub(/\/+/, "/").sub(/^\//, '')).tap do |uri|
159
+ uri.query = query if query.present?
160
+ end
161
+ end
162
+
163
+ # Checks the expected response codes against the actual response code. Use internally when
164
+ # implementing {#perform}.
165
+ # @param [String, Fixnum, Array<String,Fixnum>] expected the expected response code(s)
166
+ # @param [String, Fixnum] actual the received response code
167
+ # @return [Boolean] whether the actual response code is acceptable given the expectations
168
+ def valid_response?(expected, actual)
169
+ Array(expected).map(&:to_i).include?(actual.to_i)
170
+ end
171
+
172
+ # Checks whether a combination of the HTTP method, response code, and block should
173
+ # result in returning the :body in the response hash. Use internally when implementing {#perform}.
174
+ # @param [Symbol] method the HTTP method
175
+ # @param [String, Fixnum] code the received response code
176
+ # @param [Boolean] has_block whether a streaming block was passed to {#perform}. Pass block_given? to this parameter.
177
+ # @return [Boolean] whether to return the body in the response hash
178
+ def return_body?(method, code, has_block)
179
+ method != :head && !valid_response?([204,205,304], code) && !has_block
180
+ end
181
+
182
+ # Checks whether the submitted body is valid. That is, it must
183
+ # be a String or respond to the 'read' method.
184
+ # @param [String, #read] body the body
185
+ # @raise [ArgumentError] if the body is of invalid type
186
+ def verify_body!(body)
187
+ raise ArgumentError, t('request_body_type') unless String === body || body.respond_to?(:read)
188
+ end
189
+
190
+ private
191
+ def response_headers
192
+ Thread.current[:response_headers] ||= Riak::Util::Headers.new
193
+ end
194
+
195
+ def b64encode(n)
196
+ Base64.encode64([n].pack("N")).chomp
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,340 @@
1
+ require 'riak/util/escape'
2
+ require 'riak/util/translation'
3
+ require 'riak/util/multipart'
4
+ require 'riak/util/multipart/stream_parser'
5
+ require 'riak/json'
6
+ require 'riak/client'
7
+ require 'riak/bucket'
8
+ require 'riak/robject'
9
+ require 'riak/client/http_backend/transport_methods'
10
+ require 'riak/client/http_backend/object_methods'
11
+ require 'riak/client/http_backend/configuration'
12
+ require 'riak/client/http_backend/key_streamer'
13
+ require 'riak/client/feature_detection'
14
+
15
+ module Riak
16
+ class Client
17
+ # The parent class for all backends that connect to Riak via
18
+ # HTTP. This class implements all of the universal backend API
19
+ # methods on behalf of subclasses, which need only implement the
20
+ # {TransportMethods#perform} method for library-specific
21
+ # semantics.
22
+ class HTTPBackend
23
+ include Util::Escape
24
+ include Util::Translation
25
+ include FeatureDetection
26
+
27
+ include TransportMethods
28
+ include ObjectMethods
29
+ include Configuration
30
+
31
+ # The Riak::Client that uses this backend
32
+ attr_reader :client
33
+
34
+ # The Riak::Client::Node that uses this backend
35
+ attr_reader :node
36
+
37
+ # Create an HTTPBackend for the Riak::Client.
38
+ # @param [Client] The client
39
+ # @param [Node] The node we're connecting to.
40
+ def initialize(client, node)
41
+ raise ArgumentError, t("client_type", :client => client) unless Client === client
42
+ raise ArgumentError, t("node_type", :node => node) unless Node === node
43
+ @client = client
44
+ @node = node
45
+ end
46
+
47
+ # Pings the server
48
+ # @return [true,false] whether the server is available
49
+ def ping
50
+ get(200, ping_path)
51
+ true
52
+ rescue
53
+ false
54
+ end
55
+
56
+ # Fetches an object by bucket/key
57
+ # @param [Bucket, String] bucket the bucket where the object is
58
+ # stored
59
+ # @param [String] key the key of the object
60
+ # @param [Hash] options request quorums
61
+ # @option options [Fixnum, String, Symbol] :r the read quorum for the
62
+ # request - how many nodes should concur on the read
63
+ # @option options [Fixnum, String, Symbol] :pr the "primary"
64
+ # read quorum for the request - how many primary partitions
65
+ # must be available
66
+ # @return [RObject] the fetched object
67
+ def fetch_object(bucket, key, options={})
68
+ bucket = Bucket.new(client, bucket) if String === bucket
69
+ response = get([200,300], object_path(bucket.name, key, options))
70
+ load_object(RObject.new(bucket, key), response)
71
+ end
72
+
73
+ # Reloads the data for a given RObject, a special case of {#fetch_object}.
74
+ def reload_object(robject, options={})
75
+ response = get([200,300,304], object_path(robject.bucket.name, robject.key, options), reload_headers(robject))
76
+ if response[:code].to_i == 304
77
+ robject
78
+ else
79
+ load_object(robject, response)
80
+ end
81
+ end
82
+
83
+ # Stores an object
84
+ # @param [RObject] robject the object to store
85
+ # @param [Hash] options quorum and storage options
86
+ # @option options [true,false] :returnbody (false) whether to update the object
87
+ # after write with the new value
88
+ # @option options [Fixnum, String, Symbol] :w the write quorum
89
+ # @option options [Fixnum, String, Symbol] :pw the "primary"
90
+ # write quorum - how many primary partitions must be available
91
+ # @option options [Fixnum, String, Symbol] :dw the durable write quorum
92
+ def store_object(robject, options={})
93
+ method, codes = if robject.key.present?
94
+ [:put, [200,204,300]]
95
+ else
96
+ [:post, 201]
97
+ end
98
+ response = send(method, codes, object_path(robject.bucket.name, robject.key, options), robject.raw_data, store_headers(robject))
99
+ load_object(robject, response) if options[:returnbody]
100
+ end
101
+
102
+ # Deletes an object
103
+ # @param [Bucket, String] bucket the bucket where the object
104
+ # lives
105
+ # @param [String] key the key where the object lives
106
+ # @param [Hash] options quorum and delete options
107
+ # @options options [Fixnum, String, Symbol] :rw the read/write quorum for
108
+ # the request
109
+ # @options options [String] :vclock the vector clock of the
110
+ # object to be deleted
111
+ def delete_object(bucket, key, options={})
112
+ bucket = bucket.name if Bucket === bucket
113
+ vclock = options.delete(:vclock)
114
+ headers = vclock ? {"X-Riak-VClock" => vclock} : {}
115
+ delete([204, 404], object_path(bucket, key, options), headers)
116
+ end
117
+
118
+ # Fetches bucket properties
119
+ # @param [Bucket, String] bucket the bucket properties to fetch
120
+ # @return [Hash] bucket properties
121
+ def get_bucket_props(bucket)
122
+ bucket = bucket.name if Bucket === bucket
123
+ response = get(200, bucket_properties_path(bucket))
124
+ JSON.parse(response[:body])['props']
125
+ end
126
+
127
+ # Sets bucket properties
128
+ # @param [Bucket, String] bucket the bucket to set properties on
129
+ # @param [Hash] properties the properties to set
130
+ def set_bucket_props(bucket, props)
131
+ bucket = bucket.name if Bucket === bucket
132
+ body = {'props' => props}.to_json
133
+ put(204, bucket_properties_path(bucket), body, {"Content-Type" => "application/json"})
134
+ end
135
+
136
+ # List keys in a bucket
137
+ # @param [Bucket, String] bucket the bucket to fetch the keys
138
+ # for
139
+ # @yield [Array<String>] a list of keys from the current
140
+ # streamed chunk
141
+ # @return [Array<String>] the list of keys, if no block was given
142
+ def list_keys(bucket, &block)
143
+ bucket = bucket.name if Bucket === bucket
144
+ if block_given?
145
+ get(200, key_list_path(bucket, :keys => 'stream'), {}, &KeyStreamer.new(block))
146
+ else
147
+ response = get(200, key_list_path(bucket))
148
+ obj = JSON.parse(response[:body])
149
+ obj && obj['keys'].map {|k| unescape(k) }
150
+ end
151
+ end
152
+
153
+ # Lists known buckets
154
+ # @return [Array<String>] the list of buckets
155
+ def list_buckets
156
+ response = get(200, bucket_list_path)
157
+ JSON.parse(response[:body])['buckets']
158
+ end
159
+
160
+ # Performs a MapReduce query.
161
+ # @param [MapReduce] mr the query to perform
162
+ # @yield [Fixnum, Object] the phase number and single result
163
+ # from the phase
164
+ # @return [Array<Object>] the list of results, if no block was
165
+ # given
166
+ def mapred(mr)
167
+ raise MapReduceError.new(t("empty_map_reduce_query")) if mr.query.empty? && !mapred_phaseless?
168
+ if block_given?
169
+ parser = Riak::Util::Multipart::StreamParser.new do |response|
170
+ result = JSON.parse(response[:body])
171
+ yield result['phase'], result['data']
172
+ end
173
+ post(200, mapred_path({:chunked => true}), mr.to_json, {"Content-Type" => "application/json", "Accept" => "application/json"}, &parser)
174
+ nil
175
+ else
176
+ response = post(200, mapred_path, mr.to_json, {"Content-Type" => "application/json", "Accept" => "application/json"})
177
+ begin
178
+ JSON.parse(response[:body])
179
+ rescue
180
+ response
181
+ end
182
+ end
183
+ end
184
+
185
+ # Gets health statistics
186
+ # @return [Hash] information about the server, including stats
187
+ def stats
188
+ response = get(200, stats_path)
189
+ JSON.parse(response[:body])
190
+ end
191
+
192
+ # Performs a link-walking query
193
+ # @param [RObject] robject the object to start at
194
+ # @param [Array<WalkSpec>] walk_specs a list of walk
195
+ # specifications to process
196
+ # @return [Array<Array<RObject>>] a list of the matched objects,
197
+ # grouped by phase
198
+ def link_walk(robject, walk_specs)
199
+ response = get(200, link_walk_path(robject.bucket.name, robject.key, walk_specs))
200
+ if boundary = Util::Multipart.extract_boundary(response[:headers]['content-type'].first)
201
+ Util::Multipart.parse(response[:body], boundary).map do |group|
202
+ group.map do |obj|
203
+ if obj[:headers] && !obj[:headers]['x-riak-deleted'] && !obj[:body].blank? && obj[:headers]['location']
204
+ link = Riak::Link.new(obj[:headers]['location'].first, "")
205
+ load_object(RObject.new(client.bucket(link.bucket), link.key), obj)
206
+ end
207
+ end.compact
208
+ end
209
+ else
210
+ []
211
+ end
212
+ end
213
+
214
+ # Performs a secondary-index query.
215
+ # @param [String, Bucket] bucket the bucket to query
216
+ # @param [String] index the index to query
217
+ # @param [String, Integer, Range] query the equality query or
218
+ # range query to perform
219
+ # @return [Array<String>] a list of keys matching the query
220
+ def get_index(bucket, index, query)
221
+ bucket = bucket.name if Bucket === bucket
222
+ path = case query
223
+ when Range
224
+ raise ArgumentError, t('invalid_index_query', :value => query.inspect) unless String === query.begin || Integer === query.end
225
+ index_range_path(bucket, index, query.begin, query.end)
226
+ when String, Integer
227
+ index_eq_path(bucket, index, query)
228
+ else
229
+ raise ArgumentError, t('invalid_index_query', :value => query.inspect)
230
+ end
231
+ response = get(200, path)
232
+ JSON.parse(response[:body])['keys']
233
+ end
234
+
235
+ # (Riak Search) Performs a search query
236
+ # @param [String,nil] index the index to query, or nil for the
237
+ # default
238
+ # @param [String] query the Lucene query to perform
239
+ # @param [Hash] options query options
240
+ # @see Client#search
241
+ def search(index, query, options={})
242
+ response = get(200, solr_select_path(index, query, options.stringify_keys))
243
+ if response[:headers]['content-type'].include?("application/json")
244
+ normalize_search_response JSON.parse(response[:body])
245
+ else
246
+ response[:body]
247
+ end
248
+ end
249
+
250
+ # (Riak Search) Updates a search index (includes deletes).
251
+ # @param [String, nil] index the index to update, or nil for the
252
+ # default index.
253
+ # @param [String] updates an XML update string in Solr's required format
254
+ # @see Client#index
255
+ def update_search_index(index, updates)
256
+ post(200, solr_update_path(index), updates, {'Content-Type' => 'text/xml'})
257
+ end
258
+
259
+ # (Luwak) Fetches a file from the Luwak large-file interface.
260
+ # @param [String] filename the name of the file
261
+ # @yield [chunk] A block which will receive individual chunks of
262
+ # the file as they are streamed
263
+ # @yieldparam [String] chunk a block of the file
264
+ # @return [IO, nil] the file (also having content_type and
265
+ # original_filename accessors). The file will need to be
266
+ # reopened to be read
267
+ def get_file(filename, &block)
268
+ if block_given?
269
+ get(200, luwak_path(filename), &block)
270
+ nil
271
+ else
272
+ tmpfile = LuwakFile.new(escape(filename))
273
+ begin
274
+ response = get(200, luwak_path(filename)) do |chunk|
275
+ tmpfile.write chunk
276
+ end
277
+ tmpfile.content_type = response[:headers]['content-type'].first
278
+ tmpfile
279
+ ensure
280
+ tmpfile.close
281
+ end
282
+ end
283
+ end
284
+
285
+ # (Luwak) Detects whether a file exists in the Luwak large-file
286
+ # interface.
287
+ # @param [String] filename the name of the file
288
+ # @return [true,false] whether the file exists
289
+ def file_exists?(filename)
290
+ result = head([200,404], luwak_path(filename))
291
+ result[:code] == 200
292
+ end
293
+
294
+ # (Luwak) Deletes a file from the Luwak large-file interface.
295
+ # @param [String] filename the name of the file
296
+ def delete_file(filename)
297
+ delete([204,404], luwak_path(filename))
298
+ end
299
+
300
+ # (Luwak) Uploads a file to the Luwak large-file interface.
301
+ # @overload store_file(filename, content_type, data)
302
+ # Stores the file at the given key/filename
303
+ # @param [String] filename the key/filename for the object
304
+ # @param [String] content_type the MIME Content-Type for the data
305
+ # @param [IO, String] data the contents of the file
306
+ # @overload store_file(content_type, data)
307
+ # Stores the file with a server-determined key/filename
308
+ # @param [String] content_type the MIME Content-Type for the data
309
+ # @param [String, #read] data the contents of the file
310
+ # @return [String] the key/filename where the object was stored
311
+ def store_file(*args)
312
+ data, content_type, filename = args.reverse
313
+ if filename
314
+ put(204, luwak_path(filename), data, {"Content-Type" => content_type})
315
+ filename
316
+ else
317
+ response = post(201, luwak_path(nil), data, {"Content-Type" => content_type})
318
+ response[:headers]["location"].first.split("/").last
319
+ end
320
+ end
321
+
322
+ private
323
+ def normalize_search_response(json)
324
+ {}.tap do |result|
325
+ if json['response']
326
+ result['num_found'] = json['response']['numFound']
327
+ result['max_score'] = json['response']['maxScore'].to_f
328
+ result['docs'] = json['response']['docs'].map do |d|
329
+ if d['fields']
330
+ d['fields'].merge('id' => d['id'])
331
+ else
332
+ d
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
340
+ end