elasticsearch-api 6.0.0 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 63bde47c1cc7d46fc4c79164a9a42e04e400b335
4
- data.tar.gz: 6c4edd90ed5a8ab829f86e29118cf886d188a0ca
3
+ metadata.gz: ba7cd7dc9a07db583b12281c41cf5d612ed43f02
4
+ data.tar.gz: 2fedb5c237669e77184ab1e7bff1df3cb324614e
5
5
  SHA512:
6
- metadata.gz: a77fa034cc384ac7ed0b930584f6e7f744fe5e67a8a762a3a0984f7ee5229afad99f108815527ffa5d933e9031903382086bfe9d7abb59fd50e100c5295705c5
7
- data.tar.gz: 35a7de732e056b4be4517785557d9ed527c57987dc46ff3e765891d36d9eedcb949a38107d17acc735e89565cfb58db59f98975468317b6f2e55f2d6c05c9632
6
+ metadata.gz: e963573fb73b41056f6fe8af032d5fa2b0f6818a5ea1f0f7ebb80a03e78745d946897b0cb3ca103763f2fcb53817d92e55f30b5abef01899f87a1bbd8392447f
7
+ data.tar.gz: 5dde0d7b48b370a27d7c25aa418d1d00ac982fb6242c594aa7fad6665f7b020539cb0cfcd5dfb0970f39b7638ac9cc404ac2c69da634da144b78a1365bd988bd
data/README.md CHANGED
@@ -11,7 +11,7 @@ the [Elasticsearch](http://elasticsearch.org) REST API.
11
11
  It does not provide an Elasticsearch client; see the
12
12
  [`elasticsearch-transport`](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch-transport) library.
13
13
 
14
- The library is compatible with Ruby 1.8.7 and higher.
14
+ The library is compatible with Ruby 1.9 and higher.
15
15
 
16
16
  It is compatible with Elasticsearch's API versions from 0.90 till current,
17
17
  just use a release matching major version of Elasticsearch.
@@ -22,6 +22,7 @@ just use a release matching major version of Elasticsearch.
22
22
  | 1.x | → | 1.x |
23
23
  | 2.x | → | 2.x |
24
24
  | 5.x | → | 5.x |
25
+ | 6.x | → | 6.x |
25
26
  | master | → | master |
26
27
 
27
28
  ## Installation
@@ -20,6 +20,8 @@ Gem::Specification.new do |s|
20
20
  s.extra_rdoc_files = [ "README.md", "LICENSE.txt" ]
21
21
  s.rdoc_options = [ "--charset=UTF-8" ]
22
22
 
23
+ s.required_ruby_version = '>= 1.9'
24
+
23
25
  s.add_dependency "multi_json"
24
26
 
25
27
  s.add_development_dependency "bundler", "> 1"
@@ -0,0 +1,22 @@
1
+ module Elasticsearch
2
+ module API
3
+ module Cluster
4
+ module Actions
5
+
6
+ # Returns the configured remote cluster information
7
+ #
8
+ #
9
+ # @see http://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-remote-info.html
10
+ #
11
+ def remote_info(arguments={})
12
+ method = Elasticsearch::API::HTTP_GET
13
+ path = "_remote/info"
14
+ params = {}
15
+ body = nil
16
+
17
+ perform_request(method, path, params, body).body
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,33 +2,40 @@ module Elasticsearch
2
2
  module API
3
3
  module Actions
4
4
 
5
- # Create a document.
5
+ # Create a new document.
6
6
  #
7
- # Enforce the _create_ operation when indexing a document --
8
- # the operation will return an error when the document already exists.
7
+ # The API will create new document, if it doesn't exist yet -- in that case, it will return
8
+ # a `409` error (`version_conflict_engine_exception`).
9
9
  #
10
- # @example Create a document
10
+ # You can leave out the `:id` parameter for the ID to be generated automatically
11
+ #
12
+ # @example Create a document with an ID
11
13
  #
12
14
  # client.create index: 'myindex',
13
- # type: 'mytype',
14
- # id: '1',
15
- # body: {
16
- # title: 'Test 1',
17
- # tags: ['y', 'z'],
18
- # published: true,
19
- # published_at: Time.now.utc.iso8601,
20
- # counter: 1
21
- # }
15
+ # type: 'doc',
16
+ # id: '1',
17
+ # body: {
18
+ # title: 'Test 1'
19
+ # }
22
20
  #
23
- # @option (see Actions#index)
21
+ # @example Create a document with an auto-generated ID
24
22
  #
25
- # (The `:op_type` argument is ignored.)
23
+ # client.create index: 'myindex',
24
+ # type: 'doc',
25
+ # body: {
26
+ # title: 'Test 1'
27
+ # }
28
+ #
29
+ # @option (see Actions#index)
26
30
  #
27
- # @see http://elasticsearch.org/guide/reference/api/index_/
31
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#_automatic_id_generation
28
32
  #
29
33
  def create(arguments={})
30
- raise ArgumentError, "Required argument 'id' missing" unless arguments[:id]
31
- index arguments.update :op_type => 'create'
34
+ if arguments[:id]
35
+ index arguments.update :op_type => 'create'
36
+ else
37
+ index arguments
38
+ end
32
39
  end
33
40
  end
34
41
  end
@@ -68,7 +68,7 @@ module Elasticsearch
68
68
  # @option arguments [Number] :version Explicit version number for concurrency control
69
69
  # @option arguments [String] :version_type Specific version type (options: internal, external, external_gte, force)
70
70
  #
71
- # @see http://elasticsearch.org/guide/reference/api/index_/
71
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html
72
72
  #
73
73
  def index(arguments={})
74
74
  raise ArgumentError, "Required argument 'index' missing" unless arguments[:index]
@@ -7,48 +7,11 @@ module Elasticsearch
7
7
  # Percolator allows you to register queries and then evaluate a document against them:
8
8
  # the IDs of matching queries are returned in the response.
9
9
  #
10
- # @example Register queries named "alert-1" and "alert-2" for the "my-index" index
10
+ # @deprecated The `_percolate` API has been deprecated in favour of a special field mapping and the
11
+ # `percolate` query;
12
+ # see https://www.elastic.co/guide/en/elasticsearch/reference/5.5/breaking_50_percolator.html
11
13
  #
12
- # client.index index: 'my-index',
13
- # type: '.percolator',
14
- # id: 'alert-1',
15
- # body: { query: { query_string: { query: 'foo' } } }
16
- #
17
- # client.index index: 'my-index',
18
- # type: '.percolator',
19
- # id: 'alert-2',
20
- # body: { query: { query_string: { query: 'bar' } } }
21
- #
22
- # @example Evaluate a custom document (passed as `:doc`) against the queries
23
- #
24
- # client.percolate index: 'my-index', type: 'my-type', body: { doc: { title: "Foo" } }
25
- # # => {..., matches: [ {_index: 'my-index', _id: 'alert-1'} ]}
26
- #
27
- # client.percolate index: 'my-index', type: 'my-type', body: { doc: { title: "Foo Bar" } }
28
- # # => {..., matches: [ {_index: 'my-index', _id: 'alert-2'}, {_index: 'my-index', _id: 'alert-1'} ] }
29
- #
30
- # @example Evaluate an existing document against the queries
31
- #
32
- # client.index index: 'my-index', type: 'my-type', id: 123, body: { title: "Foo Bar" }
33
- #
34
- # client.percolate index: 'my-index', type: 'my-type', id: '123'
35
- # # => { ..., matches: [ {_index: 'my-index', _id: 'alert-2'}, { _index: 'my-index', _id: 'alert-1'} ] }
36
- #
37
- # @example Register a query with custom `priority` property
38
- #
39
- # client.index index: 'my-index',
40
- # type: '.percolator',
41
- # id: 'alert-high-1',
42
- # body: { query: { query_string: { query: 'foo' } },
43
- # priority: 'high' }
44
- #
45
- # @example Evaluate a document against "high priority" percolator queries
46
- #
47
- # client.percolate index: 'my-index', type: 'my-type', body: {
48
- # doc: { title: "Foo" },
49
- # filter: { term: { priority: 'high' } }
50
- # }
51
- # # => {..., matches: [ {_index: 'my-index', _id: 'alert-high-1'} ]}
14
+ # See full example for Elasticsearch 5.x and higher in <https://github.com/elastic/elasticsearch-ruby/blob/master/examples/percolator/percolator_alerts.rb>
52
15
  #
53
16
  # @option arguments [String] :index The index of the document being percolated. (*Required*)
54
17
  # @option arguments [String] :type The type of the document being percolated. (*Required*)
@@ -75,6 +38,8 @@ module Elasticsearch
75
38
  # @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/master/search-percolate.html
76
39
  #
77
40
  def percolate(arguments={})
41
+ Utils.__report_unsupported_method :percolate
42
+
78
43
  raise ArgumentError, "Required argument 'index' missing" unless arguments[:index]
79
44
  raise ArgumentError, "Required argument 'type' missing" unless arguments[:type]
80
45
 
@@ -16,13 +16,13 @@ module Elasticsearch
16
16
  # scroll: '5m',
17
17
  # body: { query: { match: { title: 'test' } }, sort: '_id' }
18
18
  #
19
- # result = client.scroll scroll: '5m', scroll_id: result['_scroll_id']
19
+ # result = client.scroll scroll: '5m', body: { scroll_id: result['_scroll_id'] }
20
20
  #
21
21
  # @example Call the `scroll` API until all the documents are returned
22
22
  #
23
23
  # # Index 1,000 documents
24
24
  # client.indices.delete index: 'test'
25
- # 1_000.times do |i| client.index index: 'test', type: 'test', id: i+1, body: {title: "Test #{i}"} end
25
+ # 1_000.times do |i| client.index index: 'test', type: 'test', id: i+1, body: {title: "Test #{i+1}"} end
26
26
  # client.indices.refresh index: 'test'
27
27
  #
28
28
  # # Open the "view" of the index by passing the `scroll` parameter
@@ -34,7 +34,7 @@ module Elasticsearch
34
34
  # puts r['hits']['hits'].map { |d| d['_source']['title'] }.inspect
35
35
  #
36
36
  # # Call the `scroll` API until empty results are returned
37
- # while r = client.scroll(scroll_id: r['_scroll_id'], scroll: '5m') and not r['hits']['hits'].empty? do
37
+ # while r = client.scroll(body: { scroll_id: r['_scroll_id'] }, scroll: '5m') and not r['hits']['hits'].empty? do
38
38
  # puts "--- BATCH #{defined?($i) ? $i += 1 : $i = 1} -------------------------------------------------"
39
39
  # puts r['hits']['hits'].map { |d| d['_source']['title'] }.inspect
40
40
  # puts
@@ -20,7 +20,7 @@ module Elasticsearch
20
20
  # @example Add a tag to document `tags` property using a `script`
21
21
  #
22
22
  # client.update index: 'myindex', type: 'mytype', id: '1',
23
- # body: { script: 'ctx._source.tags += tag', params: { tag: 'x' } }
23
+ # body: { script: { source: 'ctx._source.tags.add(params.tag)', params: { tag: 'x' } } }
24
24
  #
25
25
  # @example Increment a document counter by 1 _or_ initialize it, when the document does not exist
26
26
  #
@@ -30,7 +30,7 @@ module Elasticsearch
30
30
  # @example Delete a document if it's tagged "to-delete"
31
31
  #
32
32
  # client.update index: 'myindex', type: 'mytype', id: '1',
33
- # body: { script: 'ctx._source.tags.contains(tag) ? ctx.op = "delete" : ctx.op = "none"',
33
+ # body: { script: 'ctx._source.tags.contains(params.tag) ? ctx.op = "delete" : ctx.op = "none"',
34
34
  # params: { tag: 'to-delete' } }
35
35
  #
36
36
  # @option arguments [String] :id Document ID (*Required*)
@@ -35,11 +35,12 @@ module Elasticsearch
35
35
  def __listify(*list)
36
36
  options = list.last.is_a?(Hash) ? list.pop : {}
37
37
 
38
- Array(list).flatten.
39
- map { |e| e.respond_to?(:split) ? e.split(',') : e }.
38
+ escape = options[:escape]
39
+ Array(list).
40
+ flat_map { |e| e.respond_to?(:split) ? e.split(',') : e }.
40
41
  flatten.
41
42
  compact.
42
- map { |e| options[:escape] == false ? e : __escape(e) }.
43
+ map { |e| escape == false ? e : __escape(e) }.
43
44
  join(',')
44
45
  end
45
46
 
@@ -58,7 +59,7 @@ module Elasticsearch
58
59
  def __pathify(*segments)
59
60
  Array(segments).flatten.
60
61
  compact.
61
- reject { |s| s.to_s =~ /^\s*$/ }.
62
+ reject { |s| s.to_s.strip.empty? }.
62
63
  join('/').
63
64
  squeeze('/')
64
65
  end
@@ -186,8 +187,12 @@ module Elasticsearch
186
187
  return parts
187
188
  end
188
189
 
189
- # Calls given block, rescuing from any exceptions. Returns `false`
190
- # if exception contains NotFound/404 in its class name or message, else re-raises exception.
190
+ # Calls the given block, rescuing from `StandardError`.
191
+ #
192
+ # Primary use case is the `:ignore` parameter for API calls.
193
+ #
194
+ # Returns `false` if exception contains NotFound in its class name or message,
195
+ # else re-raises the exception.
191
196
  #
192
197
  # @yield [block] A block of code to be executed with exception handling.
193
198
  #
@@ -195,8 +200,8 @@ module Elasticsearch
195
200
  #
196
201
  def __rescue_from_not_found(&block)
197
202
  yield
198
- rescue Exception => e
199
- if e.class.to_s =~ /NotFound/ || e.message =~ /Not\s*Found|404/i
203
+ rescue StandardError => e
204
+ if e.class.to_s =~ /NotFound/ || e.message =~ /Not\s*Found/i
200
205
  false
201
206
  else
202
207
  raise e
@@ -235,10 +240,12 @@ module Elasticsearch
235
240
  end
236
241
 
237
242
  unless messages.empty?
243
+ messages << "Suppress this warning by the `-WO` command line flag."
244
+
238
245
  if STDERR.tty?
239
- STDERR.puts messages.map { |m| "\e[31;1m#{m}\e[0m" }.join("\n")
246
+ Kernel.warn messages.map { |m| "\e[31;1m#{m}\e[0m" }.join("\n")
240
247
  else
241
- STDERR.puts messages.join("\n")
248
+ Kernel.warn messages.join("\n")
242
249
  end
243
250
  end
244
251
  end
@@ -249,12 +256,12 @@ module Elasticsearch
249
256
  message += " in `#{source}`"
250
257
  end
251
258
 
252
- message += ". This method is not supported in the version you're using: #{Elasticsearch::API::VERSION}, and will be removed in the next release."
259
+ message += ". This method is not supported in the version you're using: #{Elasticsearch::API::VERSION}, and will be removed in the next release. Suppress this warning by the `-WO` command line flag."
253
260
 
254
261
  if STDERR.tty?
255
- STDERR.puts "\e[31;1m#{message}\e[0m"
262
+ Kernel.warn "\e[31;1m#{message}\e[0m"
256
263
  else
257
- STDERR.puts message
264
+ Kernel.warn message
258
265
  end
259
266
  end
260
267
 
@@ -1,5 +1,5 @@
1
1
  module Elasticsearch
2
2
  module API
3
- VERSION = "6.0.0"
3
+ VERSION = "6.0.1"
4
4
  end
5
5
  end
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ module Elasticsearch
4
+ module Test
5
+ class ClusterRemoteInfoTest < ::Test::Unit::TestCase
6
+
7
+ context "Cluster: Remote info" do
8
+ subject { FakeClient.new }
9
+
10
+ should "perform correct request" do
11
+ subject.expects(:perform_request).with do |method, url, params, body|
12
+ assert_equal 'GET', method
13
+ assert_equal '_remote/info', url
14
+ assert_equal Hash.new, params
15
+ assert_nil body
16
+ true
17
+ end.returns(FakeResponse.new)
18
+
19
+ subject.cluster.remote_info
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -7,12 +7,6 @@ module Elasticsearch
7
7
  context "Creating a document with the #create method" do
8
8
  subject { FakeClient.new }
9
9
 
10
- should "require the ID" do
11
- assert_raise ArgumentError do
12
- subject.create :index => 'foo', :type => 'bar', :id => nil, :body => {:foo => 'bar'}
13
- end
14
- end
15
-
16
10
  should "perform the create request with a specific ID" do
17
11
  subject.expects(:perform_request).with do |method, url, params, body|
18
12
  assert_equal 'PUT', method
@@ -34,6 +28,27 @@ module Elasticsearch
34
28
 
35
29
  subject.create :index => 'foo', :type => 'bar/bam', :id => '123', :body => {}
36
30
  end
31
+
32
+ should "update the arguments with `op_type` when the `:id` argument is present" do
33
+ subject.expects(:perform_request).with do |method, url, params, body|
34
+ assert_equal 'foo/bar/1', url
35
+ assert_not_nil params[:op_type]
36
+ true
37
+ end.returns(FakeResponse.new)
38
+
39
+ subject.create :index => 'foo', :type => 'bar', :id => 1, :body => { :foo => 'bar' }
40
+ end
41
+
42
+ should "pass the arguments when the `:id` argument is missing" do
43
+ subject.expects(:perform_request).with do |method, url, params, body|
44
+ assert_equal 'foo/bar', url
45
+ assert_nil params[:op_type]
46
+ assert_nil params[:id]
47
+ true
48
+ end.returns(FakeResponse.new)
49
+
50
+ subject.create :index => 'foo', :type => 'bar', :body => { :foo => 'bar' }
51
+ end
37
52
  end
38
53
 
39
54
  end
@@ -233,16 +233,13 @@ module Elasticsearch
233
233
  end
234
234
 
235
235
  should "return false if exception message contains 'Not Found'" do
236
- assert_equal( false, __rescue_from_not_found { raise Exception.new "Not Found" })
237
- end
238
-
239
- should "return false if exception message contains '404'" do
240
- assert_equal( false, __rescue_from_not_found { raise Exception.new "404" })
236
+ assert_equal( false, __rescue_from_not_found { raise StandardError.new "Not Found" })
237
+ assert_equal( false, __rescue_from_not_found { raise StandardError.new "NotFound" })
241
238
  end
242
239
 
243
240
  should "raise exception if exception class name and message do not contain NotFound/404" do
244
- assert_raise Exception do
245
- __rescue_from_not_found { raise Exception.new "Any other exception" }
241
+ assert_raise StandardError do
242
+ __rescue_from_not_found { raise StandardError.new "Any other exception" }
246
243
  end
247
244
  end
248
245
 
@@ -253,8 +250,8 @@ module Elasticsearch
253
250
  arguments = { :foo => 'bar', :moo => 'bam', :baz => 'qux' }
254
251
  unsupported_params = [:foo, :moo]
255
252
 
256
- STDERR.expects(:puts).with do |message|
257
- assert_equal 2, message.split("\n").size
253
+ Kernel.expects(:warn).with do |message|
254
+ assert_equal 2, message.split("\n").reject { |l| l.include? 'Suppress this warning' }.size
258
255
  true
259
256
  end
260
257
 
@@ -265,9 +262,9 @@ module Elasticsearch
265
262
  arguments = { :foo => 'bar', :moo => 'bam', :baz => 'qux' }
266
263
  unsupported_params = [ { :foo => { :explanation => 'NOT_SUPPORTED' } } ]
267
264
 
268
- STDERR.expects(:puts).with do |message|
265
+ Kernel.expects(:warn).with do |message|
269
266
  assert_match /NOT_SUPPORTED/, message
270
- assert_equal 1, message.split("\n").size
267
+ assert_equal 1, message.split("\n").reject { |l| l.include? 'Suppress this warning' }.size
271
268
  true
272
269
  end
273
270
 
@@ -278,9 +275,9 @@ module Elasticsearch
278
275
  arguments = { :foo => 'bar', :moo => 'bam', :baz => 'qux' }
279
276
  unsupported_params = [ { :foo => { :explanation => 'NOT_SUPPORTED'} }, :moo ]
280
277
 
281
- STDERR.expects(:puts).with do |message|
278
+ Kernel.expects(:warn).with do |message|
282
279
  assert_match /NOT_SUPPORTED/, message
283
- assert_equal 2, message.split("\n").size
280
+ assert_equal 2, message.split("\n").reject { |l| l.include? 'Suppress this warning' }.size
284
281
  true
285
282
  end
286
283
 
@@ -291,7 +288,7 @@ module Elasticsearch
291
288
  arguments = { :moo => 'bam', :baz => 'qux' }
292
289
  unsupported_params = [:foo]
293
290
 
294
- STDERR.expects(:puts).never
291
+ Kernel.expects(:warn).never
295
292
 
296
293
  __report_unsupported_parameters(arguments, unsupported_params)
297
294
  end
@@ -299,7 +296,7 @@ module Elasticsearch
299
296
 
300
297
  context "__report_unsupported_method" do
301
298
  should "print the warning" do
302
- STDERR.expects(:puts).with do |message|
299
+ Kernel.expects(:warn).with do |message|
303
300
  assert_match /foo/, message
304
301
  true
305
302
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0
4
+ version: 6.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karel Minarik
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-14 00:00:00.000000000 Z
11
+ date: 2018-01-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json
@@ -380,6 +380,7 @@ files:
380
380
  - lib/elasticsearch/api/actions/cluster/health.rb
381
381
  - lib/elasticsearch/api/actions/cluster/pending_tasks.rb
382
382
  - lib/elasticsearch/api/actions/cluster/put_settings.rb
383
+ - lib/elasticsearch/api/actions/cluster/remote_info.rb
383
384
  - lib/elasticsearch/api/actions/cluster/reroute.rb
384
385
  - lib/elasticsearch/api/actions/cluster/state.rb
385
386
  - lib/elasticsearch/api/actions/cluster/stats.rb
@@ -531,6 +532,7 @@ files:
531
532
  - test/unit/cluster/health_test.rb
532
533
  - test/unit/cluster/pending_tasks_test.rb
533
534
  - test/unit/cluster/put_settings_test.rb
535
+ - test/unit/cluster/remote_info_test.rb
534
536
  - test/unit/cluster/reroute_test.rb
535
537
  - test/unit/cluster/state_test.rb
536
538
  - test/unit/cluster/stats_test.rb
@@ -662,7 +664,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
662
664
  requirements:
663
665
  - - ">="
664
666
  - !ruby/object:Gem::Version
665
- version: '0'
667
+ version: '1.9'
666
668
  required_rubygems_version: !ruby/object:Gem::Requirement
667
669
  requirements:
668
670
  - - ">="
@@ -670,7 +672,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
670
672
  version: '0'
671
673
  requirements: []
672
674
  rubyforge_project:
673
- rubygems_version: 2.5.2
675
+ rubygems_version: 2.6.11
674
676
  signing_key:
675
677
  specification_version: 4
676
678
  summary: Ruby API for Elasticsearch.
@@ -708,6 +710,7 @@ test_files:
708
710
  - test/unit/cluster/health_test.rb
709
711
  - test/unit/cluster/pending_tasks_test.rb
710
712
  - test/unit/cluster/put_settings_test.rb
713
+ - test/unit/cluster/remote_info_test.rb
711
714
  - test/unit/cluster/reroute_test.rb
712
715
  - test/unit/cluster/state_test.rb
713
716
  - test/unit/cluster/stats_test.rb