New Upstream Snapshot - libsearch-elasticsearch-client-2-0-perl

Ready changes

Summary

Merged new upstream version: 6.81+git20221229.1.cd7dfdd (was: 6.81).

Resulting package

Built on 2023-01-06T15:30 (took 10m2s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots libsearch-elasticsearch-client-2-0-perl

Lintian Result

Diff

diff --git a/Changes b/Changes
deleted file mode 100644
index 27b1b4f..0000000
--- a/Changes
+++ /dev/null
@@ -1,21 +0,0 @@
-Revision history for Search::Elasticsearch::Client::2_0
-
-6.81    2020-06-26
-        Bumped to version 6.81
-
-6.80    2020-03-25
-        Bumped to version 6.80
-        
-6.80_1  2020-03-11
-        Bumped to version 6.80
-        
-5.02    2017-04-02
-        Updated to work with Search::Elasticsearch 5.02
-
-5.01    2016-10-19
-
-        Doc fixes
-
-5.00    2016-10-19
-
-        First release of the 2_0 client module for Search::Elasticsearch 5.00
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 9f2ce7c..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,207 +0,0 @@
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
diff --git a/MANIFEST b/MANIFEST
deleted file mode 100644
index 1113f1b..0000000
--- a/MANIFEST
+++ /dev/null
@@ -1,47 +0,0 @@
-# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.012.
-Changes
-LICENSE
-MANIFEST
-META.json
-Makefile.PL
-README
-lib/Search/Elasticsearch/Client/2_0.pm
-lib/Search/Elasticsearch/Client/2_0/Bulk.pm
-lib/Search/Elasticsearch/Client/2_0/Direct.pm
-lib/Search/Elasticsearch/Client/2_0/Direct/Cat.pm
-lib/Search/Elasticsearch/Client/2_0/Direct/Cluster.pm
-lib/Search/Elasticsearch/Client/2_0/Direct/Indices.pm
-lib/Search/Elasticsearch/Client/2_0/Direct/Nodes.pm
-lib/Search/Elasticsearch/Client/2_0/Direct/Snapshot.pm
-lib/Search/Elasticsearch/Client/2_0/Direct/Tasks.pm
-lib/Search/Elasticsearch/Client/2_0/Role/API.pm
-lib/Search/Elasticsearch/Client/2_0/Role/Bulk.pm
-lib/Search/Elasticsearch/Client/2_0/Role/Scroll.pm
-lib/Search/Elasticsearch/Client/2_0/Scroll.pm
-lib/Search/Elasticsearch/Client/2_0/TestServer.pm
-t/Client_2_0/00_print_version.t
-t/Client_2_0/10_live.t
-t/Client_2_0/15_conflict.t
-t/Client_2_0/20_fork_httptiny.t
-t/Client_2_0/21_fork_lwp.t
-t/Client_2_0/22_fork_hijk.t
-t/Client_2_0/30_bulk_add_action.t
-t/Client_2_0/31_bulk_helpers.t
-t/Client_2_0/32_bulk_flush.t
-t/Client_2_0/33_bulk_errors.t
-t/Client_2_0/34_bulk_cxn_errors.t
-t/Client_2_0/40_scroll.t
-t/Client_2_0/50_reindex.t
-t/Client_2_0/60_auth_httptiny.t
-t/Client_2_0/61_auth_lwp.t
-t/author-eol.t
-t/author-no-tabs.t
-t/author-pod-syntax.t
-t/lib/LogCallback.pl
-t/lib/MockCxn.pm
-t/lib/bad_cacert.pem
-t/lib/default_cxn.pl
-t/lib/es_sync.pl
-t/lib/es_sync_auth.pl
-t/lib/es_sync_fork.pl
-t/lib/index_test_data.pl
diff --git a/META.json b/META.json
index 29527aa..41531e7 100644
--- a/META.json
+++ b/META.json
@@ -1,10 +1,10 @@
 {
-   "abstract" : "Thin client with full support for Elasticsearch 2.x APIs",
+   "abstract" : "The official client for Elasticsearch",
    "author" : [
       "Enrico Zimuel <enrico.zimuel@elastic.co>"
    ],
    "dynamic_config" : 0,
-   "generated_by" : "Dist::Zilla version 6.012, CPAN::Meta::Converter version 2.150010",
+   "generated_by" : "Dist::Zilla version 6.029, CPAN::Meta::Converter version 2.150010",
    "license" : [
       "apache_2_0"
    ],
@@ -12,67 +12,87 @@
       "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
       "version" : 2
    },
-   "name" : "Search-Elasticsearch-Client-2_0",
+   "name" : "Search-Elasticsearch",
    "prereqs" : {
-      "configure" : {
-         "requires" : {
-            "ExtUtils::MakeMaker" : "0"
-         }
-      },
-      "develop" : {
-         "requires" : {
-            "Test::EOL" : "0",
-            "Test::More" : "0.88",
-            "Test::NoTabs" : "0",
-            "Test::Pod" : "1.41"
-         }
-      },
       "runtime" : {
+         "recommends" : {
+            "URI::Escape::XS" : "0"
+         },
          "requires" : {
+            "Any::URI::Escape" : "0",
+            "AnyEvent::HTTP" : "0",
+            "AnyEvent::TLS" : "0",
+            "Data::Dumper" : "0",
             "Devel::GlobalDestruction" : "0",
-            "Moo" : "0",
+            "Encode" : "0",
+            "File::Temp" : "0",
+            "HTTP::Headers" : "0",
+            "HTTP::Parser::XS" : "0",
+            "HTTP::Request" : "0",
+            "HTTP::Tiny" : "0.076",
+            "IO::Compress::Deflate" : "0",
+            "IO::Compress::Gzip" : "0",
+            "IO::Select" : "0",
+            "IO::Socket" : "0",
+            "IO::Uncompress::Gunzip" : "0",
+            "IO::Uncompress::Inflate" : "0",
+            "JSON::MaybeXS" : "1.002002",
+            "JSON::PP" : "0",
+            "LWP::UserAgent" : "0",
+            "List::Util" : "0",
+            "Log::Any" : "1.02",
+            "Log::Any::Adapter" : "0",
+            "MIME::Base64" : "0",
+            "Module::Runtime" : "0",
+            "Moo" : "2.001000",
             "Moo::Role" : "0",
-            "Search::Elasticsearch" : "6.00",
-            "Search::Elasticsearch::Role::API" : "0",
-            "Search::Elasticsearch::Role::Client::Direct" : "0",
-            "Search::Elasticsearch::Role::Is_Sync" : "0",
-            "Search::Elasticsearch::Util" : "0",
+            "Net::Curl::Easy" : "0",
+            "Net::IP" : "0",
+            "POSIX" : "0",
+            "Package::Stash" : "0.34",
+            "Promises" : "0.93",
+            "Scalar::Util" : "0",
+            "Sub::Exporter" : "0",
+            "Time::HiRes" : "0",
             "Try::Tiny" : "0",
+            "URI" : "0",
             "namespace::clean" : "0",
+            "overload" : "0",
+            "parent" : "0",
             "strict" : "0",
             "warnings" : "0"
          }
       },
       "test" : {
+         "recommends" : {
+            "Cpanel::JSON::XS" : "0",
+            "JSON::XS" : "2.26",
+            "Mojo::IOLoop" : "0",
+            "Mojo::UserAgent" : "0"
+         },
          "requires" : {
-            "Data::Dumper" : "0",
+            "AE" : "0",
+            "EV" : "0",
             "IO::Socket::SSL" : "0",
-            "Log::Any::Adapter" : "0",
             "Log::Any::Adapter::Callback" : "0.09",
-            "POSIX" : "0",
-            "Search::Elasticsearch::Role::Cxn" : "0",
-            "Sub::Exporter" : "0",
+            "TAP::Harness::JUnit" : "0",
             "Test::Deep" : "0",
+            "Test::EOL" : "0",
             "Test::Exception" : "0",
             "Test::More" : "0.98",
+            "Test::NoTabs" : "0",
+            "Test::SharedFork" : "0",
+            "YAML" : "0",
+            "YAML::XS" : "0",
+            "base" : "2.18",
             "lib" : "0"
          }
       }
    },
    "release_status" : "stable",
-   "resources" : {
-      "bugtracker" : {
-         "web" : "https://github.com/elastic/elasticsearch-perl/issues"
-      },
-      "homepage" : "https://metacpan.org/pod/Search::Elasticsearch",
-      "repository" : {
-         "type" : "git",
-         "url" : "git://github.com/elastic/elasticsearch-perl.git",
-         "web" : "https://github.com/elastic/elasticsearch-perl"
-      }
-   },
-   "version" : "6.81",
-   "x_generated_by_perl" : "v5.26.1",
-   "x_serialization_backend" : "Cpanel::JSON::XS version 4.19"
+   "version" : "8.00",
+   "x_generated_by_perl" : "v5.36.0",
+   "x_serialization_backend" : "Cpanel::JSON::XS version 4.32",
+   "x_spdx_expression" : "Apache-2.0"
 }
 
diff --git a/Makefile.PL b/Makefile.PL
deleted file mode 100644
index 108fb41..0000000
--- a/Makefile.PL
+++ /dev/null
@@ -1,88 +0,0 @@
-# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.012.
-use strict;
-use warnings;
-
-
-
-use ExtUtils::MakeMaker;
-
-my %WriteMakefileArgs = (
-  "ABSTRACT" => "Thin client with full support for Elasticsearch 2.x APIs",
-  "AUTHOR" => "Enrico Zimuel <enrico.zimuel\@elastic.co>",
-  "CONFIGURE_REQUIRES" => {
-    "ExtUtils::MakeMaker" => 0
-  },
-  "DISTNAME" => "Search-Elasticsearch-Client-2_0",
-  "LICENSE" => "apache",
-  "NAME" => "Search::Elasticsearch::Client::2_0",
-  "PREREQ_PM" => {
-    "Devel::GlobalDestruction" => 0,
-    "Moo" => 0,
-    "Moo::Role" => 0,
-    "Search::Elasticsearch" => "6.00",
-    "Search::Elasticsearch::Role::API" => 0,
-    "Search::Elasticsearch::Role::Client::Direct" => 0,
-    "Search::Elasticsearch::Role::Is_Sync" => 0,
-    "Search::Elasticsearch::Util" => 0,
-    "Try::Tiny" => 0,
-    "namespace::clean" => 0,
-    "strict" => 0,
-    "warnings" => 0
-  },
-  "TEST_REQUIRES" => {
-    "Data::Dumper" => 0,
-    "IO::Socket::SSL" => 0,
-    "Log::Any::Adapter" => 0,
-    "Log::Any::Adapter::Callback" => "0.09",
-    "POSIX" => 0,
-    "Search::Elasticsearch::Role::Cxn" => 0,
-    "Sub::Exporter" => 0,
-    "Test::Deep" => 0,
-    "Test::Exception" => 0,
-    "Test::More" => "0.98",
-    "lib" => 0
-  },
-  "VERSION" => "6.81",
-  "test" => {
-    "TESTS" => "t/*.t t/Client_2_0/*.t"
-  }
-);
-
-
-my %FallbackPrereqs = (
-  "Data::Dumper" => 0,
-  "Devel::GlobalDestruction" => 0,
-  "IO::Socket::SSL" => 0,
-  "Log::Any::Adapter" => 0,
-  "Log::Any::Adapter::Callback" => "0.09",
-  "Moo" => 0,
-  "Moo::Role" => 0,
-  "POSIX" => 0,
-  "Search::Elasticsearch" => "6.00",
-  "Search::Elasticsearch::Role::API" => 0,
-  "Search::Elasticsearch::Role::Client::Direct" => 0,
-  "Search::Elasticsearch::Role::Cxn" => 0,
-  "Search::Elasticsearch::Role::Is_Sync" => 0,
-  "Search::Elasticsearch::Util" => 0,
-  "Sub::Exporter" => 0,
-  "Test::Deep" => 0,
-  "Test::Exception" => 0,
-  "Test::More" => "0.98",
-  "Try::Tiny" => 0,
-  "lib" => 0,
-  "namespace::clean" => 0,
-  "strict" => 0,
-  "warnings" => 0
-);
-
-
-unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) {
-  delete $WriteMakefileArgs{TEST_REQUIRES};
-  delete $WriteMakefileArgs{BUILD_REQUIRES};
-  $WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs;
-}
-
-delete $WriteMakefileArgs{CONFIGURE_REQUIRES}
-  unless eval { ExtUtils::MakeMaker->VERSION(6.52) };
-
-WriteMakefile(%WriteMakefileArgs);
diff --git a/README b/README
deleted file mode 100644
index 453ca21..0000000
--- a/README
+++ /dev/null
@@ -1,29 +0,0 @@
-NAME
-    Search::Elasticsearch::Client::2_0 - Thin client with full support for
-    Elasticsearch 2.x APIs
-
-VERSION
-    version 6.81
-
-DESCRIPTION
-    The Search::Elasticsearch::Client::2_0 package provides a client
-    compatible with Elasticsearch 2.x. It should be used in conjunction with
-    Search::Elasticsearch as follows:
-
-        $e = Search::Elasticsearch->new(
-            client => "2_0::Direct"
-        );
-
-    See Search::Elasticsearch::Client::2_0::Direct for documentation about
-    how to use the client itself.
-
-AUTHOR
-    Enrico Zimuel <enrico.zimuel@elastic.co>
-
-COPYRIGHT AND LICENSE
-    This software is Copyright (c) 2020 by Elasticsearch BV.
-
-    This is free software, licensed under:
-
-      The Apache License, Version 2.0, January 2004
-
diff --git a/cpanfile b/cpanfile
new file mode 100644
index 0000000..a216fa5
--- /dev/null
+++ b/cpanfile
@@ -0,0 +1,71 @@
+# This file is generated by Dist::Zilla::Plugin::CPANFile v6.029
+# Do not edit this file directly. To change prereqs, edit the `dist.ini` file.
+
+requires "Any::URI::Escape" => "0";
+requires "AnyEvent::HTTP" => "0";
+requires "AnyEvent::TLS" => "0";
+requires "Data::Dumper" => "0";
+requires "Devel::GlobalDestruction" => "0";
+requires "Encode" => "0";
+requires "File::Temp" => "0";
+requires "HTTP::Headers" => "0";
+requires "HTTP::Parser::XS" => "0";
+requires "HTTP::Request" => "0";
+requires "HTTP::Tiny" => "0.076";
+requires "IO::Compress::Deflate" => "0";
+requires "IO::Compress::Gzip" => "0";
+requires "IO::Select" => "0";
+requires "IO::Socket" => "0";
+requires "IO::Uncompress::Gunzip" => "0";
+requires "IO::Uncompress::Inflate" => "0";
+requires "JSON::MaybeXS" => "1.002002";
+requires "JSON::PP" => "0";
+requires "LWP::UserAgent" => "0";
+requires "List::Util" => "0";
+requires "Log::Any" => "1.02";
+requires "Log::Any::Adapter" => "0";
+requires "MIME::Base64" => "0";
+requires "Module::Runtime" => "0";
+requires "Moo" => "2.001000";
+requires "Moo::Role" => "0";
+requires "Net::Curl::Easy" => "0";
+requires "Net::IP" => "0";
+requires "POSIX" => "0";
+requires "Package::Stash" => "0.34";
+requires "Promises" => "0.93";
+requires "Scalar::Util" => "0";
+requires "Sub::Exporter" => "0";
+requires "Time::HiRes" => "0";
+requires "Try::Tiny" => "0";
+requires "URI" => "0";
+requires "namespace::clean" => "0";
+requires "overload" => "0";
+requires "parent" => "0";
+requires "strict" => "0";
+requires "warnings" => "0";
+recommends "URI::Escape::XS" => "0";
+
+on 'test' => sub {
+  requires "AE" => "0";
+  requires "EV" => "0";
+  requires "IO::Socket::SSL" => "0";
+  requires "Log::Any::Adapter::Callback" => "0.09";
+  requires "TAP::Harness::JUnit" => "0";
+  requires "Test::Deep" => "0";
+  requires "Test::EOL" => "0";
+  requires "Test::Exception" => "0";
+  requires "Test::More" => "0.98";
+  requires "Test::NoTabs" => "0";
+  requires "Test::SharedFork" => "0";
+  requires "YAML" => "0";
+  requires "YAML::XS" => "0";
+  requires "base" => "2.18";
+  requires "lib" => "0";
+};
+
+on 'test' => sub {
+  recommends "Cpanel::JSON::XS" => "0";
+  recommends "JSON::XS" => "2.26";
+  recommends "Mojo::IOLoop" => "0";
+  recommends "Mojo::UserAgent" => "0";
+};
diff --git a/debian/changelog b/debian/changelog
index 8881f8b..94aaeb8 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,9 @@
-libsearch-elasticsearch-client-2-0-perl (6.81-2) UNRELEASED; urgency=medium
+libsearch-elasticsearch-client-2-0-perl (6.81+git20221229.1.cd7dfdd-1) UNRELEASED; urgency=medium
 
   * Update standards version to 4.6.2, no changes needed.
+  * New upstream snapshot.
 
- -- Debian Janitor <janitor@jelmer.uk>  Fri, 06 Jan 2023 11:14:56 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 06 Jan 2023 15:26:19 -0000
 
 libsearch-elasticsearch-client-2-0-perl (6.81-1) unstable; urgency=medium
 
diff --git a/lib/Search/Elasticsearch.pm b/lib/Search/Elasticsearch.pm
new file mode 100644
index 0000000..aec5065
--- /dev/null
+++ b/lib/Search/Elasticsearch.pm
@@ -0,0 +1,570 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch;
+
+use Moo 2.001000 ();
+
+use Search::Elasticsearch::Util qw(parse_params load_plugin);
+use namespace::clean;
+
+our $VERSION = '8.00';
+
+my %Default_Plugins = (
+    client      => [ 'Search::Elasticsearch::Client',       '8_0::Direct' ],
+    cxn_factory => [ 'Search::Elasticsearch::Cxn::Factory', '' ],
+    cxn_pool    => [ 'Search::Elasticsearch::CxnPool',      'Static' ],
+    logger      => [ 'Search::Elasticsearch::Logger',       'LogAny' ],
+    serializer  => [ 'Search::Elasticsearch::Serializer',   'JSON' ],
+    transport   => [ 'Search::Elasticsearch::Transport',    '' ],
+);
+
+my @Load_Order = qw(
+    serializer
+    logger
+    cxn_factory
+    cxn_pool
+    transport
+    client
+);
+
+#===================================
+sub new {
+#===================================
+    my ( $class, $params ) = parse_params(@_);
+
+    $params->{cxn} ||= 'HTTPTiny';
+    my $plugins = delete $params->{plugins} || [];
+    $plugins = [$plugins] unless ref $plugins eq 'ARRAY';
+
+    for my $name (@Load_Order) {
+        my ( $base, $default ) = @{ $Default_Plugins{$name} };
+        my $sub_class = $params->{$name} || $default;
+        my $plugin_class = load_plugin( $base, $sub_class );
+        $params->{$name} = $plugin_class->new($params);
+    }
+
+    for my $name (@$plugins) {
+        my $plugin_class
+            = load_plugin( 'Search::Elasticsearch::Plugin', $name );
+        $plugin_class->_init_plugin($params);
+    }
+
+    return $params->{client};
+}
+
+1;
+
+__END__
+
+# ABSTRACT: The official client for Elasticsearch
+
+=head1 SYNOPSIS
+
+    use Search::Elasticsearch;
+
+    # Connect to localhost:9200:
+
+    my $e = Search::Elasticsearch->new();
+
+    # Round-robin between two nodes:
+
+    my $e = Search::Elasticsearch->new(
+        nodes => [
+            'search1:9200',
+            'search2:9200'
+        ]
+    );
+
+    # Connect to cluster at search1:9200, sniff all nodes and round-robin between them:
+
+    my $e = Search::Elasticsearch->new(
+        nodes    => 'search1:9200',
+        cxn_pool => 'Sniff'
+    );
+
+    # Index a document:
+
+    $e->index(
+        index   => 'my_app',
+        type    => 'blog_post',
+        id      => 1,
+        body    => {
+            title   => 'Elasticsearch clients',
+            content => 'Interesting content...',
+            date    => '2013-09-24'
+        }
+    );
+
+    # Get the document:
+
+    my $doc = $e->get(
+        index   => 'my_app',
+        type    => 'blog_post',
+        id      => 1
+    );
+
+    # Search:
+
+    my $results = $e->search(
+        index => 'my_app',
+        body  => {
+            query => {
+                match => { title => 'elasticsearch' }
+            }
+        }
+    );
+
+    # Cluster requests:
+
+    $info        = $e->cluster->info;
+    $health      = $e->cluster->health;
+    $node_stats  = $e->cluster->node_stats;
+
+    # Index requests:
+
+    $e->indices->create(index=>'my_index');
+    $e->indices->delete(index=>'my_index');
+
+=head1 DESCRIPTION
+
+L<Search::Elasticsearch> is the official Perl client for Elasticsearch,
+supported by L<elastic.co|http://elastic.co>.  Elasticsearch
+itself is a flexible and powerful open source, distributed real-time
+search and analytics engine for the cloud.  You can read more about it
+on L<elastic.co|http://www.elastic.co>.
+
+=head1 PREVIOUS VERSIONS OF ELASTICSEARCH
+
+This version of the client supports the Elasticsearch 7.0 branch,
+which is not backwards compatible with earlier branches.
+
+If you need to talk to a version of Elasticsearch before 7.0.0, please
+install one of the following packages:
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Client::2_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::1_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::0_90>
+
+=back
+
+=head2 Motivation
+
+=over
+
+I<The greatest deception men suffer is from their own opinions.>
+
+Leonardo da Vinci
+
+=back
+
+All of us have opinions, especially when it comes to designing APIs.
+Unfortunately, the opinions of programmers seldom coincide. The intention of
+this client, and of the officially supported clients available for other
+languages, is to provide robust support for the full native Elasticsearch API
+with as few opinions as possible:  you should be able to read the
+L<Elasticsearch reference documentation|http://www.elastic.co/guide>
+and understand how to use this client, or any of the other official clients.
+
+Should you decide that you want to customize the API, then this client
+provides the basis for your code.  It does the hard stuff for you,
+allowing you to build on top of it.
+
+=head2 Features
+
+This client provides:
+
+=over
+
+=item *
+
+Full support for all Elasticsearch APIs
+
+=item *
+
+HTTP backend (for an async backend using L<Promises>, see
+L<Search::Elasticsearch::Async>)
+
+=item *
+
+Robust networking support which handles load balancing, failure detection
+and failover
+
+=item *
+
+Good defaults
+
+=item *
+
+Helper utilities for more complex operations, such as
+L<bulk indexing|Search::Elasticsearch::Client::7_0::Bulk>, and
+L<scrolled searches|Search::Elasticsearch::Client::7_0::Scroll>
+
+=item *
+
+Logging support via L<Log::Any>
+
+=item *
+
+Compatibility with the official clients for Python, Ruby, PHP, and Javascript
+
+=item *
+
+Easy extensibility
+
+=back
+
+=head1 INSTALLING ELASTICSEARCH
+
+You can download the latest version of Elasticsearch from
+L<http://www.elastic.co/download>. See the
+L<installation instructions|https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html>
+for details. You will need to have a recent version of Java installed,
+preferably the Java v8 from Sun.
+
+=head1 CREATING A NEW INSTANCE
+
+The L</new()> method returns a new L<client|Search::Elasticsearch::Client::6_0::Direct>
+which can be used to run requests against the Elasticsearch cluster.
+
+    use Search::Elasticsearch;
+    my $e = Search::Elasticsearch->new( %params );
+
+The most important arguments to L</new()> are the following:
+
+=head2 C<nodes>
+
+The C<nodes> parameter tells the client which Elasticsearch nodes it should
+talk to.  It can be a single node, multiples nodes or, if not
+specified, will default to C<localhost:9200>:
+
+    # default: localhost:9200
+    $e = Search::Elasticsearch->new();
+
+    # single
+    $e = Search::Elasticsearch->new( nodes => 'search_1:9200');
+
+    # multiple
+    $e = Search::Elasticsearch->new(
+        nodes => [
+            'search_1:9200',
+            'search_2:9200'
+        ]
+    );
+
+Each C<node> can be a URL including a scheme, host, port, path and userinfo
+(for authentication).  For instance, this would be a valid node:
+
+    https://username:password@search.domain.com:443/prefix/path
+
+See L<Search::Elasticsearch::Role::Cxn/node> for more on node specification.
+
+=head2 C<cxn_pool>
+
+The L<CxnPool|Search::Elasticsearch::Role::CxnPool> modules manage connections to
+nodes in the Elasticsearch cluster.  They handle the load balancing between
+nodes and failover when nodes fail. Which C<CxnPool> you should use depends on
+where your cluster is. There are three choices:
+
+=over
+
+=item * C<Static>
+
+    $e = Search::Elasticsearch->new(
+        cxn_pool => 'Static'     # default
+        nodes    => [
+            'search1.domain.com:9200',
+            'search2.domain.com:9200'
+        ],
+    );
+
+The L<Static|Search::Elasticsearch::CxnPool::Static> connection pool, which is the
+default, should be used when you don't have direct access to the Elasticsearch
+cluster, eg when you are accessing the cluster through a proxy.  See
+L<Search::Elasticsearch::CxnPool::Static> for more.
+
+=item * C<Sniff>
+
+    $e = Search::Elasticsearch->new(
+        cxn_pool => 'Sniff',
+        nodes    => [
+            'search1:9200',
+            'search2:9200'
+        ],
+    );
+
+The L<Sniff|Search::Elasticsearch::CxnPool::Sniff> connection pool should be used
+when you B<do> have direct access to the Elasticsearch cluster, eg when
+your web servers and Elasticsearch servers are on the same network.
+The nodes that you specify are used to I<discover> the cluster, which is
+then I<sniffed> to find the current list of live nodes that the cluster
+knows about. See L<Search::Elasticsearch::CxnPool::Sniff>.
+
+=item * C<Static::NoPing>
+
+    $e = Search::Elasticsearch->new(
+        cxn_pool => 'Static::NoPing'
+        nodes    => [
+            'proxy1.domain.com:80',
+            'proxy2.domain.com:80'
+        ],
+    );
+
+The L<Static::NoPing|Search::Elasticsearch::CxnPool::Static::NoPing> connection
+pool should be used when your access to a remote cluster is so limited
+that you cannot ping individual nodes with a C<HEAD /> request.
+
+See L<Search::Elasticsearch::CxnPool::Static::NoPing> for more.
+
+=back
+
+=head2 C<trace_to>
+
+For debugging purposes, it is useful to be able to dump the actual HTTP
+requests which are sent to the cluster, and the response that is received.
+This can be enabled with the C<trace_to> parameter, as follows:
+
+    # To STDERR
+    $e = Search::Elasticsearch->new(
+        trace_to => 'Stderr'
+    );
+
+    # To a file
+    $e = Search::Elasticsearch->new(
+        trace_to => ['File','/path/to/filename']
+    );
+
+Logging is handled by L<Log::Any>.  See L<Search::Elasticsearch::Logger::LogAny>
+for more information.
+
+=head2 Other
+
+Other arguments are explained in the respective L<module docs|/MODULES>.
+
+=head1 RUNNING REQUESTS
+
+When you create a new instance of Search::Elasticsearch, it returns a
+L<client|Search::Elasticsearch::Client::6_0::Direct> object, which can be used for
+running requests.
+
+    use Search::Elasticsearch;
+    my $e = Search::Elasticsearch->new( %params );
+
+    # create an index
+    $e->indices->create( index => 'my_index' );
+
+    # index a document
+    $e->index(
+        index   => 'my_index',
+        type    => 'blog_post',
+        id      => 1,
+        body    => {
+            title   => 'Elasticsearch clients',
+            content => 'Interesting content...',
+            date    => '2013-09-24'
+        }
+    );
+
+See L<Search::Elasticsearch::Client::6_0::Direct> for more details about the requests that
+can be run.
+
+=head1 MODULES
+
+Each chunk of functionality is handled by a different module,
+which can be specified in the call to L<new()> as shown in L<cxn_pool> above.
+For instance, the following will use the L<Search::Elasticsearch::CxnPool::Sniff>
+module for the connection pool.
+
+    $e = Search::Elasticsearch->new(
+        cxn_pool => 'Sniff'
+    );
+
+Custom modules can be named with the appropriate prefix,
+eg C<Search::Elasticsearch::CxnPool::>, or by prefixing the full class name
+with C<+>:
+
+    $e = Search::Elasticsearch->new(
+        cxn_pool => '+My::Custom::CxnClass'
+    );
+
+The modules that you can override are specified with the following
+arguments to L</new()>:
+
+=head2 C<client>
+
+The class to use for the client functionality, which provides
+methods that can be called to execute requests, such as
+C<search()>, C<index()> or C<delete()>. The client parses the user's
+requests and passes them to the L</transport> class to be executed.
+
+The default version of the client is C<7_0::Direct>, which can
+be explicitly specified as follows:
+
+    $e = Search::Elasticsearch->new(
+        client => '7_0::Direct'
+    );
+
+=head2 C<transport>
+
+The Transport class accepts a parsed request from the L</client> class,
+fetches a L</cxn> from its L</cxn_pool> and tries to execute the request,
+retrying after failure where appropriate. See:
+
+=over
+
+=item * L<Search::Elasticsearch::Transport>
+
+=back
+
+=head2 C<cxn>
+
+The class which handles raw requests to Elasticsearch nodes.
+See:
+
+=over
+
+=item * L<Search::Elasticsearch::Cxn::HTTPTiny> (default)
+
+=item * L<Search::Elasticsearch::Cxn::LWP>
+
+=item * L<Search::Elasticsearch::Cxn::NetCurl>
+
+=back
+
+=head2 C<cxn_factory>
+
+The class which the L</cxn_pool> uses to create new L</cxn> objects.
+See:
+
+=over
+
+=item * L<Search::Elasticsearch::Cxn::Factory>
+
+=back
+
+=head2 C<cxn_pool> (2)
+
+The class to use for the L<connection pool|/cxn_pool> functionality.
+It calls the L</cxn_factory> class to create new L</cxn> objects when
+appropriate. See:
+
+=over
+
+=item * L<Search::Elasticsearch::CxnPool::Static> (default)
+
+=item * L<Search::Elasticsearch::CxnPool::Sniff>
+
+=item * L<Search::Elasticsearch::CxnPool::Static::NoPing>
+
+=back
+
+=head2 C<logger>
+
+The class to use for logging events and tracing HTTP requests/responses.  See:
+
+=over
+
+=item * L<Search::Elasticsearch::Logger::LogAny>
+
+=back
+
+=head2 C<serializer>
+
+The class to use for serializing request bodies and deserializing response
+bodies.  See:
+
+=over
+
+=item * L<Search::Elasticsearch::Serializer::JSON> (default)
+
+=item * L<Search::Elasticsearch::Serializer::JSON::Cpanel>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::XS>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::PP>
+
+=back
+
+=head1 BUGS
+
+This is a stable API but this implementation is new. Watch this space
+for new releases.
+
+If you have any suggestions for improvements, or find any bugs, please report
+them to L<http://github.com/elasticsearch/elasticsearch-perl/issues>.
+I will be notified, and then you'll automatically be notified of progress on
+your bug as I make changes.
+
+=head1 SUPPORT
+
+You can find documentation for this module with the perldoc command.
+
+    perldoc Search::Elasticsearch
+
+You can also look for information at:
+
+=over 4
+
+=item * GitHub
+
+L<http://github.com/elasticsearch/elasticsearch-perl>
+
+=item * CPAN Ratings
+
+L<http://cpanratings.perl.org/d/Search::Elasticsearch>
+
+
+=item * Search MetaCPAN
+
+L<https://metacpan.org/module/Search::Elasticsearch>
+
+=item * IRC
+
+The L<#elasticsearch|irc://irc.freenode.net/elasticsearch> channel on
+C<irc.freenode.net>.
+
+=item * Mailing list
+
+The main L<Elasticsearch mailing list|http://discuss.elastic.co>.
+
+=back
+
+=head1 TEST SUITE
+
+The full test suite requires a live Elasticsearch node to run, and should
+be run as :
+
+    perl Makefile.PL
+    ES=localhost:9200 make test
+
+B<TESTS RUN IN THIS WAY ARE DESTRUCTIVE! DO NOT RUN AGAINST A CLUSTER WITH
+DATA YOU WANT TO KEEP!>
+
+You can change the Cxn class which is used by setting the C<ES_CXN>
+environment variable:
+
+    ES_CXN=NetCurl ES=localhost:9200 make test
diff --git a/lib/Search/Elasticsearch/Async.pm b/lib/Search/Elasticsearch/Async.pm
new file mode 100644
index 0000000..bdd05b3
--- /dev/null
+++ b/lib/Search/Elasticsearch/Async.pm
@@ -0,0 +1,649 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Async;
+
+our $VERSION = '8.00';
+use Search::Elasticsearch 8.00;
+use Promises 0.93 ();
+use parent 'Search::Elasticsearch';
+
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+
+
+#===================================
+sub new {
+#===================================
+    my ( $class, $params ) = parse_params(@_);
+    my $self = $class->SUPER::new(
+        {   cxn_pool  => 'Async::Static',
+            transport => 'Async',
+            cxn       => 'AEHTTP',
+            %$params
+        }
+    );
+    unless ( $self->bulk_helper_class ) {
+        $self->bulk_helper_class(
+            'Client::' . $self->api_version . '::Async::Bulk' );
+    }
+    unless ( $self->scroll_helper_class ) {
+        $self->scroll_helper_class(
+            'Client::' . $self->api_version . '::Async::Scroll' );
+    }
+    return $self;
+}
+
+1;
+
+# ABSTRACT: Async API for Elasticsearch using Promises
+
+__END__
+
+=head1 SYNOPSIS
+
+    use Search::Elasticsearch::Async;
+    use Promises backend => ['AnyEvent'];
+
+    # Connect to localhost:9200:
+
+    my $e = Search::Elasticsearch::Async->new();
+
+    # Round-robin between two nodes:
+
+    my $e = Search::Elasticsearch::Async->new(
+        nodes => [
+            'search1:9200',
+            'search2:9200'
+        ]
+    );
+
+    # Connect to cluster at search1:9200, sniff all nodes and round-robin between them:
+
+    my $e = Search::Elasticsearch::Async->new(
+        nodes    => 'search1:9200',
+        cxn_pool => 'Async::Sniff'
+    );
+
+    # Index a document:
+
+    $e->index(
+        index   => 'my_app',
+        type    => 'blog_post',
+        id      => 1,
+        body    => {
+            title   => 'Elasticsearch clients',
+            content => 'Interesting content...',
+            date    => '2013-09-24'
+        }
+    )->then( sub { my $result = shift; do_something($result) } );
+
+    # Get the document:
+
+    my $doc;
+    $e->get(
+        index   => 'my_app',
+        type    => 'blog_post',
+        id      => 1
+    )->then( sub { $doc = shift });
+
+    # Search:
+
+    my $results;
+    $e->search(
+        index => 'my_app',
+        body  => {
+            query => {
+                match => { title => 'elasticsearch' }
+            }
+        }
+    )->then( sub { $results = shift });
+
+    # Cluster requests:
+
+    $e->cluster->info      ->then( sub { do_something(@_) });
+    $e->cluster->health    ->then( sub { do_something(@_) });
+    $e->cluster->node_stats->then( sub { do_something(@_) });
+
+    # Index requests:
+
+    $e->indices->create(index=>'my_index')->then( sub { do_something(@_) });
+    $e->indices->delete(index=>'my_index')->then( sub { do_something(@_) });
+
+=head1 DESCRIPTION
+
+L<Search::Elasticsearch::Async> is the official B<asynchronous> Perl client for
+Elasticsearch, supported by L<elastic.co|http://elastic.co>.
+Elasticsearch itself is a flexible and powerful open source, distributed real-time
+search and analytics engine for the cloud.  You can read more about it
+on L<elastic.co|http://www.elastic.co>.
+
+This module uses L<Promises> to provide a sane async interface, making your
+async code look more like synchronous code.  It can be used with
+L<Mojolicious> or with any of the event loops supported by L<AnyEvent>.
+
+L<Search::Elasticsearch::Async> builds on L<Search::Elasticsearch>, which
+you should see for the main documentation.
+
+=head1 PREVIOUS VERSIONS OF ELASTICSEARCH
+
+This version of the async client supports the Elasticsearch 5.0 branch,
+which is not backwards compatible with earlier branches.
+
+If you need to talk to a version of Elasticsearch before 5.0.0, please
+install one of the following packages:
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Client::2_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::1_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::0_90::Async>
+
+=back
+
+=head1 USING PROMISES
+
+First, go and read L<Promises::Cookbook::GentleIntro>, which tells you
+everything you need to know about working with L<Promises>.  Using them
+with L<Search::Elasticsearch::Async> is easy:
+
+=head2 Choose a Promises backend
+
+The Promises module does not use an event loop by default. You need to specify
+the one to use at the start of your application.  Typically, you will be using
+the L<EV> event loop (which both AnyEvent and Mojo prefer), in which case you
+need:
+
+    use Promises backend => ['EV'];
+
+Otherwise you can specify the C<Mojo> or C<AnyEvent> backends.
+
+=head2 Instantiate the client
+
+    use Search::Elasticsearch::Async;
+    my $es = Search::Elasticsearch::Async->new( %params );
+
+See L</"CREATING A NEW INSTANCE"> for an explantion of C<%params>.
+
+=head2 Make a request
+
+    my $promise = $es->search;
+
+All requests to Elasticsearch return a L<Promise> object, which is a value
+that will be resolved later on.  You can call C<then()> on the C<$promise>
+to specify a success callback and an error callback:
+
+    $promise->then(
+        sub { my $result = shift; do_something() },  # success callback
+        sub { my $error  = shift; warn $error    }   # error callback
+    );
+
+So far, so much like L<AnyEvent/CONDITION VARIABLES>... but
+C<then()> returns another C<$promise>, which makes them chainable:
+
+    $promise->then(sub  { print "Got a result"; return @_ })
+            ->then(sub  { my $result = shift; something_async($result) })
+            ->then(sub  { my $next_result = shift; ...etc...})
+            ->catch(sub { warn "Something failed: @_"});
+
+See L<Promises::Cookbook::GentleIntro> for a full explanation of
+what you can do with Promises.
+
+=head2 Start the event loop
+
+Async requests are run by the event loop, so no promises will be resolved
+or rejected until the event loop is started.  In a fully async application,
+you would start the event loop once and just let it run until the application
+exits. For instance, here's a simple example which reads search keywords
+from STDIN, performs an async search and prints the results. This process
+is repeated until the application is interrupted with C<Ctrl-C>.:
+
+    use v5.12;
+    use AnyEvent;
+    use Search::Elasticsearch::Async;
+
+    # EV must be installed
+    use Promises (backend => ['EV'], 'deferred');
+
+    my $es = Search::Elasticsearch::Async->new;
+
+    main();
+
+    say "Starting";
+
+    # start the event loop
+    EV::run;
+
+    sub main {
+        read_input()
+            ->then( \&do_search )
+            ->then( \&print_results )
+
+            # warn if either of the above steps throws an error
+            ->catch( sub { warn "Something went wrong: @_"; } )
+
+            # regardless of success or failure, run main() again
+            ->finally( \&main );
+    }
+
+    sub read_input {
+        say "Enter search keywords:";
+
+        # We wrap AnyEvent so that it returns a promise
+        # which is resolved when we have read from STDIN
+        my $d = deferred;
+
+        my $w;
+        $w = AnyEvent->io(
+            fh   => \*STDIN,
+            poll => 'r',
+            cb   => sub {
+                chomp( my $input = <STDIN> );
+                undef $w;
+
+                # resolve the promise
+                $d->resolve($input);
+            }
+        );
+
+        # return a promise
+        return $d->promise;
+    }
+
+    sub do_search {
+        my $keywords = shift();
+        # returns a promise
+        $es->search(
+            index => 'myindex',
+            body  => {
+                query => {
+                    match => {
+                        title => $keywords
+                    }
+                }
+            }
+        );
+    }
+
+    sub print_results {
+        my $results = shift;
+        my $total   = $results->{hits}{total};
+
+        unless ($total) {
+            say "No results found";
+            return;
+        }
+
+        say "$total results found";
+        my $i = 1;
+        for ( @{ $results->{hits}{hits} } ) {
+            say $i++ . ': ' . $_->{_source}{title};
+        }
+    }
+
+=head1 CREATING A NEW INSTANCE
+
+The L</new()> method returns a new L<client|Search::Elasticsearch::Client::6_0::Direct>
+which can be used to run requests against the Elasticsearch cluster.
+
+    use Search::Elasticsearch::Async;
+    my $e = Search::Elasticsearch::Async->new( %params );
+
+The most important arguments to L</new()> are the following:
+
+=head2 C<nodes>
+
+The C<nodes> parameter tells the client which Elasticsearch nodes it should
+talk to.  It can be a single node, multiples nodes or, if not
+specified, will default to C<localhost:9200>:
+
+    # default: localhost:9200
+    $e = Search::Elasticsearch::Async->new();
+
+    # single
+    $e = Search::Elasticsearch::Async->new( nodes => 'search_1:9200');
+
+    # multiple
+    $e = Search::Elasticsearch::Async->new(
+        nodes => [
+            'search_1:9200',
+            'search_2:9200'
+        ]
+    );
+
+Each C<node> can be a URL including a scheme, host, port, path and userinfo
+(for authentication).  For instance, this would be a valid node:
+
+    https://username:password@search.domain.com:443/prefix/path
+
+See L<Search::Elasticsearch::Role::Cxn/node> for more on node specification.
+
+=head2 C<cxn_pool>
+
+The L<CxnPool|Search::Elasticsearch::Role::CxnPool> modules manage connections to
+nodes in the Elasticsearch cluster.  They handle the load balancing between
+nodes and failover when nodes fail. Which C<CxnPool> you should use depends on
+where your cluster is. There are three choices:
+
+=over
+
+=item * C<Async::Static>
+
+    $e = Search::Elasticsearch::Async->new(
+        cxn_pool => 'Async::Static'     # default
+        nodes    => [
+            'search1.domain.com:9200',
+            'search2.domain.com:9200'
+        ],
+    );
+
+The L<Async::Static|Search::Elasticsearch::CxnPool::Async::Static> connection pool,
+which is the default, should be used when you don't have direct access to the
+Elasticsearch cluster, eg when you are accessing the cluster through a proxy.  See
+L<Search::Elasticsearch::CxnPool::Async::Static> for more.
+
+=item * C<Async::Sniff>
+
+    $e = Search::Elasticsearch::Async->new(
+        cxn_pool => 'Async::Sniff',
+        nodes    => [
+            'search1:9200',
+            'search2:9200'
+        ],
+    );
+
+The L<Async::Sniff|Search::Elasticsearch::CxnPool::Async::Sniff> connection pool should be used
+when you B<do> have direct access to the Elasticsearch cluster, eg when
+your web servers and Elasticsearch servers are on the same network.
+The nodes that you specify are used to I<discover> the cluster, which is
+then I<sniffed> to find the current list of live nodes that the cluster
+knows about. See L<Search::Elasticsearch::CxnPool::Async::Sniff>.
+
+=item * C<Async::Static::NoPing>
+
+    $e = Search::Elasticsearch::Async->new(
+        cxn_pool => 'Async::Static::NoPing'
+        nodes    => [
+            'proxy1.domain.com:80',
+            'proxy2.domain.com:80'
+        ],
+    );
+
+The L<Async::Static::NoPing|Search::Elasticsearch::CxnPool::Async::Static::NoPing> connection
+pool should be used when your access to a remote cluster is so limited
+that you cannot ping individual nodes with a C<HEAD /> request.
+
+See L<Search::Elasticsearch::CxnPool::Async::Static::NoPing> for more.
+
+=back
+
+=head2 C<trace_to>
+
+For debugging purposes, it is useful to be able to dump the actual HTTP
+requests which are sent to the cluster, and the response that is received.
+This can be enabled with the C<trace_to> parameter, as follows:
+
+    # To STDERR
+    $e = Search::Elasticsearch::Async->new(
+        trace_to => 'Stderr'
+    );
+
+    # To a file
+    $e = Search::Elasticsearch::Async->new(
+        trace_to => ['File','/path/to/filename']
+    );
+
+Logging is handled by L<Log::Any>.  See L<Search::Elasticsearch::Logger::LogAny>
+for more information.
+
+=head2 Other
+
+Other arguments are explained in the respective L<module docs|/MODULES>.
+
+=head1 RUNNING REQUESTS
+
+When you create a new instance of Search::Elasticsearch::Async, it returns a
+L<client|Search::Elasticsearch::Client::6_0::Direct> object, which can be used for
+running requests.
+
+    use Search::Elasticsearch::Async;
+    my $e = Search::Elasticsearch::Async->new( %params );
+
+    # create an index
+    $e->indices->create( index => 'my_index' )
+
+    ->then(sub {
+
+        # index a document
+        $e->index(
+            index   => 'my_index',
+            type    => 'blog_post',
+            id      => 1,
+            body    => {
+                title   => 'Elasticsearch clients',
+                content => 'Interesting content...',
+                date    => '2013-09-24'
+            }
+        );
+    });
+
+See L<Search::Elasticsearch::Client::6_0::Direct> for more details about the requests
+that can be run.
+
+=head1 MODULES
+
+Each chunk of functionality is handled by a different module,
+which can be specified in the call to L<new()> as shown in L<cxn_pool> above.
+For instance, the following will use the L<Search::Elasticsearch::CxnPool::Async::Sniff>
+module for the connection pool.
+
+    $e = Search::Elasticsearch::Async->new(
+        cxn_pool => 'Async::Sniff'
+    );
+
+Custom modules can be named with the appropriate prefix,
+eg C<Search::Elasticsearch::CxnPool::>, or by prefixing the full class name
+with C<+>:
+
+    $e = Search::Elasticsearch::Async->new(
+        cxn_pool => '+My::Custom::CxnClass'
+    );
+
+The modules that you can override are specified with the following
+arguments to L</new()>:
+
+=head2 C<client>
+
+The class to use for the client functionality, which provides
+methods that can be called to execute requests, such as
+C<search()>, C<index()> or C<delete()>. The client parses the user's
+requests and passes them to the L</transport> class to be executed.
+
+The default version of the client is C<6_0::Direct>, which can
+be explicitly specified as follows:
+
+    $e = Search::Elasticsearch::Async->new(
+        client => '6_0::Direct'
+    );
+
+See :
+
+=over
+
+=item * L<Search::Elasticsearch::Client::6_0::Direct> (default, for 6.0 branch)
+
+=item * L<Search::Elasticsearch::Client::5_0::Direct> (for 5.0 branch)
+
+=item * L<Search::Elasticsearch::Client::2_0::Direct> (for 2.0 branch)
+
+=item * L<Search::Elasticsearch::Client::1_0::Direct> (for 1.0 branch)
+
+=item * L<Search::Elasticsearch::Client::0_90::Direct> (for 0.90 branch)
+
+=back
+
+=head2 C<transport>
+
+The Transport class accepts a parsed request from the L</client> class,
+fetches a L</cxn> from its L</cxn_pool> and tries to execute the request,
+retrying after failure where appropriate. See:
+
+=over
+
+=item * L<Search::Elasticsearch::Async::Transport>
+
+=back
+
+=head2 C<cxn>
+
+The class which handles raw requests to Elasticsearch nodes.
+See:
+
+=over
+
+=item * L<Search::Elasticsearch::Cxn::AEHTTP> (default)
+
+=item * L<Search::Elasticsearch::Cxn::Mojo>
+
+=back
+
+=head2 C<cxn_factory>
+
+The class which the L</cxn_pool> uses to create new L</cxn> objects.
+See:
+
+=over
+
+=item * L<Search::Elasticsearch::Cxn::Factory>
+
+=back
+
+=head2 C<cxn_pool> (2)
+
+The class to use for the L<connection pool|/cxn_pool> functionality.
+It calls the L</cxn_factory> class to create new L</cxn> objects when
+appropriate. See:
+
+=over
+
+=item * L<Search::Elasticsearch::CxnPool::Async::Static> (default)
+
+=item * L<Search::Elasticsearch::CxnPool::Async::Sniff>
+
+=item * L<Search::Elasticsearch::CxnPool::Async::Static::NoPing>
+
+=back
+
+=head2 C<logger>
+
+The class to use for logging events and tracing HTTP requests/responses.  See:
+
+=over
+
+=item * L<Search::Elasticsearch::Logger::LogAny>
+
+=back
+
+=head2 C<serializer>
+
+The class to use for serializing request bodies and deserializing response
+bodies.  See:
+
+=over
+
+=item * L<Search::Elasticsearch::Serializer::JSON> (default)
+
+=item * L<Search::Elasticsearch::Serializer::JSON::Cpanel>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::XS>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::PP>
+
+=back
+
+=head1 HELPER MODULES
+
+L<Search::Elasticsearch::Client::6_0::Async::Bulk> and L<Search::Elasticsearch::Client::6_0::Async::Scroll>
+are helper modules which assist with bulk indexing and scrolled searching, eg:
+
+    $es->scroll_helper(
+        index     => 'myindex',
+        on_result => sub { my $doc = shift; do_something($doc) }
+    )->then( sub { say "Done!" });
+
+=head1 BUGS
+
+This is a stable API but this implementation is new. Watch this space
+for new releases.
+
+If you have any suggestions for improvements, or find any bugs, please report
+them to L<http://github.com/elasticsearch/elasticsearch-perl/issues>.
+I will be notified, and then you'll automatically be notified of progress on
+your bug as I make changes.
+
+=head1 SUPPORT
+
+You can find documentation for this module with the perldoc command.
+
+    perldoc Search::Elasticsearch::Async
+
+You can also look for information at:
+
+=over 4
+
+=item * GitHub
+
+L<http://github.com/elasticsearch/elasticsearch-perl>
+
+=item * CPAN Ratings
+
+L<http://cpanratings.perl.org/d/Search::Elasticsearch::Async>
+
+
+=item * Search MetaCPAN
+
+L<https://metacpan.org/module/Search::Elasticsearch::Async>
+
+=item * IRC
+
+The L<#elasticsearch|irc://irc.freenode.net/elasticsearch> channel on
+C<irc.freenode.net>.
+
+=item * Mailing list
+
+The main L<Elasticsearch mailing list|http://discuss.elastic.co>.
+
+=back
+
+=head1 TEST SUITE
+
+The full test suite requires a live Elasticsearch node to run, and should
+be run as :
+
+    perl Makefile.PL
+    ES=localhost:9200 make test
+
+B<TESTS RUN IN THIS WAY ARE DESTRUCTIVE! DO NOT RUN AGAINST A CLUSTER WITH
+DATA YOU WANT TO KEEP!>
diff --git a/lib/Search/Elasticsearch/Async/Util.pm b/lib/Search/Elasticsearch/Async/Util.pm
new file mode 100644
index 0000000..3d8e26f
--- /dev/null
+++ b/lib/Search/Elasticsearch/Async/Util.pm
@@ -0,0 +1,35 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Async::Util;
+
+use Moo;
+use Scalar::Util qw(blessed);
+use Sub::Exporter -setup => { exports => ['thenable'] };
+
+#===================================
+sub thenable {
+#===================================
+    return
+           unless @_ == 1
+        && blessed $_[0]
+        && $_[0]->can('then');
+    return shift();
+}
+1;
+
+# ABSTRACT: A utility class for internal use by Elasticsearch
diff --git a/lib/Search/Elasticsearch/Client/2_0.pm b/lib/Search/Elasticsearch/Client/2_0.pm
deleted file mode 100644
index 671935a..0000000
--- a/lib/Search/Elasticsearch/Client/2_0.pm
+++ /dev/null
@@ -1,50 +0,0 @@
-package Search::Elasticsearch::Client::2_0;
-
-our $VERSION='6.81';
-use Search::Elasticsearch 6.00 ();
-
-1;
-
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0 - Thin client with full support for Elasticsearch 2.x APIs
-
-=head1 VERSION
-
-version 6.81
-
-=head1 DESCRIPTION
-
-The L<Search::Elasticsearch::Client::2_0> package provides a client
-compatible with Elasticsearch 2.x.  It should be used in conjunction
-with L<Search::Elasticsearch> as follows:
-
-    $e = Search::Elasticsearch->new(
-        client => "2_0::Direct"
-    );
-
-See L<Search::Elasticsearch::Client::2_0::Direct> for documentation
-about how to use the client itself.
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
-
-__END__
-
-# ABSTRACT: Thin client with full support for Elasticsearch 2.x APIs
-
diff --git a/lib/Search/Elasticsearch/Client/2_0/Direct/Tasks.pm b/lib/Search/Elasticsearch/Client/2_0/Direct/Tasks.pm
deleted file mode 100644
index d366545..0000000
--- a/lib/Search/Elasticsearch/Client/2_0/Direct/Tasks.pm
+++ /dev/null
@@ -1,85 +0,0 @@
-package Search::Elasticsearch::Client::2_0::Direct::Tasks;
-$Search::Elasticsearch::Client::2_0::Direct::Tasks::VERSION = '6.81';
-use Moo;
-with 'Search::Elasticsearch::Client::2_0::Role::API';
-with 'Search::Elasticsearch::Role::Client::Direct';
-__PACKAGE__->_install_api('tasks');
-
-1;
-
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Direct::Tasks - A client for accessing the Task Management API
-
-=head1 VERSION
-
-version 6.81
-
-=head1 DESCRIPTION
-
-This module provides methods to access the Task Management API, such as listing
-tasks and cancelling tasks.
-
-It does L<Search::Elasticsearch::Role::Client::Direct>.
-
-=head1 METHODS
-
-=head2 C<list()>
-
-    $response = $e->tasks->list(
-        task_id => $task_id  # optional
-    );
-
-The C<list()> method returns all running tasks or, if a C<task_id> is specified, info
-about that task.
-
-Query string parameters:
-    C<actions>,
-    C<detailed>,
-    C<node_id>,
-    C<parent_node>,
-    C<parent_task>,
-    C<wait_for_completion>
-
-See the L<task management docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
-for more information.
-
-=head2 C<cancel()>
-
-    $response = $e->tasks->cancel(
-        task_id => $task_id  # option
-    );
-
-The C<cancel()> method attempts to cancel the specified C<task_id> or multiple tasks.
-
-Query string parameters:
-    C<actions>,
-    C<node_id>,
-    C<parent_node>,
-    C<parent_task>
-
-See the L<task management docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
-for more information.
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
-
-__END__
-
-# ABSTRACT: A client for accessing the Task Management API
-
diff --git a/lib/Search/Elasticsearch/Client/2_0/Role/API.pm b/lib/Search/Elasticsearch/Client/2_0/Role/API.pm
deleted file mode 100644
index 8de0671..0000000
--- a/lib/Search/Elasticsearch/Client/2_0/Role/API.pm
+++ /dev/null
@@ -1,2254 +0,0 @@
-package Search::Elasticsearch::Client::2_0::Role::API;
-$Search::Elasticsearch::Client::2_0::Role::API::VERSION = '6.81';
-use Moo::Role;
-with 'Search::Elasticsearch::Role::API';
-
-use Search::Elasticsearch::Util qw(throw);
-use namespace::clean;
-
-has 'api_version' => ( is => 'ro', default => '2_0' );
-
-our %API;
-
-#===================================
-sub api {
-#===================================
-    my $name = $_[1] || return \%API;
-    return $API{$name}
-        || throw( 'Internal', "Unknown api name ($name)" );
-}
-
-#===================================
-%API = (
-#===================================
-
-    'bulk.metadata' => {
-        params => [
-            'index',   'type',   'id',        'fields',
-            'routing', 'parent', 'timestamp', 'ttl',
-            'version', 'version_type'
-        ]
-    },
-    'bulk.update' => {
-        params => [
-            'doc',             'upsert',
-            'doc_as_upsert',   'fields',
-            'scripted_upsert', 'script',
-            'script_id',       'script_file',
-            'params',          'lang',
-            'detect_noop',     '_retry_on_conflict',
-        ]
-    },
-    'bulk.required' => { params => [ 'index', 'type' ] },
-
-#=== AUTOGEN - START ===
-
-    'bulk' => {
-        body   => { required => 1 },
-        doc    => "docs-bulk",
-        method => "POST",
-        parts => { index => {}, type => {} },
-        paths => [
-            [ { index => 0, type => 1 }, "{index}", "{type}", "_bulk" ],
-            [ { index => 0 }, "{index}", "_bulk" ],
-            [ {}, "_bulk" ],
-        ],
-        qs => {
-            consistency => "enum",
-            fields      => "list",
-            filter_path => "list",
-            refresh     => "boolean",
-            routing     => "string",
-            timeout     => "time",
-        },
-        serialize => "bulk",
-    },
-
-    'clear_scroll' => {
-        body   => {},
-        doc    => "search-request-scroll",
-        method => "DELETE",
-        parts  => { scroll_id => { multi => 1 } },
-        paths  => [
-            [ { scroll_id => 2 }, "_search", "scroll", "{scroll_id}" ],
-            [ {}, "_search", "scroll" ],
-        ],
-        qs => { filter_path => "list" },
-    },
-
-    'count' => {
-        body   => {},
-        doc    => "search-count",
-        method => "POST",
-        parts  => { index => { multi => 1 }, type => { multi => 1 } },
-        paths  => [
-            [ { index => 0, type => 1 }, "{index}", "{type}", "_count" ],
-            [ { index => 0 }, "{index}", "_count" ],
-            [ {}, "_count" ],
-        ],
-        qs => {
-            allow_no_indices         => "boolean",
-            analyze_wildcard         => "boolean",
-            analyzer                 => "string",
-            default_operator         => "enum",
-            df                       => "string",
-            expand_wildcards         => "enum",
-            filter_path              => "list",
-            ignore_unavailable       => "boolean",
-            lenient                  => "boolean",
-            lowercase_expanded_terms => "boolean",
-            min_score                => "number",
-            preference               => "string",
-            q                        => "string",
-            routing                  => "string",
-        },
-    },
-
-    'count_percolate' => {
-        body  => {},
-        doc   => "search-percolate",
-        parts => {
-            id    => {},
-            index => { required => 1 },
-            type  => { required => 1 }
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 }, "{index}",
-                "{type}",     "{id}",
-                "_percolate", "count",
-            ],
-            [   { index => 0, type => 1 }, "{index}",
-                "{type}", "_percolate",
-                "count",
-            ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            percolate_index    => "string",
-            percolate_type     => "string",
-            preference         => "string",
-            routing            => "list",
-            version            => "number",
-            version_type       => "enum",
-        },
-    },
-
-    'delete' => {
-        doc    => "docs-delete",
-        method => "DELETE",
-        parts  => {
-            id    => { required => 1 },
-            index => { required => 1 },
-            type  => { required => 1 },
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 }, "{index}",
-                "{type}", "{id}"
-            ],
-        ],
-        qs => {
-            consistency  => "enum",
-            filter_path  => "list",
-            parent       => "string",
-            refresh      => "boolean",
-            routing      => "string",
-            timeout      => "time",
-            version      => "number",
-            version_type => "enum",
-        },
-    },
-
-    'delete_by_query' => {
-        body   => {},
-        doc    => "plugins-delete-by-query",
-        method => "DELETE",
-        parts  => {
-            index => { multi => 1, required => 1 },
-            type  => { multi => 1 }
-        },
-        paths => [
-            [ { index => 0, type => 1 }, "{index}", "{type}", "_query" ],
-            [ { index => 0 }, "{index}", "_query" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            analyzer           => "string",
-            default_operator   => "enum",
-            df                 => "string",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            q                  => "string",
-            routing            => "string",
-            timeout            => "time",
-        },
-    },
-
-    'delete_script' => {
-        doc    => "modules-scripting",
-        method => "DELETE",
-        parts  => { id => { required => 1 }, lang => { required => 1 } },
-        paths => [ [ { id => 2, lang => 1 }, "_scripts", "{lang}", "{id}" ] ],
-        qs    => {
-            filter_path  => "list",
-            version      => "number",
-            version_type => "enum"
-        },
-    },
-
-    'delete_template' => {
-        doc    => "search-template",
-        method => "DELETE",
-        parts  => { id => { required => 1 } },
-        paths  => [ [ { id => 2 }, "_search", "template", "{id}" ] ],
-        qs     => {
-            filter_path  => "list",
-            version      => "number",
-            version_type => "enum"
-        },
-    },
-
-    'exists' => {
-        doc    => "docs-get",
-        method => "HEAD",
-        parts  => {
-            id    => { required => 1 },
-            index => { required => 1 },
-            type  => { required => 1 },
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 }, "{index}",
-                "{type}", "{id}"
-            ],
-        ],
-        qs => {
-            parent     => "string",
-            preference => "string",
-            realtime   => "boolean",
-            refresh    => "boolean",
-            routing    => "string",
-        },
-    },
-
-    'explain' => {
-        body  => {},
-        doc   => "search-explain",
-        parts => {
-            id    => { required => 1 },
-            index => { required => 1 },
-            type  => { required => 1 },
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 }, "{index}",
-                "{type}", "{id}",
-                "_explain",
-            ],
-        ],
-        qs => {
-            _source                  => "list",
-            _source_exclude          => "list",
-            _source_include          => "list",
-            analyze_wildcard         => "boolean",
-            analyzer                 => "string",
-            default_operator         => "enum",
-            df                       => "string",
-            fields                   => "list",
-            filter_path              => "list",
-            lenient                  => "boolean",
-            lowercase_expanded_terms => "boolean",
-            parent                   => "string",
-            preference               => "string",
-            q                        => "string",
-            routing                  => "string",
-        },
-    },
-
-    'field_stats' => {
-        body  => {},
-        doc   => "search-field-stats",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 0 }, "{index}", "_field_stats" ],
-            [ {}, "_field_stats" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            fields             => "list",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            level              => "enum",
-        },
-    },
-
-    'get' => {
-        doc   => "docs-get",
-        parts => {
-            id    => { required => 1 },
-            index => { required => 1 },
-            type  => { required => 1 },
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 }, "{index}",
-                "{type}", "{id}"
-            ],
-        ],
-        qs => {
-            _source         => "list",
-            _source_exclude => "list",
-            _source_include => "list",
-            fields          => "list",
-            filter_path     => "list",
-            parent          => "string",
-            preference      => "string",
-            realtime        => "boolean",
-            refresh         => "boolean",
-            routing         => "string",
-            version         => "number",
-            version_type    => "enum",
-        },
-    },
-
-    'get_script' => {
-        doc   => "modules-scripting",
-        parts => { id => { required => 1 }, lang => { required => 1 } },
-        paths => [ [ { id => 2, lang => 1 }, "_scripts", "{lang}", "{id}" ] ],
-        qs    => {
-            filter_path  => "list",
-            version      => "number",
-            version_type => "enum"
-        },
-    },
-
-    'get_source' => {
-        doc   => "docs-get",
-        parts => {
-            id    => { required => 1 },
-            index => { required => 1 },
-            type  => { required => 1 },
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 },
-                "{index}", "{type}", "{id}", "_source",
-            ],
-        ],
-        qs => {
-            _source         => "list",
-            _source_exclude => "list",
-            _source_include => "list",
-            filter_path     => "list",
-            parent          => "string",
-            preference      => "string",
-            realtime        => "boolean",
-            refresh         => "boolean",
-            routing         => "string",
-            version         => "number",
-            version_type    => "enum",
-        },
-    },
-
-    'get_template' => {
-        doc   => "search-template",
-        parts => { id => { required => 1 } },
-        paths => [ [ { id => 2 }, "_search", "template", "{id}" ] ],
-        qs    => {
-            filter_path  => "list",
-            version      => "number",
-            version_type => "enum"
-        },
-    },
-
-    'index' => {
-        body   => { required => 1 },
-        doc    => "docs-index_",
-        method => "POST",
-        parts  => {
-            id    => {},
-            index => { required => 1 },
-            type  => { required => 1 }
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 }, "{index}",
-                "{type}", "{id}"
-            ],
-            [ { index => 0, type => 1 }, "{index}", "{type}" ],
-        ],
-        qs => {
-            consistency  => "enum",
-            filter_path  => "list",
-            op_type      => "enum",
-            parent       => "string",
-            refresh      => "boolean",
-            routing      => "string",
-            timeout      => "time",
-            timestamp    => "time",
-            ttl          => "time",
-            version      => "number",
-            version_type => "enum",
-        },
-    },
-
-    'info' => {
-        doc   => "",
-        parts => {},
-        paths => [ [ {} ] ],
-        qs    => { filter_path => "list" }
-    },
-
-    'mget' => {
-        body => { required => 1 },
-        doc  => "docs-multi-get",
-        parts => { index => {}, type => {} },
-        paths => [
-            [ { index => 0, type => 1 }, "{index}", "{type}", "_mget" ],
-            [ { index => 0 }, "{index}", "_mget" ],
-            [ {}, "_mget" ],
-        ],
-        qs => {
-            _source         => "list",
-            _source_exclude => "list",
-            _source_include => "list",
-            fields          => "list",
-            filter_path     => "list",
-            preference      => "string",
-            realtime        => "boolean",
-            refresh         => "boolean",
-        },
-    },
-
-    'mpercolate' => {
-        body => { required => 1 },
-        doc  => "search-percolate",
-        parts => { index => {}, type => {} },
-        paths => [
-            [ { index => 0, type => 1 }, "{index}", "{type}", "_mpercolate" ],
-            [ { index => 0 }, "{index}", "_mpercolate" ],
-            [ {}, "_mpercolate" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-        },
-        serialize => "bulk",
-    },
-
-    'msearch' => {
-        body => { required => 1 },
-        doc  => "search-multi-search",
-        parts => { index => { multi => 1 }, type => { multi => 1 } },
-        paths => [
-            [ { index => 0, type => 1 }, "{index}", "{type}", "_msearch" ],
-            [ { index => 0 }, "{index}", "_msearch" ],
-            [ {}, "_msearch" ],
-        ],
-        qs        => { filter_path => "list", search_type => "enum" },
-        serialize => "bulk",
-    },
-
-    'mtermvectors' => {
-        body  => {},
-        doc   => "docs-multi-termvectors",
-        parts => { index => {}, type => {} },
-        paths => [
-            [   { index => 0, type => 1 }, "{index}",
-                "{type}", "_mtermvectors"
-            ],
-            [ { index => 0 }, "{index}", "_mtermvectors" ],
-            [ {}, "_mtermvectors" ],
-        ],
-        qs => {
-            field_statistics => "boolean",
-            fields           => "list",
-            filter_path      => "list",
-            ids              => "list",
-            offsets          => "boolean",
-            parent           => "string",
-            payloads         => "boolean",
-            positions        => "boolean",
-            preference       => "string",
-            realtime         => "boolean",
-            routing          => "string",
-            term_statistics  => "boolean",
-            version          => "number",
-            version_type     => "enum",
-        },
-    },
-
-    'percolate' => {
-        body  => {},
-        doc   => "search-percolate",
-        parts => {
-            id    => {},
-            index => { required => 1 },
-            type  => { required => 1 }
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 }, "{index}",
-                "{type}", "{id}",
-                "_percolate",
-            ],
-            [ { index => 0, type => 1 }, "{index}", "{type}", "_percolate" ],
-        ],
-        qs => {
-            allow_no_indices     => "boolean",
-            expand_wildcards     => "enum",
-            filter_path          => "list",
-            ignore_unavailable   => "boolean",
-            percolate_format     => "enum",
-            percolate_index      => "string",
-            percolate_preference => "string",
-            percolate_routing    => "string",
-            percolate_type       => "string",
-            preference           => "string",
-            routing              => "list",
-            version              => "number",
-            version_type         => "enum",
-        },
-    },
-
-    'ping' => {
-        doc    => "",
-        method => "HEAD",
-        parts  => {},
-        paths  => [ [ {} ] ],
-        qs     => {}
-    },
-
-    'put_script' => {
-        body   => { required => 1 },
-        doc    => "modules-scripting",
-        method => "PUT",
-        parts => { id => { required => 1 }, lang => { required => 1 } },
-        paths => [ [ { id => 2, lang => 1 }, "_scripts", "{lang}", "{id}" ] ],
-        qs => {
-            filter_path  => "list",
-            op_type      => "enum",
-            version      => "number",
-            version_type => "enum",
-        },
-    },
-
-    'put_template' => {
-        body   => { required => 1 },
-        doc    => "search-template",
-        method => "PUT",
-        parts => { id => { required => 1 } },
-        paths => [ [ { id => 2 }, "_search", "template", "{id}" ] ],
-        qs => {
-            filter_path  => "list",
-            op_type      => "enum",
-            version      => "number",
-            version_type => "enum",
-        },
-    },
-
-    'reindex' => {
-        body   => { required => 1 },
-        doc    => "plugins-reindex",
-        method => "POST",
-        parts  => {},
-        paths => [ [ {}, "_reindex" ] ],
-        qs => {
-            consistency         => "enum",
-            filter_path         => "list",
-            refresh             => "boolean",
-            requests_per_second => "number",
-            timeout             => "time",
-            wait_for_completion => "boolean",
-        },
-    },
-
-    'reindex_rethrottle' => {
-        doc    => "docs-reindex",
-        method => "POST",
-        parts  => { task_id => {} },
-        paths =>
-            [ [ { task_id => 1 }, "_reindex", "{task_id}", "_rethrottle" ] ],
-        qs => { filter_path => "list", requests_per_second => "number" },
-    },
-
-    'render_search_template' => {
-        body  => {},
-        doc   => "search-template",
-        parts => { id => {} },
-        paths => [
-            [ { id => 2 }, "_render", "template", "{id}" ],
-            [ {}, "_render", "template" ],
-        ],
-        qs => { filter_path => "list" },
-    },
-
-    'scroll' => {
-        body  => {},
-        doc   => "search-request-scroll",
-        parts => { scroll_id => {} },
-        paths => [
-            [ { scroll_id => 2 }, "_search", "scroll", "{scroll_id}" ],
-            [ {}, "_search", "scroll" ],
-        ],
-        qs => { filter_path => "list", scroll => "time" },
-    },
-
-    'search' => {
-        body  => {},
-        doc   => "search-search",
-        parts => { index => { multi => 1 }, type => { multi => 1 } },
-        paths => [
-            [ { index => 0, type => 1 }, "{index}", "{type}", "_search" ],
-            [ { index => 0 }, "{index}", "_search" ],
-            [ {}, "_search" ],
-        ],
-        qs => {
-            _source                  => "list",
-            _source_exclude          => "list",
-            _source_include          => "list",
-            allow_no_indices         => "boolean",
-            analyze_wildcard         => "boolean",
-            analyzer                 => "string",
-            default_operator         => "enum",
-            df                       => "string",
-            expand_wildcards         => "enum",
-            explain                  => "boolean",
-            fielddata_fields         => "list",
-            fields                   => "list",
-            filter_path              => "list",
-            from                     => "number",
-            ignore_unavailable       => "boolean",
-            lenient                  => "boolean",
-            lowercase_expanded_terms => "boolean",
-            preference               => "string",
-            q                        => "string",
-            request_cache            => "boolean",
-            routing                  => "list",
-            scroll                   => "time",
-            search_type              => "enum",
-            size                     => "number",
-            sort                     => "list",
-            stats                    => "list",
-            suggest_field            => "string",
-            suggest_mode             => "enum",
-            suggest_size             => "number",
-            suggest_text             => "string",
-            terminate_after          => "number",
-            timeout                  => "time",
-            track_scores             => "boolean",
-            version                  => "boolean",
-        },
-    },
-
-    'search_exists' => {
-        body   => {},
-        doc    => "search-exists",
-        method => "POST",
-        parts  => { index => { multi => 1 }, type => { multi => 1 } },
-        paths  => [
-            [   { index => 0, type => 1 }, "{index}",
-                "{type}", "_search",
-                "exists",
-            ],
-            [ { index => 0 }, "{index}", "_search", "exists" ],
-            [ {}, "_search", "exists" ],
-        ],
-        qs => {
-            allow_no_indices         => "boolean",
-            analyze_wildcard         => "boolean",
-            analyzer                 => "string",
-            default_operator         => "enum",
-            df                       => "string",
-            expand_wildcards         => "enum",
-            filter_path              => "list",
-            ignore_unavailable       => "boolean",
-            lenient                  => "boolean",
-            lowercase_expanded_terms => "boolean",
-            min_score                => "number",
-            preference               => "string",
-            q                        => "string",
-            routing                  => "string",
-        },
-    },
-
-    'search_shards' => {
-        doc   => "search-shards",
-        parts => { index => { multi => 1 }, type => { multi => 1 } },
-        paths => [
-            [   { index => 0, type => 1 }, "{index}",
-                "{type}", "_search_shards",
-            ],
-            [ { index => 0 }, "{index}", "_search_shards" ],
-            [ {}, "_search_shards" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-            preference         => "string",
-            routing            => "string",
-        },
-    },
-
-    'search_template' => {
-        body  => {},
-        doc   => "search-template",
-        parts => { index => { multi => 1 }, type => { multi => 1 } },
-        paths => [
-            [   { index => 0, type => 1 }, "{index}",
-                "{type}", "_search",
-                "template",
-            ],
-            [ { index => 0 }, "{index}", "_search", "template" ],
-            [ {}, "_search", "template" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            preference         => "string",
-            routing            => "list",
-            scroll             => "time",
-            search_type        => "enum",
-        },
-    },
-
-    'suggest' => {
-        body   => { required => 1 },
-        doc    => "search-suggesters",
-        method => "POST",
-        parts => { index => { multi => 1 } },
-        paths =>
-            [ [ { index => 0 }, "{index}", "_suggest" ], [ {}, "_suggest" ] ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            preference         => "string",
-            routing            => "string",
-        },
-    },
-
-    'termvectors' => {
-        body  => {},
-        doc   => "docs-termvectors",
-        parts => {
-            id    => {},
-            index => { required => 1 },
-            type  => { required => 1 }
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 }, "{index}",
-                "{type}", "{id}",
-                "_termvectors",
-            ],
-            [   { index => 0, type => 1 }, "{index}", "{type}",
-                "_termvectors"
-            ],
-        ],
-        qs => {
-            dfs              => "boolean",
-            field_statistics => "boolean",
-            fields           => "list",
-            filter_path      => "list",
-            offsets          => "boolean",
-            parent           => "string",
-            payloads         => "boolean",
-            positions        => "boolean",
-            preference       => "string",
-            realtime         => "boolean",
-            routing          => "string",
-            term_statistics  => "boolean",
-            version          => "number",
-            version_type     => "enum",
-        },
-    },
-
-    'update' => {
-        body   => {},
-        doc    => "docs-update",
-        method => "POST",
-        parts  => {
-            id    => { required => 1 },
-            index => { required => 1 },
-            type  => { required => 1 },
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 },
-                "{index}", "{type}", "{id}", "_update",
-            ],
-        ],
-        qs => {
-            consistency       => "enum",
-            fields            => "list",
-            filter_path       => "list",
-            lang              => "string",
-            parent            => "string",
-            refresh           => "boolean",
-            retry_on_conflict => "number",
-            routing           => "string",
-            script            => "string",
-            script_id         => "string",
-            scripted_upsert   => "boolean",
-            timeout           => "time",
-            timestamp         => "time",
-            ttl               => "time",
-            version           => "number",
-            version_type      => "enum",
-        },
-    },
-
-    'update_by_query' => {
-        body   => {},
-        doc    => "plugins-reindex",
-        method => "POST",
-        parts  => {
-            index => { multi => 1, required => 1 },
-            type  => { multi => 1 }
-        },
-        paths => [
-            [   { index => 0, type => 1 }, "{index}",
-                "{type}", "_update_by_query",
-            ],
-            [ { index => 0 }, "{index}", "_update_by_query" ],
-        ],
-        qs => {
-            _source                  => "list",
-            _source_exclude          => "list",
-            _source_include          => "list",
-            allow_no_indices         => "boolean",
-            analyze_wildcard         => "boolean",
-            analyzer                 => "string",
-            conflicts                => "enum",
-            consistency              => "enum",
-            default_operator         => "enum",
-            df                       => "string",
-            expand_wildcards         => "enum",
-            explain                  => "boolean",
-            fielddata_fields         => "list",
-            fields                   => "list",
-            filter_path              => "list",
-            from                     => "number",
-            ignore_unavailable       => "boolean",
-            lenient                  => "boolean",
-            lowercase_expanded_terms => "boolean",
-            preference               => "string",
-            q                        => "string",
-            refresh                  => "boolean",
-            request_cache            => "boolean",
-            requests_per_second      => "number",
-            routing                  => "list",
-            scroll                   => "time",
-            scroll_size              => "number",
-            search_timeout           => "time",
-            search_type              => "enum",
-            size                     => "number",
-            sort                     => "list",
-            stats                    => "list",
-            suggest_field            => "string",
-            suggest_mode             => "enum",
-            suggest_size             => "number",
-            suggest_text             => "string",
-            terminate_after          => "number",
-            timeout                  => "time",
-            track_scores             => "boolean",
-            version                  => "boolean",
-            version_type             => "boolean",
-            wait_for_completion      => "boolean",
-        },
-    },
-
-    'cat.aliases' => {
-        doc   => "cat-alias",
-        parts => { name => { multi => 1 } },
-        paths => [
-            [ { name => 2 }, "_cat", "aliases", "{name}" ],
-            [ {}, "_cat", "aliases" ],
-        ],
-        qs => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.allocation' => {
-        doc   => "cat-allocation",
-        parts => { node_id => { multi => 1 } },
-        paths => [
-            [ { node_id => 2 }, "_cat", "allocation", "{node_id}" ],
-            [ {}, "_cat", "allocation" ],
-        ],
-        qs => {
-            bytes          => "enum",
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.count' => {
-        doc   => "cat-count",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 2 }, "_cat", "count", "{index}" ],
-            [ {}, "_cat", "count" ],
-        ],
-        qs => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.fielddata' => {
-        doc   => "cat-fielddata",
-        parts => { fields => { multi => 1 } },
-        paths => [
-            [ { fields => 2 }, "_cat", "fielddata", "{fields}" ],
-            [ {}, "_cat", "fielddata" ],
-        ],
-        qs => {
-            bytes          => "enum",
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.health' => {
-        doc   => "cat-health",
-        parts => {},
-        paths => [ [ {}, "_cat", "health" ] ],
-        qs    => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            ts             => "boolean",
-            v              => "boolean",
-        },
-    },
-
-    'cat.help' => {
-        doc   => "cat",
-        parts => {},
-        paths => [ [ {}, "_cat" ] ],
-        qs => { help => "boolean" },
-    },
-
-    'cat.indices' => {
-        doc   => "cat-indices",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 2 }, "_cat", "indices", "{index}" ],
-            [ {}, "_cat", "indices" ],
-        ],
-        qs => {
-            bytes          => "enum",
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            pri            => "boolean",
-            v              => "boolean",
-        },
-    },
-
-    'cat.master' => {
-        doc   => "cat-master",
-        parts => {},
-        paths => [ [ {}, "_cat", "master" ] ],
-        qs    => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.nodeattrs' => {
-        doc   => "cat-nodeattrs",
-        parts => {},
-        paths => [ [ {}, "_cat", "nodeattrs" ] ],
-        qs    => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.nodes' => {
-        doc   => "cat-nodes",
-        parts => {},
-        paths => [ [ {}, "_cat", "nodes" ] ],
-        qs    => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.pending_tasks' => {
-        doc   => "cat-pending-tasks",
-        parts => {},
-        paths => [ [ {}, "_cat", "pending_tasks" ] ],
-        qs    => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.plugins' => {
-        doc   => "cat-plugins",
-        parts => {},
-        paths => [ [ {}, "_cat", "plugins" ] ],
-        qs    => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.recovery' => {
-        doc   => "cat-recovery",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 2 }, "_cat", "recovery", "{index}" ],
-            [ {}, "_cat", "recovery" ],
-        ],
-        qs => {
-            bytes          => "enum",
-            h              => "list",
-            help           => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.repositories' => {
-        doc   => "cat-repositories",
-        parts => {},
-        paths => [ [ {}, "_cat", "repositories" ] ],
-        qs    => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.segments' => {
-        doc   => "cat-segments",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 2 }, "_cat", "segments", "{index}" ],
-            [ {}, "_cat", "segments" ],
-        ],
-        qs => { h => "list", help => "boolean", v => "boolean" },
-    },
-
-    'cat.shards' => {
-        doc   => "cat-shards",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 2 }, "_cat", "shards", "{index}" ],
-            [ {}, "_cat", "shards" ],
-        ],
-        qs => {
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cat.snapshots' => {
-        doc   => "cat-snapshots",
-        parts => { repository => { multi => 1, required => 1 } },
-        paths =>
-            [ [ { repository => 2 }, "_cat", "snapshots", "{repository}" ] ],
-        qs => {
-            h                  => "list",
-            help               => "boolean",
-            ignore_unavailable => "boolean",
-            master_timeout     => "time",
-            v                  => "boolean",
-        },
-    },
-
-    'cat.thread_pool' => {
-        doc   => "cat-thread-pool",
-        parts => {},
-        paths => [ [ {}, "_cat", "thread_pool" ] ],
-        qs    => {
-            full_id        => "boolean",
-            h              => "list",
-            help           => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-            v              => "boolean",
-        },
-    },
-
-    'cluster.get_settings' => {
-        doc   => "cluster-update-settings",
-        parts => {},
-        paths => [ [ {}, "_cluster", "settings" ] ],
-        qs    => {
-            filter_path    => "list",
-            flat_settings  => "boolean",
-            master_timeout => "time",
-            timeout        => "time",
-        },
-    },
-
-    'cluster.health' => {
-        doc   => "cluster-health",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 2 }, "_cluster", "health", "{index}" ],
-            [ {}, "_cluster", "health" ],
-        ],
-        qs => {
-            filter_path                => "list",
-            level                      => "enum",
-            local                      => "boolean",
-            master_timeout             => "time",
-            timeout                    => "time",
-            wait_for_active_shards     => "number",
-            wait_for_nodes             => "string",
-            wait_for_relocating_shards => "number",
-            wait_for_status            => "enum",
-        },
-    },
-
-    'cluster.pending_tasks' => {
-        doc   => "cluster-pending",
-        parts => {},
-        paths => [ [ {}, "_cluster", "pending_tasks" ] ],
-        qs    => {
-            filter_path    => "list",
-            local          => "boolean",
-            master_timeout => "time"
-        },
-    },
-
-    'cluster.put_settings' => {
-        body   => {},
-        doc    => "cluster-update-settings",
-        method => "PUT",
-        parts  => {},
-        paths  => [ [ {}, "_cluster", "settings" ] ],
-        qs     => {
-            filter_path    => "list",
-            flat_settings  => "boolean",
-            master_timeout => "time",
-            timeout        => "time",
-        },
-    },
-
-    'cluster.reroute' => {
-        body   => {},
-        doc    => "cluster-reroute",
-        method => "POST",
-        parts  => {},
-        paths  => [ [ {}, "_cluster", "reroute" ] ],
-        qs     => {
-            dry_run        => "boolean",
-            explain        => "boolean",
-            filter_path    => "list",
-            master_timeout => "time",
-            metric         => "list",
-            timeout        => "time",
-        },
-    },
-
-    'cluster.state' => {
-        doc   => "cluster-state",
-        parts => { index => { multi => 1 }, metric => { multi => 1 } },
-        paths => [
-            [   { index => 3, metric => 2 }, "_cluster",
-                "state", "{metric}",
-                "{index}",
-            ],
-            [ { metric => 2 }, "_cluster", "state", "{metric}" ],
-            [ {}, "_cluster", "state" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            flat_settings      => "boolean",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-            master_timeout     => "time",
-        },
-    },
-
-    'cluster.stats' => {
-        doc   => "cluster-stats",
-        parts => { node_id => { multi => 1 } },
-        paths => [
-            [ { node_id => 3 }, "_cluster", "stats", "nodes", "{node_id}" ],
-            [ {}, "_cluster", "stats" ],
-        ],
-        qs => {
-            filter_path   => "list",
-            flat_settings => "boolean",
-            human         => "boolean",
-            timeout       => "time",
-        },
-    },
-
-    'indices.analyze' => {
-        body  => {},
-        doc   => "indices-analyze",
-        parts => { index => {} },
-        paths =>
-            [ [ { index => 0 }, "{index}", "_analyze" ], [ {}, "_analyze" ] ],
-        qs => {
-            analyzer     => "string",
-            attributes   => "list",
-            char_filter  => "list",
-            char_filters => "list",
-            explain      => "boolean",
-            field        => "string",
-            filter       => "list",
-            filter_path  => "list",
-            filters      => "list",
-            format       => "enum",
-            prefer_local => "boolean",
-            text         => "list",
-            tokenizer    => "string",
-        },
-    },
-
-    'indices.clear_cache' => {
-        doc    => "indices-clearcache",
-        method => "POST",
-        parts  => { index => { multi => 1 } },
-        paths  => [
-            [ { index => 0 }, "{index}", "_cache", "clear" ],
-            [ {}, "_cache", "clear" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            fielddata          => "boolean",
-            fields             => "list",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            query              => "boolean",
-            recycler           => "boolean",
-            request            => "boolean",
-        },
-    },
-
-    'indices.close' => {
-        doc    => "indices-open-close",
-        method => "POST",
-        parts  => { index => { multi => 1, required => 1 } },
-        paths  => [ [ { index => 0 }, "{index}", "_close" ] ],
-        qs     => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            master_timeout     => "time",
-            timeout            => "time",
-        },
-    },
-
-    'indices.create' => {
-        body   => {},
-        doc    => "indices-create-index",
-        method => "PUT",
-        parts  => { index => { required => 1 } },
-        paths  => [ [ { index => 0 }, "{index}" ] ],
-        qs     => {
-            filter_path      => "list",
-            master_timeout   => "time",
-            timeout          => "time",
-            update_all_types => "boolean",
-        },
-    },
-
-    'indices.delete' => {
-        doc    => "indices-delete-index",
-        method => "DELETE",
-        parts  => { index => { multi => 1, required => 1 } },
-        paths => [ [ { index => 0 }, "{index}" ] ],
-        qs => {
-            filter_path    => "list",
-            master_timeout => "time",
-            timeout        => "time"
-        },
-    },
-
-    'indices.delete_alias' => {
-        doc    => "indices-aliases",
-        method => "DELETE",
-        parts  => {
-            index => { multi => 1, required => 1 },
-            name  => { multi => 1, required => 1 },
-        },
-        paths =>
-            [ [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ] ],
-        qs => {
-            filter_path    => "list",
-            master_timeout => "time",
-            timeout        => "time"
-        },
-    },
-
-    'indices.delete_template' => {
-        doc    => "indices-templates",
-        method => "DELETE",
-        parts  => { name => { required => 1 } },
-        paths  => [ [ { name => 1 }, "_template", "{name}" ] ],
-        qs     => {
-            filter_path    => "list",
-            master_timeout => "time",
-            timeout        => "time"
-        },
-    },
-
-    'indices.delete_warmer' => {
-        doc    => "indices-warmers",
-        method => "DELETE",
-        parts  => {
-            index => { multi => 1, required => 1 },
-            name  => { multi => 1, required => 1 },
-        },
-        paths =>
-            [ [ { index => 0, name => 2 }, "{index}", "_warmer", "{name}" ] ],
-        qs => { filter_path => "list", master_timeout => "time" },
-    },
-
-    'indices.exists' => {
-        doc    => "indices-exists",
-        method => "HEAD",
-        parts  => { index => { multi => 1, required => 1 } },
-        paths => [ [ { index => 0 }, "{index}" ] ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-        },
-    },
-
-    'indices.exists_alias' => {
-        doc    => "indices-aliases",
-        method => "HEAD",
-        parts  => { index => { multi => 1 }, name => { multi => 1 } },
-        paths  => [
-            [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ],
-            [ { index => 0 }, "{index}", "_alias" ],
-            [ { name  => 1 }, "_alias",  "{name}" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-        },
-    },
-
-    'indices.exists_template' => {
-        doc    => "indices-templates",
-        method => "HEAD",
-        parts  => { name => { required => 1 } },
-        paths  => [ [ { name => 1 }, "_template", "{name}" ] ],
-        qs => { local => "boolean", master_timeout => "time" },
-    },
-
-    'indices.exists_type' => {
-        doc    => "indices-types-exists",
-        method => "HEAD",
-        parts  => {
-            index => { multi => 1, required => 1 },
-            type  => { multi => 1, required => 1 },
-        },
-        paths => [ [ { index => 0, type => 1 }, "{index}", "{type}" ] ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-        },
-    },
-
-    'indices.flush' => {
-        doc    => "indices-flush",
-        method => "POST",
-        parts  => { index => { multi => 1 } },
-        paths =>
-            [ [ { index => 0 }, "{index}", "_flush" ], [ {}, "_flush" ] ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            force              => "boolean",
-            ignore_unavailable => "boolean",
-            wait_if_ongoing    => "boolean",
-        },
-    },
-
-    'indices.flush_synced' => {
-        doc    => "indices-synced-flush",
-        method => "POST",
-        parts  => { index => { multi => 1 } },
-        paths  => [
-            [ { index => 0 }, "{index}", "_flush", "synced" ],
-            [ {}, "_flush", "synced" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-        },
-    },
-
-    'indices.forcemerge' => {
-        doc    => "indices-forcemerge",
-        method => "POST",
-        parts  => { index => { multi => 1 } },
-        paths  => [
-            [ { index => 0 }, "{index}", "_forcemerge" ],
-            [ {}, "_forcemerge" ],
-        ],
-        qs => {
-            allow_no_indices     => "boolean",
-            expand_wildcards     => "enum",
-            filter_path          => "list",
-            flush                => "boolean",
-            ignore_unavailable   => "boolean",
-            max_num_segments     => "number",
-            only_expunge_deletes => "boolean",
-            wait_for_merge       => "boolean",
-        },
-    },
-
-    'indices.get' => {
-        doc   => "indices-get-index",
-        parts => {
-            feature => { multi => 1 },
-            index   => { multi => 1, required => 1 }
-        },
-        paths => [
-            [ { feature => 1, index => 0 }, "{index}", "{feature}" ],
-            [ { index => 0 }, "{index}" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            flat_settings      => "boolean",
-            human              => "boolean",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-        },
-    },
-
-    'indices.get_alias' => {
-        doc   => "indices-aliases",
-        parts => { index => { multi => 1 }, name => { multi => 1 } },
-        paths => [
-            [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ],
-            [ { index => 0 }, "{index}", "_alias" ],
-            [ { name  => 1 }, "_alias",  "{name}" ],
-            [ {}, "_alias" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-        },
-    },
-
-    'indices.get_aliases' => {
-        doc   => "indices-aliases",
-        parts => { index => { multi => 1 }, name => { multi => 1 } },
-        paths => [
-            [ { index => 0, name => 2 }, "{index}", "_aliases", "{name}" ],
-            [ { index => 0 }, "{index}",  "_aliases" ],
-            [ { name  => 1 }, "_aliases", "{name}" ],
-            [ {}, "_aliases" ],
-        ],
-        qs =>
-            { filter_path => "list", local => "boolean", timeout => "time" },
-    },
-
-    'indices.get_field_mapping' => {
-        doc   => "indices-get-field-mapping",
-        parts => {
-            fields => { multi => 1, required => 1 },
-            index  => { multi => 1 },
-            type   => { multi => 1 },
-        },
-        paths => [
-            [   { fields => 4, index => 0, type => 2 }, "{index}",
-                "_mapping", "{type}",
-                "field",    "{fields}",
-            ],
-            [   { fields => 3, index => 0 }, "{index}",
-                "_mapping", "field",
-                "{fields}",
-            ],
-            [   { fields => 3, type => 1 }, "_mapping",
-                "{type}", "field",
-                "{fields}",
-            ],
-            [ { fields => 2 }, "_mapping", "field", "{fields}" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            include_defaults   => "boolean",
-            local              => "boolean",
-        },
-    },
-
-    'indices.get_mapping' => {
-        doc   => "indices-get-mapping",
-        parts => { index => { multi => 1 }, type => { multi => 1 } },
-        paths => [
-            [ { index => 0, type => 2 }, "{index}", "_mapping", "{type}" ],
-            [ { index => 0 }, "{index}",  "_mapping" ],
-            [ { type  => 1 }, "_mapping", "{type}" ],
-            [ {}, "_mapping" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-        },
-    },
-
-    'indices.get_settings' => {
-        doc   => "indices-get-settings",
-        parts => { index => { multi => 1 }, name => { multi => 1 } },
-        paths => [
-            [ { index => 0, name => 2 }, "{index}", "_settings", "{name}" ],
-            [ { index => 0 }, "{index}",   "_settings" ],
-            [ { name  => 1 }, "_settings", "{name}" ],
-            [ {}, "_settings" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            flat_settings      => "boolean",
-            human              => "boolean",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-        },
-    },
-
-    'indices.get_template' => {
-        doc   => "indices-templates",
-        parts => { name => { multi => 1 } },
-        paths =>
-            [ [ { name => 1 }, "_template", "{name}" ], [ {}, "_template" ] ],
-        qs => {
-            filter_path    => "list",
-            flat_settings  => "boolean",
-            local          => "boolean",
-            master_timeout => "time",
-        },
-    },
-
-    'indices.get_upgrade' => {
-        doc   => "indices-upgrade",
-        parts => { index => { multi => 1 } },
-        paths =>
-            [ [ { index => 0 }, "{index}", "_upgrade" ], [ {}, "_upgrade" ] ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            human              => "boolean",
-            ignore_unavailable => "boolean",
-        },
-    },
-
-    'indices.get_warmer' => {
-        doc   => "indices-warmers",
-        parts => {
-            index => { multi => 1 },
-            name  => { multi => 1 },
-            type  => { multi => 1 }
-        },
-        paths => [
-            [   { index => 0, name => 3, type => 1 },
-                "{index}", "{type}", "_warmer", "{name}",
-            ],
-            [ { index => 0, name => 2 }, "{index}", "_warmer", "{name}" ],
-            [ { index => 0 }, "{index}", "_warmer" ],
-            [ { name  => 1 }, "_warmer", "{name}" ],
-            [ {}, "_warmer" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            local              => "boolean",
-        },
-    },
-
-    'indices.open' => {
-        doc    => "indices-open-close",
-        method => "POST",
-        parts  => { index => { multi => 1, required => 1 } },
-        paths  => [ [ { index => 0 }, "{index}", "_open" ] ],
-        qs     => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            master_timeout     => "time",
-            timeout            => "time",
-        },
-    },
-
-    'indices.optimize' => {
-        doc    => "indices-optimize",
-        method => "POST",
-        parts  => { index => { multi => 1 } },
-        paths  => [
-            [ { index => 0 }, "{index}", "_optimize" ],
-            [ {}, "_optimize" ]
-        ],
-        qs => {
-            allow_no_indices     => "boolean",
-            expand_wildcards     => "enum",
-            filter_path          => "list",
-            flush                => "boolean",
-            ignore_unavailable   => "boolean",
-            max_num_segments     => "number",
-            only_expunge_deletes => "boolean",
-            wait_for_merge       => "boolean",
-        },
-    },
-
-    'indices.put_alias' => {
-        body   => {},
-        doc    => "indices-aliases",
-        method => "PUT",
-        parts  => {
-            index => { multi    => 1, required => 1 },
-            name  => { required => 1 }
-        },
-        paths =>
-            [ [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ] ],
-        qs => {
-            filter_path    => "list",
-            master_timeout => "time",
-            timeout        => "time"
-        },
-    },
-
-    'indices.put_mapping' => {
-        body   => { required => 1 },
-        doc    => "indices-put-mapping",
-        method => "PUT",
-        parts => { index => { multi => 1 }, type => { required => 1 } },
-        paths => [
-            [ { index => 0, type => 2 }, "{index}", "_mapping", "{type}" ],
-            [ { type => 1 }, "_mapping", "{type}" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            master_timeout     => "time",
-            timeout            => "time",
-            update_all_types   => "boolean",
-        },
-    },
-
-    'indices.put_settings' => {
-        body   => { required => 1 },
-        doc    => "indices-update-settings",
-        method => "PUT",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 0 }, "{index}", "_settings" ],
-            [ {}, "_settings" ]
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            flat_settings      => "boolean",
-            ignore_unavailable => "boolean",
-            master_timeout     => "time",
-        },
-    },
-
-    'indices.put_template' => {
-        body   => { required => 1 },
-        doc    => "indices-templates",
-        method => "PUT",
-        parts => { name => { required => 1 } },
-        paths => [ [ { name => 1 }, "_template", "{name}" ] ],
-        qs => {
-            create         => "boolean",
-            filter_path    => "list",
-            flat_settings  => "boolean",
-            master_timeout => "time",
-            order          => "number",
-            timeout        => "time",
-        },
-    },
-
-    'indices.put_warmer' => {
-        body   => { required => 1 },
-        doc    => "indices-warmers",
-        method => "PUT",
-        parts  => {
-            index => { multi    => 1 },
-            name  => { required => 1 },
-            type  => { multi    => 1 },
-        },
-        paths => [
-            [   { index => 0, name => 3, type => 1 },
-                "{index}", "{type}", "_warmer", "{name}",
-            ],
-            [ { index => 0, name => 2 }, "{index}", "_warmer", "{name}" ],
-            [ { name => 1 }, "_warmer", "{name}" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            master_timeout     => "time",
-            request_cache      => "boolean",
-        },
-    },
-
-    'indices.recovery' => {
-        doc   => "indices-recovery",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 0 }, "{index}", "_recovery" ],
-            [ {}, "_recovery" ]
-        ],
-        qs => {
-            active_only => "boolean",
-            detailed    => "boolean",
-            filter_path => "list",
-            human       => "boolean",
-        },
-    },
-
-    'indices.refresh' => {
-        doc    => "indices-refresh",
-        method => "POST",
-        parts  => { index => { multi => 1 } },
-        paths =>
-            [ [ { index => 0 }, "{index}", "_refresh" ], [ {}, "_refresh" ] ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            force              => "boolean",
-            ignore_unavailable => "boolean",
-        },
-    },
-
-    'indices.segments' => {
-        doc   => "indices-segments",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 0 }, "{index}", "_segments" ],
-            [ {}, "_segments" ]
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            human              => "boolean",
-            ignore_unavailable => "boolean",
-            verbose            => "boolean",
-        },
-    },
-
-    'indices.shard_stores' => {
-        doc   => "indices-shards-stores",
-        parts => { index => { multi => 1 } },
-        paths => [
-            [ { index => 0 }, "{index}", "_shard_stores" ],
-            [ {}, "_shard_stores" ],
-        ],
-        qs => {
-            allow_no_indices   => "boolean",
-            expand_wildcards   => "enum",
-            filter_path        => "list",
-            ignore_unavailable => "boolean",
-            status             => "list",
-        },
-    },
-
-    'indices.stats' => {
-        doc   => "indices-stats",
-        parts => { index => { multi => 1 }, metric => { multi => 1 } },
-        paths => [
-            [ { index => 0, metric => 2 }, "{index}", "_stats", "{metric}" ],
-            [ { index  => 0 }, "{index}", "_stats" ],
-            [ { metric => 1 }, "_stats",  "{metric}" ],
-            [ {}, "_stats" ],
-        ],
-        qs => {
-            completion_fields => "list",
-            fielddata_fields  => "list",
-            fields            => "list",
-            filter_path       => "list",
-            groups            => "list",
-            human             => "boolean",
-            level             => "enum",
-            types             => "list",
-        },
-    },
-
-    'indices.update_aliases' => {
-        body   => { required => 1 },
-        doc    => "indices-aliases",
-        method => "POST",
-        parts  => {},
-        paths => [ [ {}, "_aliases" ] ],
-        qs => {
-            filter_path    => "list",
-            master_timeout => "time",
-            timeout        => "time"
-        },
-    },
-
-    'indices.upgrade' => {
-        doc    => "indices-upgrade",
-        method => "POST",
-        parts  => { index => { multi => 1 } },
-        paths =>
-            [ [ { index => 0 }, "{index}", "_upgrade" ], [ {}, "_upgrade" ] ],
-        qs => {
-            allow_no_indices      => "boolean",
-            expand_wildcards      => "enum",
-            filter_path           => "list",
-            ignore_unavailable    => "boolean",
-            only_ancient_segments => "boolean",
-            wait_for_completion   => "boolean",
-        },
-    },
-
-    'indices.validate_query' => {
-        body  => {},
-        doc   => "search-validate",
-        parts => { index => { multi => 1 }, type => { multi => 1 } },
-        paths => [
-            [   { index => 0, type => 1 }, "{index}",
-                "{type}", "_validate",
-                "query",
-            ],
-            [ { index => 0 }, "{index}", "_validate", "query" ],
-            [ {}, "_validate", "query" ],
-        ],
-        qs => {
-            allow_no_indices         => "boolean",
-            analyze_wildcard         => "boolean",
-            analyzer                 => "string",
-            default_operator         => "enum",
-            df                       => "string",
-            expand_wildcards         => "enum",
-            explain                  => "boolean",
-            filter_path              => "list",
-            ignore_unavailable       => "boolean",
-            lenient                  => "boolean",
-            lowercase_expanded_terms => "boolean",
-            q                        => "string",
-            rewrite                  => "boolean",
-        },
-    },
-
-    'nodes.hot_threads' => {
-        doc   => "cluster-nodes-hot-threads",
-        parts => { node_id => { multi => 1 } },
-        paths => [
-            [ { node_id => 1 }, "_nodes", "{node_id}", "hot_threads" ],
-            [ {}, "_nodes", "hot_threads" ],
-        ],
-        qs => {
-            filter_path         => "list",
-            ignore_idle_threads => "boolean",
-            interval            => "time",
-            snapshots           => "number",
-            threads             => "number",
-            timeout             => "time",
-            type                => "enum",
-        },
-    },
-
-    'nodes.info' => {
-        doc   => "cluster-nodes-info",
-        parts => { metric => { multi => 1 }, node_id => { multi => 1 } },
-        paths => [
-            [   { metric => 2, node_id => 1 }, "_nodes",
-                "{node_id}", "{metric}",
-            ],
-            [ { metric  => 1 }, "_nodes", "{metric}" ],
-            [ { node_id => 1 }, "_nodes", "{node_id}" ],
-            [ {}, "_nodes" ],
-        ],
-        qs => {
-            filter_path   => "list",
-            flat_settings => "boolean",
-            human         => "boolean",
-            timeout       => "time",
-        },
-    },
-
-    'nodes.stats' => {
-        doc   => "cluster-nodes-stats",
-        parts => {
-            index_metric => { multi => 1 },
-            metric       => { multi => 1 },
-            node_id      => { multi => 1 },
-        },
-        paths => [
-            [   { index_metric => 4, metric => 3, node_id => 1 },
-                "_nodes", "{node_id}", "stats", "{metric}", "{index_metric}",
-            ],
-            [   { index_metric => 3, metric => 2 }, "_nodes",
-                "stats", "{metric}",
-                "{index_metric}",
-            ],
-            [   { metric => 3, node_id => 1 }, "_nodes",
-                "{node_id}", "stats",
-                "{metric}",
-            ],
-            [ { metric  => 2 }, "_nodes", "stats",     "{metric}" ],
-            [ { node_id => 1 }, "_nodes", "{node_id}", "stats" ],
-            [ {}, "_nodes", "stats" ],
-        ],
-        qs => {
-            completion_fields => "list",
-            fielddata_fields  => "list",
-            fields            => "list",
-            filter_path       => "list",
-            groups            => "boolean",
-            human             => "boolean",
-            level             => "enum",
-            timeout           => "time",
-            types             => "list",
-        },
-    },
-
-    'snapshot.create' => {
-        body   => {},
-        doc    => "modules-snapshots",
-        method => "PUT",
-        parts  => {
-            repository => { required => 1 },
-            snapshot   => { required => 1 }
-        },
-        paths => [
-            [   { repository => 1, snapshot => 2 }, "_snapshot",
-                "{repository}", "{snapshot}",
-            ],
-        ],
-        qs => {
-            filter_path         => "list",
-            master_timeout      => "time",
-            wait_for_completion => "boolean",
-        },
-    },
-
-    'snapshot.create_repository' => {
-        body   => { required => 1 },
-        doc    => "modules-snapshots",
-        method => "PUT",
-        parts => { repository => { required => 1 } },
-        paths => [ [ { repository => 1 }, "_snapshot", "{repository}" ] ],
-        qs => {
-            filter_path    => "list",
-            master_timeout => "time",
-            timeout        => "time",
-            verify         => "boolean",
-        },
-    },
-
-    'snapshot.delete' => {
-        doc    => "modules-snapshots",
-        method => "DELETE",
-        parts  => {
-            repository => { required => 1 },
-            snapshot   => { required => 1 }
-        },
-        paths => [
-            [   { repository => 1, snapshot => 2 }, "_snapshot",
-                "{repository}", "{snapshot}",
-            ],
-        ],
-        qs => { filter_path => "list", master_timeout => "time" },
-    },
-
-    'snapshot.delete_repository' => {
-        doc    => "modules-snapshots",
-        method => "DELETE",
-        parts  => { repository => { multi => 1, required => 1 } },
-        paths  => [ [ { repository => 1 }, "_snapshot", "{repository}" ] ],
-        qs     => {
-            filter_path    => "list",
-            master_timeout => "time",
-            timeout        => "time"
-        },
-    },
-
-    'snapshot.get' => {
-        doc   => "modules-snapshots",
-        parts => {
-            repository => { required => 1 },
-            snapshot   => { multi    => 1, required => 1 },
-        },
-        paths => [
-            [   { repository => 1, snapshot => 2 }, "_snapshot",
-                "{repository}", "{snapshot}",
-            ],
-        ],
-        qs => { filter_path => "list", master_timeout => "time" },
-    },
-
-    'snapshot.get_repository' => {
-        doc   => "modules-snapshots",
-        parts => { repository => { multi => 1 } },
-        paths => [
-            [ { repository => 1 }, "_snapshot", "{repository}" ],
-            [ {}, "_snapshot" ],
-        ],
-        qs => {
-            filter_path    => "list",
-            local          => "boolean",
-            master_timeout => "time"
-        },
-    },
-
-    'snapshot.restore' => {
-        body   => {},
-        doc    => "modules-snapshots",
-        method => "POST",
-        parts  => {
-            repository => { required => 1 },
-            snapshot   => { required => 1 }
-        },
-        paths => [
-            [   { repository => 1, snapshot => 2 }, "_snapshot",
-                "{repository}", "{snapshot}",
-                "_restore",
-            ],
-        ],
-        qs => {
-            filter_path         => "list",
-            master_timeout      => "time",
-            wait_for_completion => "boolean",
-        },
-    },
-
-    'snapshot.status' => {
-        doc   => "modules-snapshots",
-        parts => { repository => {}, snapshot => { multi => 1 } },
-        paths => [
-            [   { repository => 1, snapshot => 2 }, "_snapshot",
-                "{repository}", "{snapshot}",
-                "_status",
-            ],
-            [ { repository => 1 }, "_snapshot", "{repository}", "_status" ],
-            [ {}, "_snapshot", "_status" ],
-        ],
-        qs => { filter_path => "list", master_timeout => "time" },
-    },
-
-    'snapshot.verify_repository' => {
-        doc    => "modules-snapshots",
-        method => "POST",
-        parts  => { repository => { required => 1 } },
-        paths  => [
-            [ { repository => 1 }, "_snapshot", "{repository}", "_verify" ],
-        ],
-        qs => {
-            filter_path    => "list",
-            master_timeout => "time",
-            timeout        => "time"
-        },
-    },
-
-    'tasks.cancel' => {
-        doc    => "tasks-cancel",
-        method => "POST",
-        parts  => { task_id => {} },
-        paths  => [
-            [ { task_id => 1 }, "_tasks", "{task_id}", "_cancel" ],
-            [ {}, "_tasks", "_cancel" ],
-        ],
-        qs => {
-            actions     => "list",
-            filter_path => "list",
-            node_id     => "list",
-            parent_node => "string",
-            parent_task => "string",
-        },
-    },
-
-    'tasks.list' => {
-        doc   => "tasks-list",
-        parts => { task_id => {} },
-        paths =>
-            [ [ { task_id => 1 }, "_tasks", "{task_id}" ], [ {}, "_tasks" ] ],
-        qs => {
-            actions             => "list",
-            detailed            => "boolean",
-            filter_path         => "list",
-            node_id             => "list",
-            parent_node         => "string",
-            parent_task         => "string",
-            wait_for_completion => "boolean",
-        },
-    },
-
-#=== AUTOGEN - END ===
-
-);
-
-__PACKAGE__->_qs_init( \%API );
-1;
-
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Role::API - This class contains the spec for the Elasticsearch APIs
-
-=head1 VERSION
-
-version 6.81
-
-=head1 DESCRIPTION
-
-All of the Elasticsearch APIs are defined in this role. The example given below
-is the definition for the L<Search::Elasticsearch::Client::2_0::Direct/index()> method:
-
-    'index' => {
-        body   => { required => 1 },
-        doc    => "docs-index_",
-        method => "POST",
-        parts  => {
-            id    => {},
-            index => { required => 1 },
-            type  => { required => 1 }
-        },
-        paths => [
-            [   { id => 2, index => 0, type => 1 }, "{index}",
-                "{type}", "{id}"
-            ],
-            [ { index => 0, type => 1 }, "{index}", "{type}" ],
-        ],
-        qs => {
-            consistency  => "enum",
-            filter_path  => "list",
-            op_type      => "enum",
-            parent       => "string",
-            refresh      => "boolean",
-            routing      => "string",
-            timeout      => "time",
-            timestamp    => "time",
-            ttl          => "time",
-            version      => "number",
-            version_type => "enum",
-        },
-    },
-
-These definitions can be used by different L<Search::Elasticsearch::Role::Client>
-implementations to provide distinct user interfaces.
-
-=head1 METHODS
-
-=head2 C<api()>
-
-    $defn = $api->api($name);
-
-The only method in this class is the C<api()> method which takes the name
-of the I<action> and returns its definition.  Actions in the
-C<indices> or C<cluster> namespace use the namespace as a prefix, eg:
-
-    $defn = $e->api('indices.create');
-    $defn = $e->api('cluster.node_stats');
-
-=head1 SEE ALSO
-
-=over
-
-=item *
-
-L<Search::Elasticsearch::Role::API>
-
-=item *
-
-L<Search::Elasticsearch::Client::2_0::Direct>
-
-=back
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
-
-__END__
-
-# ABSTRACT: This class contains the spec for the Elasticsearch APIs
-
diff --git a/lib/Search/Elasticsearch/Client/2_0/TestServer.pm b/lib/Search/Elasticsearch/Client/2_0/TestServer.pm
deleted file mode 100644
index 468fa33..0000000
--- a/lib/Search/Elasticsearch/Client/2_0/TestServer.pm
+++ /dev/null
@@ -1,59 +0,0 @@
-package Search::Elasticsearch::Client::2_0::TestServer;
-$Search::Elasticsearch::Client::2_0::TestServer::VERSION = '6.81';
-use strict;
-use warnings;
-
-#===================================
-sub command_line {
-#===================================
-    my ( $class, $ts, $pid_file, $dir, $transport, $http ) = @_;
-
-    return (
-        $ts->es_home . '/bin/elasticsearch',
-        '-p',
-        $pid_file->filename,
-        map {"-Des.$_"} (
-            'path.data=' . $dir,
-            'network.host=127.0.0.1',
-            'cluster.name=es_test',
-            'discovery.zen.ping_timeout=1s',
-            'discovery.zen.ping.multicast.enabled=false',
-            'discovery.zen.ping.unicast.hosts=127.0.0.1:' . $ts->es_port,
-            'transport.tcp.port=' . $transport,
-            'http.port=' . $http,
-            @{ $ts->conf }
-        )
-    );
-}
-
-1
-
-# ABSTRACT: Client-specific backend for Search::Elasticsearch::TestServer
-
-__END__
-
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::TestServer - Client-specific backend for Search::Elasticsearch::TestServer
-
-=head1 VERSION
-
-version 6.81
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
diff --git a/lib/Search/Elasticsearch/Client/7_0.pm b/lib/Search/Elasticsearch/Client/7_0.pm
new file mode 100644
index 0000000..6449809
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0.pm
@@ -0,0 +1,72 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0;
+
+our $VERSION='8.00';
+use Search::Elasticsearch 8.00 ();
+
+1;
+
+__END__
+
+# ABSTRACT: Thin client with full support for Elasticsearch 7.x APIs
+
+=head1 DESCRIPTION
+
+The L<Search::Elasticsearch::Client::7_0> package provides a client
+compatible with Elasticsearch 7.x.  It should be used in conjunction
+with L<Search::Elasticsearch> as follows:
+
+    $e = Search::Elasticsearch->new(
+        client => "7_0::Direct"
+    );
+
+See L<Search::Elasticsearch::Client::7_0::Direct> for documentation
+about how to use the client itself.
+
+=head1 PREVIOUS VERSIONS OF ELASTICSEARCH
+
+This version of the client supports the Elasticsearch 7.0 branch,
+which is not backwards compatible with earlier branches.
+
+If you need to talk to a version of Elasticsearch before 7.0.0, please
+install one of the following packages:
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Client::6_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::5_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::2_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::1_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::0_90>
+
+=back
diff --git a/lib/Search/Elasticsearch/Client/7_0/Async.pm b/lib/Search/Elasticsearch/Client/7_0/Async.pm
new file mode 100644
index 0000000..b46702f
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Async.pm
@@ -0,0 +1,72 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Async;
+
+our $VERSION='8.00';
+use Search::Elasticsearch::Client::7_0 7.00 ();
+
+1;
+
+__END__
+
+# ABSTRACT: Thin async client with full support for Elasticsearch 7.x APIs
+
+=head1 DESCRIPTION
+
+The L<Search::Elasticsearch::Client::7_0::Async> package provides a client
+compatible with Elasticsearch 7.x.  It should be used in conjunction
+with L<Search::Elasticsearch::Async> as follows:
+
+    $e = Search::Elasticsearch::Async->new(
+        client => "7_0::Direct"
+    );
+
+See L<Search::Elasticsearch::Client::7_0::Direct> for documentation
+about how to use the client itself.
+
+=head1 PREVIOUS VERSIONS OF ELASTICSEARCH
+
+This version of the client supports the Elasticsearch 7.0 branch,
+which is not backwards compatible with earlier branches.
+
+If you need to talk to a version of Elasticsearch before 7.0.0, please
+install one of the following packages:
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Client::6_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::5_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::2_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::1_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::0_90::Async>
+
+=back
diff --git a/lib/Search/Elasticsearch/Client/7_0/Async/Bulk.pm b/lib/Search/Elasticsearch/Client/7_0/Async/Bulk.pm
new file mode 100644
index 0000000..1e664bb
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Async/Bulk.pm
@@ -0,0 +1,498 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Async::Bulk;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::Bulk',
+    'Search::Elasticsearch::Role::Is_Async';
+
+use Search::Elasticsearch::Util qw(parse_params throw);
+use Scalar::Util qw(weaken blessed);
+use Promises qw(deferred);
+use Try::Tiny;
+use namespace::clean;
+
+has 'on_fatal' => ( is => 'lazy' );
+
+#===================================
+sub _build_on_fatal {
+#===================================
+    my $self = shift;
+    return sub {
+        warn("Fatal bulk error: @_");
+    };
+}
+
+#===================================
+sub add_action {
+#===================================
+    my $self      = shift;
+    my $buffer    = $self->_buffer;
+    my $max_size  = $self->max_size;
+    my $max_count = $self->max_count;
+    my $max_time  = $self->max_time;
+
+    my $deferred = deferred;
+    my @actions  = @_;
+
+    my $weak_add;
+    my $add = sub {
+        while (@actions) {
+            my @json = try {
+                $self->_encode_action( splice( @actions, 0, 2 ) );
+            }
+            catch {
+                $self->on_fatal->($_);
+                $deferred->reject($_);
+                ();
+            };
+            return unless @json;
+
+            push @$buffer, @json;
+
+            my $size = $self->_buffer_size;
+            $size += length($_) + 1 for @json;
+            $self->_buffer_size($size);
+
+            my $count = $self->_buffer_count( $self->_buffer_count + 1 );
+
+            next
+                unless ( $max_size and $size >= $max_size )
+                || ( $max_count and $count >= $max_count )
+                || ( $max_time  and time >= $self->_last_flush + $max_time );
+
+            return $self->flush->done( $weak_add,
+                sub { $deferred->reject(@_) } );
+        }
+        return $deferred->resolve;
+
+    };
+
+    weaken( $weak_add = $add );
+    $add->();
+    return $deferred->promise;
+
+}
+
+#===================================
+sub flush {
+#===================================
+    my $self = shift;
+
+    my $size  = $self->_buffer_size;
+    my $count = $self->_buffer_count;
+
+    $self->_last_flush(time);
+
+    unless ($size) {
+        return deferred->resolve( { items => [] } )->promise;
+    }
+
+    my @items = ( @{ $self->_buffer } );
+    $self->clear_buffer;
+
+    if ( $self->verbose ) {
+        local $| = 1;
+        print ".";
+    }
+
+    my $promise
+        = $self->es->bulk( %{ $self->_bulk_args }, body => \@items )->catch(
+        sub {
+            my $error = shift;
+            if ( $error->is( 'Cxn', 'NoNodes' ) ) {
+                push @{ $self->_buffer }, @items;
+                $self->_buffer_size( $self->_buffer_size + $size );
+                $self->_buffer_count( $self->_buffer_count + $count );
+            }
+            die $error;
+        }
+        );
+    $promise->then( sub { $self->_report( \@items, @_ ) },
+        sub { $self->on_fatal(@_) } );
+    return $promise;
+}
+
+1;
+
+# ABSTRACT: A helper module for the Bulk API
+
+=head1 SYNOPSIS
+
+    use Search::Elasticsearch::Async;
+
+    my $es   = Search::Elasticsearch::Async->new;
+    my $bulk = $es->bulk_helper(
+        index   => 'my_index',
+        type    => 'my_type'
+    );
+
+    # Index docs:
+    $promise = $bulk->index({ id => 1, source => { foo => 'bar' }});
+    $promise = $bulk->add_action( index => { id => 1, source => { foo=> 'bar' }});
+
+    # Create docs:
+    $promise = $bulk->create({ id => 1, source => { foo => 'bar' }});
+    $promise = $bulk->add_action( create => { id => 1, source => { foo=> 'bar' }});
+    $promise = $bulk->create_docs({ foo => 'bar' })
+
+    # Delete docs:
+    $promise = $bulk->delete({ id => 1});
+    $promise = $bulk->add_action( delete => { id => 1 });
+    $promise = $bulk->delete_ids(1,2,3)
+
+    # Update docs:
+    $promise = $bulk->update({ id => 1, script => '...' });
+    $promise = $bulk->add_action( update => { id => 1, script => '...' });
+
+    # Manual flush
+    $promise = $bulk->flush;
+
+=head1 DESCRIPTION
+
+This module provides an async wrapper for the L<Search::Elasticsearch::Client::7_0::Direct/bulk()>
+method which makes it easier to run multiple create, index, update or delete
+actions in a single request.
+
+The L<Search::Elasticsearch::Client::7_0::Async::Bulk> module acts as a queue, buffering up actions
+until it reaches a maximum count of actions, or a maximum size of JSON request
+body, at which point it issues a C<bulk()> request.
+
+Once you have finished adding actions, call L</flush()> to force the final
+C<bulk()> request on the items left in the queue.
+
+This class does L<Search::Elasticsearch::Client::7_0::Role::Bulk> and
+L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 CREATING A NEW INSTANCE
+
+=head2 C<new()>
+
+    $bulk = $es->bulk_helper(
+
+        index       => 'default_index',     # optional
+        type        => 'default_type',      # optional
+        %other_bulk_params                  # optional
+
+        max_count   => 1_000,               # optional
+        max_size    => 1_000_000,           # optional
+        max_time    => 6,                   # optional
+
+        verbose     => 0 | 1,               # optional
+
+        on_success  => sub {...},           # optional
+        on_error    => sub {...},           # optional
+        on_conflict => sub {...},           # optional
+        on_fatal    => sub {...},           # optional
+
+    );
+
+The C<bulk_helper> method loads L<Search::Elasticsearch::Client::7_0::Async::Bulk>,
+calls L</new()> with the specified parameters and returns a new C<$bulk> object.
+
+The C<index> and C<type> parameters provide default values for
+C<index> and C<type>, which can be overridden in each action.
+You can also pass any other values which are accepted
+by the L<bulk()|Search::Elasticsearch::Client::7_0::Direct/bulk()> method.
+
+See L</flush()> for more information about the other parameters.
+
+=head1 FLUSHING THE BUFFER
+
+=head2 C<flush()>
+
+    $promise = $bulk->flush;
+
+The C<flush()> method sends all buffered actions to Elasticsearch using
+a L<bulk()|Search::Elasticsearch::Client::7_0::Direct/bulk()> request and returns
+a L<Promise>, which is rejected if the bulk request fails or if any of
+the C<on_success>, C<on_error> or C<on_conflict> callbacks throws an
+exception, otherwise it is resolved with the items that have been flushed.
+
+=head2 Auto-flushing
+
+An automatic L</flush()> is triggered whenever the C<max_count>, C<max_size>,
+or C<max_time> threshold is breached.  This causes all actions in the buffer to be
+sent to Elasticsearch.
+
+=over
+
+=item * C<max_count>
+
+The maximum number of actions to allow before triggering a L</flush()>.
+This can be disabled by setting C<max_count> to C<0>. Defaults to
+C<1,000>.
+
+=item * C<max_size>
+
+The maximum size of JSON request body to allow before triggering a
+L</flush()>.  This can be disabled by setting C<max_size> to C<0>.  Defaults
+to C<1_000,000> bytes.
+
+=item * C<max_time>
+
+The maximum number of seconds to wait before triggering a flush.  Defaults
+to C<0> seconds, which means that it is disabled.  B<Note:> This timeout
+is only triggered when new items are added to the queue, not in the background.
+
+=back
+
+=head2 Errors when flushing
+
+There are two levels of error which can be thrown when L</flush()>
+is called, either manually or automatically.
+
+=over
+
+=item * Temporary Elasticsearch errors
+
+A C<Cxn> error like a C<NoNodes> error which indicates that your cluster is down.
+These errors do not clear the buffer, as they can be retried later on.
+These errors are reported via the C<on_fatal> callback and by rejecting
+the promise returned by L</flush()>, L</index()> etc.
+
+=item * Action errors
+
+Individual actions may fail. For instance, a C<create> action will fail
+if a document with the same C<index>, C<type> and C<id> already exists.
+These action errors are reported via L<callbacks|/Using callbacks>.
+
+=back
+
+=head2 Using callbacks
+
+By default, any I<Action errors> (see above) cause warnings to be
+written to C<STDERR>.  However, you can use the C<on_error>, C<on_conflict>
+and C<on_success> callbacks for more fine-grained control.
+
+All callbacks receive the following arguments:
+
+=over
+
+=item C<$action>
+
+The name of the action, ie C<index>, C<create>, C<update> or C<delete>.
+
+=item C<$response>
+
+The response that Elasticsearch returned for this action.
+
+=item C<$i>
+
+The index of the action, ie the first action in the flush request
+will have C<$i> set to C<0>, the second will have C<$i> set to C<1> etc.
+
+=back
+
+=head3 C<on_success>
+
+    $bulk = $e->bulk_helper->new(
+        on_success  => sub {
+            my ($action,$response,$i) = @_;
+            # do something
+        },
+    );
+
+The C<on_success> callback is called for every action that has a successful
+response.
+
+=head3 C<on_conflict>
+
+    $bulk = $e->bulk_helper->new(
+        on_conflict  => sub {
+            my ($action,$response,$i,$version) = @_;
+            # do something
+        },
+    );
+
+The C<on_conflict> callback is called for actions that have triggered
+a C<Conflict> error, eg trying to C<create> a document which already
+exists.  The C<$version> argument will contain the version number
+of the document currently stored in Elasticsearch (if found).
+
+=head3 C<on_error>
+
+    $bulk = $e->bulk_helper->new(
+        on_error  => sub {
+            my ($action,$response,$i) = @_;
+            # do something
+        },
+    );
+
+The C<on_error> callback is called for any error (unless the C<on_conflict>)
+callback has already been called).
+
+=head2 Disabling callbacks and autoflush
+
+If you want to be in control of flushing, and you just want to receive
+the raw response that Elasticsearch sends instead of using callbacks,
+then you can do so as follows:
+
+    $bulk = $e->bulk_helper->new(
+        max_count   => 0,
+        max_size    => 0,
+        on_error    => undef
+    );
+
+    $bulk->add_actions(....);
+    $bulk->flush
+         ->then(
+            sub { my $response = shift; ...},
+            sub { my $error = shift; ....}
+           )
+
+=head1 CREATE, INDEX, UPDATE, DELETE
+
+The L</add_action()>, L</create()>, L</create_docs()>, L</index()>,
+L</delete()>, L</delete_ids()> and L</update()> methods all return a Promise,
+which is resolved once the actions have been added to the queue and
+AFTER the queue has been flushed (if necessary).  It is important
+to wait for the promise to be resolved before continuing to queue more
+items, otherwise the pending requests may fill up your available memory.
+
+For instance:
+
+    use Promises qw(deferred);
+    use Scalar::Util qw(weaken);
+
+    $bulk = $es->bulk_helper;
+
+    sub bulk_index {
+        my $d = deferred;
+        my $weak_cb;
+        my $cb = sub {
+            my @docs = get_next_docs_from_somewhere();
+            unless (@docs) {
+                return $d->resolve;
+            }
+            $bulk->index(@docs)
+                 ->then(
+                      $weak_cb,
+                      sub { $d->reject(@_) }
+                   );
+        };
+        weaken ($weak_cb = $cb);
+        $cb->();
+        $d->promise->then( sub {$b->flush} );
+    }
+
+=head2 C<add_action()>
+
+    $promise = $bulk->add_action(
+        create => { ...params... },
+        index  => { ...params... },
+        update => { ...params... },
+        delete => { ...params... }
+    );
+
+The C<add_action()> method allows you to add multiple C<create>, C<index>,
+C<update> and C<delete> actions to the queue. The first value is the action
+type, and the second value is the parameters that describe that action.
+See the individual helper methods below for details.
+
+B<Note:> Parameters like C<index> or C<type> can be specified as C<index> or as
+C<_index>, so the following two lines are equivalent:
+
+    index => { index  => 'index', type  => 'type', id  => 1, source => {...}},
+    index => { _index => 'index', _type => 'type', _id => 1, source => {...}},
+
+B<Note:> The C<index> and C<type> parameters can be specified in the
+params for any action, but if not specified, will default to the C<index>
+and C<type> values specified in L</new()>.  These are required parameters:
+they must be specified either in L</new()> or in every action.
+
+=head2 C<create()>
+
+    $promise = $bulk->create(
+        { index => 'custom_index',         source => { doc body }},
+        { type  => 'custom_type', id => 1, source => { doc body }},
+        ...
+    );
+
+The C<create()> helper method allows you to add multiple C<create> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::7_0::Direct/create()>
+except that the document body should be passed as the C<source> or C<_source>
+parameter, instead of as C<body>.
+
+=head2 C<create_docs()>
+
+    $promise = $bulk->create_docs(
+        { doc body },
+        { doc body },
+        ...
+    );
+
+The C<create_docs()> helper is a shorter form of L</create()> which can be used
+when you are using the default C<index> and C<type> as set in L</new()>
+and you are not specifying a custom C<id> per document.  In this case,
+you can just pass the individual document bodies.
+
+=head2 C<index()>
+
+    $promise = $bulk->index(
+        { index => 'custom_index',         source => { doc body }},
+        { type  => 'custom_type', id => 1, source => { doc body }},
+        ...
+    );
+
+The C<index()> helper method allows you to add multiple C<index> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::7_0::Direct/index()>
+except that the document body should be passed as the C<source> or C<_source>
+parameter, instead of as C<body>.
+
+=head2 C<delete()>
+
+    $promise = $bulk->delete(
+        { index => 'custom_index', id => 1},
+        { type  => 'custom_type',  id => 2},
+        ...
+    );
+
+The C<delete()> helper method allows you to add multiple C<delete> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::7_0::Direct/delete()>.
+
+=head2 C<delete_ids()>
+
+    $bulk->delete_ids(1,2,3...)
+
+The C<delete_ids()> helper method can be used when all of the documents you
+want to delete have the default C<index> and C<type> as set in L</new()>.
+In this case, all you have to do is to pass in a list of IDs.
+
+=head2 C<update()>
+
+    $promise = $bulk->update(
+        { id            => 1,
+          doc           => { partial doc },
+          doc_as_upsert => 1
+        },
+        { id            => 2,
+          script        => { script },
+          upsert        => { upsert doc }
+        },
+        ...
+    );
+
+
+The C<update()> helper method allows you to add multiple C<update> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::7_0::Direct/update()>.
+An update can either use a I<partial doc> which gets merged with an existing
+doc (example 1 above), or can use a C<script> to update an existing doc
+(example 2 above). More information on C<script> can be found here:
+L<Search::Elasticsearch::Client::7_0::Direct/update()>.
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Async/Scroll.pm b/lib/Search/Elasticsearch/Client/7_0/Async/Scroll.pm
new file mode 100644
index 0000000..347bfe8
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Async/Scroll.pm
@@ -0,0 +1,528 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Async::Scroll;
+
+use Moo;
+use Search::Elasticsearch::Util qw(parse_params throw);
+use Search::Elasticsearch::Async::Util qw(thenable);
+use Scalar::Util qw(weaken blessed);
+use Promises qw(deferred);
+use namespace::clean;
+
+has 'one_at_a_time' => ( is => 'ro' );
+has 'on_start'      => ( is => 'ro', clearer => '_clear_on_start' );
+has 'on_results'    => ( is => 'ro', clearer => '_clear_on_results' );
+has 'on_error'      => ( is => 'lazy', clearer => '_clear_on_error' );
+has '_guard'        => ( is => 'rwp', clearer => '_clear__guard' );
+
+with 'Search::Elasticsearch::Role::Is_Async',
+    'Search::Elasticsearch::Client::7_0::Role::Scroll';
+
+#===================================
+sub BUILDARGS {
+#===================================
+    my ( $class, $search_params ) = parse_params(@_);
+
+        my %params;
+    for (qw(es on_start on_result on_results on_error)) {
+        my $val = delete $search_params->{$_};
+        next unless defined $val;
+        $params{$_} = $val;
+    }
+
+    $params{scroll} = $search_params->{scroll} ||= '1m';
+    $params{search_params} = $search_params;
+
+    if ( $params{on_result} ) {
+        $params{on_results}    = delete $params{on_result};
+        $params{one_at_a_time} = 1;
+    }
+    elsif ( !$params{on_results} ) {
+        throw( 'Param', 'Missing required param: on_results or on_result' );
+    }
+    return \%params;
+}
+
+#===================================
+sub _build_on_error {
+#===================================
+    sub { warn "Scroll error: @_"; die @_ }
+}
+
+#===================================
+sub start {
+#===================================
+    my $self = shift;
+    $self->_set__guard($self);
+
+    $self->es->search( $self->search_params )->then(
+        sub {
+            $self->_first_results(@_);
+        }
+        )->then(
+        sub {
+            $self->_fetch_loop;
+        }
+        )->catch(
+        sub {
+            $self->on_error->(@_);
+            @_;
+        }
+        )->finally(
+        sub {
+            $self->finish;
+            $self->_clear__guard;
+        }
+        );
+}
+
+#===================================
+sub _first_results {
+#===================================
+    my ( $self, $results ) = @_;
+
+    my $total = $results->{hits}{total};
+    if (ref $total) {
+        $total = $total->{value};
+    }
+    $self->_set_total($total);
+    $self->_set_max_score( $results->{hits}{max_score} );
+    $self->_set_aggregations( $results->{aggregations} );
+    $self->_set_facets( $results->{facets} );
+    $self->_set_suggest( $results->{suggest} );
+    $self->_set_took( $results->{took} );
+    $self->_set_total_took( $results->{took} );
+
+    if ($total) {
+        $self->_set__scroll_id( $results->{_scroll_id} );
+    }
+    else {
+        $self->finish;
+    }
+
+    $self->on_start && $self->on_start->($self);
+
+    my $hits = $results->{hits}{hits};
+    return unless @$hits;
+    return $self->_push_results($hits);
+}
+
+#===================================
+sub _next_results {
+#===================================
+    my ( $self, $results ) = @_;
+    $self->_set__scroll_id( $results->{_scroll_id} );
+    $self->_set_total_took( $self->total_took + $results->{took} );
+
+    my $hits = $results->{hits}{hits};
+    return $self->finish
+        unless @$hits;
+    $self->_push_results($hits);
+}
+
+#===================================
+sub _fetch_loop {
+#===================================
+    my $self = shift;
+    my $d    = deferred;
+
+    my $weak_loop;
+    my $loop = sub {
+        if ( $self->is_finished ) {
+            return $d->resolve;
+        }
+        $self->scroll_request->then( sub { $self->_next_results(@_) } )
+            ->done( $weak_loop, sub { $d->reject(@_) } );
+    };
+    weaken( $weak_loop = $loop );
+    $loop->();
+    return $d->promise;
+}
+
+#===================================
+sub _push_results {
+#===================================
+    my $self       = shift;
+    my $it         = $self->_results_iterator(@_);
+    my $on_results = $self->on_results;
+
+    my $deferred = deferred;
+
+    my $weak_process;
+    my $process = sub {
+        while ( !$self->is_finished ) {
+            my @results  = $it->() or last;
+            my @response = $on_results->(@results);
+            my $promise  = thenable(@response) or next;
+            return $promise->done( $weak_process,
+                sub { $deferred->reject(@_) } );
+        }
+        $deferred->resolve;
+    };
+    weaken( $weak_process = $process );
+    $process->();
+    return $deferred->promise;
+}
+
+#===================================
+sub _results_iterator {
+#===================================
+    my $self    = shift;
+    my @results = @{ shift() };
+
+    $self->one_at_a_time
+        ? sub { splice @results, 0, 1 }
+        : sub { splice @results };
+}
+
+#===================================
+sub finish {
+#===================================
+    my $self = shift;
+    $self->_set_is_finished(1);
+
+    my $scroll_id = $self->_scroll_id;
+    $self->_clear_scroll_id;
+
+    if ( !$scroll_id || $self->_pid != $$ ) {
+        my $d = deferred;
+        $d->resolve();
+        return $d->promise;
+    }
+
+    my %args = ( body => { scroll_id => $scroll_id } );
+
+    $self->es->clear_scroll(%args)->then(
+        sub {
+            $self->_clear_on_start;
+            $self->_clear_on_results;
+            $self->_clear_on_error;
+        },
+        sub { }
+    );
+}
+
+1;
+
+# ABSTRACT: A helper module for scrolled searches
+
+=head1 SYNOPSIS
+
+    use Search::Elasticsearch::Async;
+
+    my $es = Search::Elasticsearch::Async->new;
+
+    my $scroll = $es->scroll_helper
+        index       => 'my_index',
+        body => {
+            size    => 1000,
+            sort    => '_doc',
+            query   => {...}
+        },
+        on_start    => \&on_start,
+        on_result   => \&on_result,
+      | on_results  => \&on_results,
+        on_error    => \&on_error
+    );
+
+    $scroll->start->then( sub {say "Done"}, sub { warn @_ } );
+
+    sub on_start {
+        my $scroll = shift;
+        say "Total hits: ". $scroll->total;
+    }
+
+    sub on_result {
+        my $doc = shift;
+        do_something($doc);
+    }
+
+    sub on_results {
+        for my $doc (@_) {
+            do_something($doc)
+        }
+    }
+
+    sub on_error {
+        my $error = shift;
+        warn "$error";
+    }
+
+=head1 DESCRIPTION
+
+A I<scrolled search> is a search that allows you to keep pulling results
+until there are no more matching results, much like a cursor in an SQL
+database.
+
+Unlike paginating through results (with the C<from> parameter in
+L<search()|Search::Elasticsearch::Client::7_0::Direct/search()>),
+scrolled searches take a snapshot of the current state of the index. Even
+if you keep adding new documents to the index or updating existing documents,
+a scrolled search will only see the index as it was when the search began.
+
+This module is a helper utility that wraps the functionality of the
+L<search()|Search::Elasticsearch::Client::7_0::Direct/search()> and
+L<scroll()|Search::Elasticsearch::Client::7_0::Direct/scroll()> methods to make
+them easier to use.
+
+This class does L<Search::Elasticsearch::Client::7_0::Role::Scroll> and
+L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 USE CASES
+
+There are two primary use cases:
+
+=head2 Pulling enough results
+
+Perhaps you want to group your results by some field, and you don't know
+exactly how many results you will need in order to return 10 grouped
+results.  With a scrolled search you can keep pulling more results
+until you have enough.  For instance, you can search emails in a mailing
+list, and return results grouped by C<thread_id>:
+
+    use Promises qw(deferred);
+
+    sub find_email_threads {
+        my (%groups,@results,$scroll);
+
+        my $d = deferred;
+
+        $scroll = $es->scroll_helper(
+            index     => 'my_emails',
+            type      => 'email',
+            body      => { query => {... some query ... }},
+            on_result => sub {
+                my $doc = shift;
+                my $thread = $doc->{_source}{thread_id};
+                unless ($groups{$thread}) {
+                    $groups{$thread} = [];
+                    push @results, $groups{$thread};
+                }
+                push @{$groups{$thread}},$doc;
+
+                # stop collecting if we have 10 results
+                if (@results == 10) {
+                    $scroll->finish;
+                }
+            }
+        );
+
+        $scroll->start->then(
+            # resolve with results if completed successfully
+            sub { $d->resolve(@results) },
+
+            # reject with error if failed
+            sub { $d->reject(@_) }
+        );
+
+        return $d->promise;
+    }
+
+=head2 Extracting all documents
+
+Often you will want to extract all (or a subset of) documents in an index.
+If you want to change your type mappings, you will need to reindex all of your
+data. Or perhaps you want to move a subset of the data in one index into
+a new dedicated index. In these cases, you don't care about sort
+order, you just want to retrieve all documents which match a query, and do
+something with them. For instance, to retrieve all the docs for a particular
+C<client_id>:
+
+    $es->scroll_helper(
+        index       => 'my_index',
+        size        => 1000,
+        body        => {
+            query => {
+                match => {
+                    client_id => 123
+                }
+            },
+            sort => '_doc'
+        },
+        on_result => sub { do_something(@_) }
+    )->start;
+
+Very often the I<something> that you will want to do with these results
+involves bulk-indexing them into a new index. The easiest way to
+do this is to use the built-in L<Search::Elasticsearch::Client::7_0::Direct/reindex()>
+functionality provided by Elasticsearch.
+
+=head1 METHODS
+
+=head2 C<new()>
+
+    use Search::Elasticsearch::Async;
+
+    my $es = Search::Elasticsearch::Async->new(...);
+    my $scroll = $es->scroll_helper(
+        scroll             => '1m',            # optional
+
+        on_result          => sub {...}        # required
+      | on_results         => sub {...}        # required
+
+        on_start           => sub {...}        # optional
+        on_error           => sub {...}        # optional
+        %search_params,
+    );
+    $scroll->start;
+
+The L<Search::Elasticsearch::Client::7_0::Direct/scroll_helper()> method loads
+L<Search::Elasticsearch::Client::7_0::Async::Scroll> class and calls L</new()>,
+passing in any arguments.
+
+You can specify a C<scroll> duration (which defaults to C<"1m">).
+Any other parameters are passed directly to L<Search::Elasticsearch::Client::7_0::Direct/search()>.
+
+The C<scroll> duration tells Elasticearch how long it should keep the scroll
+alive.  B<Note>: this duration doesn't need to be long enough to process
+all results, just long enough to process a single B<batch> of results.
+The expiry gets renewed for another C<scroll> period every time new
+a new batch of results is retrieved from the cluster.
+
+By default, the C<scroll_id> is passed as the C<body> to the
+L<scroll|Search::Elasticsearch::Client::7_0::Direct/scroll()> request.
+
+The C<scroll> request uses C<GET> by default.  To use C<POST> instead,
+set L<send_get_body_as|Search::Elasticsearch::Transport/send_get_body_as> to
+C<POST>.
+
+=head3 Callbacks
+
+You must specify either an C<on_result> callback or an C<on_results> callback.
+
+=head4 C<on_result> and C<on_results>
+
+The C<on_result> callback is called once for every result that is received.
+
+    sub on_result {
+        my $doc = shift;
+        do_something($doc);
+    }
+
+Alternatively, you can specify an C<on_results> callback which is called
+once for every set of results returned by Elasticsearch:
+
+    sub on_results {
+        for my $doc (@_) {
+            do_something($doc)
+        }
+    }
+
+If either C<on_result> or C<on_results> returns a new L<Promise>, processing
+of further results will be paused until the promise has been rejected or
+resolved.
+
+=head4 C<on_start>
+
+The C<on_start> callback is called after the first request has completed,
+at which stage the properties like C<total()>, C<aggregations()>, etc
+will have been populated.
+
+=head4 C<on_error>
+
+The C<on_error> callback is called if any error occurs.  The default
+implementation warns about the error, and rethrows it.
+
+    sub on_error { warn "Scroll error: @_"; die @_ }
+
+If you wish to handle (and surpress) certain errors, then don't call C<die()>,
+eg:
+
+    sub on_error {
+        my $error = shift;
+        if ($error =~/SomeCatchableError/) {
+            # do something to handle error
+        }
+        else {
+            # rethrow error
+            die $error;
+        }
+    }
+
+=head2 C<start()>
+
+    $scroll->start
+           ->then( \&success, \&failure );
+
+The C<start()> method starts the scroll and returns a L<Promise> which
+will be resolved when the scroll completes (or L</finish()> is called),
+or rejected if any errors remain unhandled.
+
+=head2 C<finish()>
+
+    $scroll->finish;
+
+The C<finish()> method clears out the buffer, sets L</is_finished()> to C<true>
+and tries to clear the C<scroll_id> on Elasticsearch.  This API is only
+supported since v0.90.6, but the call to C<clear_scroll> is wrapped in an
+C<eval> so the C<finish()> method can be safely called with any version
+of Elasticsearch.
+
+When the C<$scroll> instance goes out of scope, L</finish()> is called
+automatically if required.
+
+=head2 C<is_finished()>
+
+    $bool = $scroll->is_finished;
+
+A flag which returns C<true> if all results have been processed or
+L</finish()> has been called.
+
+=head1 INFO ACCESSORS
+
+The information from the original search is returned via the accessors
+below.  These values can be accessed in the C<on_start> callback:
+
+=head2 C<total>
+
+The total number of documents that matched your query.
+
+=head2 C<max_score>
+
+The maximum score of any documents in your query.
+
+=head2 C<aggregations>
+
+Any aggregations that were specified, or C<undef>
+
+=head2 C<facets>
+
+Any facets that were specified, or C<undef>
+
+=head2 C<suggest>
+
+Any suggestions that were specified, or C<undef>
+
+=head2 C<took>
+
+How long the original search took, in milliseconds
+
+=head2 C<took_total>
+
+How long the original search plus all subsequent batches took, in milliseconds.
+This value can only be checked once the scroll has completed.
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Client::7_0::Direct/search()>
+
+=item * L<Search::Elasticsearch::Client::7_0::Direct/scroll()>
+
+=back
diff --git a/lib/Search/Elasticsearch/Client/2_0/Bulk.pm b/lib/Search/Elasticsearch/Client/7_0/Bulk.pm
similarity index 59%
rename from lib/Search/Elasticsearch/Client/2_0/Bulk.pm
rename to lib/Search/Elasticsearch/Client/7_0/Bulk.pm
index c1b409d..134f973 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Bulk.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Bulk.pm
@@ -1,7 +1,24 @@
-package Search::Elasticsearch::Client::2_0::Bulk;
-$Search::Elasticsearch::Client::2_0::Bulk::VERSION = '6.81';
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Bulk;
+
 use Moo;
-with 'Search::Elasticsearch::Client::2_0::Role::Bulk',
+with 'Search::Elasticsearch::Client::7_0::Role::Bulk',
     'Search::Elasticsearch::Role::Is_Sync';
 use Search::Elasticsearch::Util qw(parse_params throw);
 use Try::Tiny;
@@ -65,53 +82,11 @@ sub flush {
     return defined wantarray ? $results : undef;
 }
 
-#===================================
-sub reindex {
-#===================================
-    my ( $self, $params ) = parse_params(@_);
-    my $src = $params->{source}
-        or throw( 'Param', "Missing required param <source>" );
-
-    my $transform = $self->_doc_transformer($params);
-
-    if ( ref $src eq 'HASH' ) {
-        $src = {%$src};
-        my $es = delete $src->{es} || $self->es;
-        my $scroll = $es->scroll_helper(
-            search_type => 'scan',
-            size        => 500,
-            %$src
-        );
-
-        $src = sub {
-            $scroll->refill_buffer;
-            $scroll->drain_buffer;
-        };
-
-        print "Reindexing " . $scroll->total . " docs\n"
-            if $self->verbose;
-    }
-
-    while ( my @docs = grep {defined} $src->() ) {
-        $self->index( grep {$_} map { $transform->($_) } @docs );
-    }
-    $self->flush;
-    return 1;
-}
-
 1;
 
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Bulk - A helper module for the Bulk API and for reindexing
-
-=head1 VERSION
+__END__
 
-version 6.81
+# ABSTRACT: A helper module for the Bulk API
 
 =head1 SYNOPSIS
 
@@ -144,29 +119,20 @@ version 6.81
     # Manual flush
     $bulk->flush;
 
-    # Reindex docs:
-    my $bulk = $es->bulk_helper(
-        index   => 'new_index',
-        verbose => 1
-    );
-
-    $bulk->reindex( source => { index => 'old_index' });
-
 =head1 DESCRIPTION
 
-This module provides a wrapper for the L<Search::Elasticsearch::Client::2_0::Direct/bulk()>
+This module provides a wrapper for the L<Search::Elasticsearch::Client::7_0::Direct/bulk()>
 method which makes it easier to run multiple create, index, update or delete
-actions in a single request. It also provides a simple interface
-for L<reindexing documents|/REINDEXING DOCUMENTS>.
+actions in a single request.
 
-The L<Search::Elasticsearch::Client::2_0::Bulk> module acts as a queue, buffering up actions
+The L<Search::Elasticsearch::Client::7_0::Bulk> module acts as a queue, buffering up actions
 until it reaches a maximum count of actions, or a maximum size of JSON request
 body, at which point it issues a C<bulk()> request.
 
 Once you have finished adding actions, call L</flush()> to force the final
 C<bulk()> request on the items left in the queue.
 
-This class does L<Search::Elasticsearch::Client::2_0::Role::Bulk> and
+This class does L<Search::Elasticsearch::Client::7_0::Role::Bulk> and
 L<Search::Elasticsearch::Role::Is_Sync>.
 
 =head1 CREATING A NEW INSTANCE
@@ -181,7 +147,7 @@ L<Search::Elasticsearch::Role::Is_Sync>.
 
         max_count   => 1_000,               # optional
         max_size    => 1_000_000,           # optional
-        max_time    => 5,                   # optional
+        max_time    => 6,                   # optional
 
         verbose     => 0 | 1,               # optional
 
@@ -198,7 +164,7 @@ Search::Elasticsearch client as the C<es> argument.
 The C<index> and C<type> parameters provide default values for
 C<index> and C<type>, which can be overridden in each action.
 You can also pass any other values which are accepted
-by the L<bulk()|Search::Elasticsearch::Client::2_0::Direct/bulk()> method.
+by the L<bulk()|Search::Elasticsearch::Client::7_0::Direct/bulk()> method.
 
 See L</flush()> for more information about the other parameters.
 
@@ -209,7 +175,7 @@ See L</flush()> for more information about the other parameters.
     $result = $bulk->flush;
 
 The C<flush()> method sends all buffered actions to Elasticsearch using
-a L<bulk()|Search::Elasticsearch::Client::2_0::Direct/bulk()> request.
+a L<bulk()|Search::Elasticsearch::Client::7_0::Direct/bulk()> request.
 
 =head2 Auto-flushing
 
@@ -356,8 +322,8 @@ See the individual helper methods below for details.
 B<Note:> Parameters like C<index> or C<type> can be specified as C<index> or as
 C<_index>, so the following two lines are equivalent:
 
-    index => { index  => 'index', type  => 'type', id  => 1, source  => {...}},
-    index => { _index => 'index', _type => 'type', _id => 1, _source => {...}},
+    index => { index  => 'index', type  => 'type', id  => 1, source => {...}},
+    index => { _index => 'index', _type => 'type', _id => 1, source => {...}},
 
 B<Note:> The C<index> and C<type> parameters can be specified in the
 params for any action, but if not specified, will default to the C<index>
@@ -373,7 +339,7 @@ they must be specified either in L</new()> or in every action.
     );
 
 The C<create()> helper method allows you to add multiple C<create> actions.
-It accepts the same parameters as L<Search::Elasticsearch::Client::2_0::Direct/create()>
+It accepts the same parameters as L<Search::Elasticsearch::Client::7_0::Direct/create()>
 except that the document body should be passed as the C<source> or C<_source>
 parameter, instead of as C<body>.
 
@@ -399,7 +365,7 @@ you can just pass the individual document bodies.
     );
 
 The C<index()> helper method allows you to add multiple C<index> actions.
-It accepts the same parameters as L<Search::Elasticsearch::Client::2_0::Direct/index()>
+It accepts the same parameters as L<Search::Elasticsearch::Client::7_0::Direct/index()>
 except that the document body should be passed as the C<source> or C<_source>
 parameter, instead of as C<body>.
 
@@ -412,7 +378,7 @@ parameter, instead of as C<body>.
     );
 
 The C<delete()> helper method allows you to add multiple C<delete> actions.
-It accepts the same parameters as L<Search::Elasticsearch::Client::2_0::Direct/delete()>.
+It accepts the same parameters as L<Search::Elasticsearch::Client::7_0::Direct/delete()>.
 
 =head2 C<delete_ids()>
 
@@ -430,185 +396,17 @@ In this case, all you have to do is to pass in a list of IDs.
           doc_as_upsert => 1
         },
         { id            => 2,
-          lang          => 'mvel',
           script        => { script }
           upsert        => { upsert doc }
         },
         ...
     );
 
+
 The C<update()> helper method allows you to add multiple C<update> actions.
-It accepts the same parameters as L<Search::Elasticsearch::Client::2_0::Direct/update()>.
+It accepts the same parameters as L<Search::Elasticsearch::Client::7_0::Direct/update()>.
 An update can either use a I<partial doc> which gets merged with an existing
 doc (example 1 above), or can use a C<script> to update an existing doc
 (example 2 above). More information on C<script> can be found here:
-L<Search::Elasticsearch::Client::2_0::Direct/update()>.
-
-=head1 REINDEXING DOCUMENTS
-
-A common use case for bulk indexing is to reindex a whole index when
-changing the type mappings or analysis chain. This typically
-combines bulk indexing with L<scrolled searches|Search::Elasticsearch::Client::2_0::Scroll>:
-the scrolled search pulls all of the data from the source index, and
-the bulk indexer indexes the data into the new index.
-
-=head2 C<reindex()>
-
-    $bulk->reindex(
-        source       => $source,                # required
-        transform    => \&transform,            # optional
-        version_type => 'external|internal',    # optional
-    );
-
-The C<reindex()> method requires a C<$source> parameter, which provides
-the source for the documents which are to be reindexed.
-
-=head2 Reindexing from another index
-
-If the C<source> argument is a HASH ref, then the hash is passed to
-L<Search::Elasticsearch::Client::2_0::Scroll/new()> to create a new scrolled search.
-
-    my $bulk = $es->bulk_helper(
-        index   => 'new_index',
-        verbose => 1
-    );
-
-    $bulk->reindex(
-        source  => {
-            index       => 'old_index',
-            size        => 500,         # default
-            search_type => 'scan'       # default
-        }
-    );
-
-If a default C<index> or C<type> has been specified in the call to
-L</new()>, then it will replace the C<index> and C<type> values for
-the docs returned from the scrolled search. In the example above,
-all docs will be retrieved from C<"old_index"> and will be bulk indexed
-into C<"new_index">.
-
-=head2 Reindexing from a generic source
-
-The C<source> parameter also accepts a coderef or an anonymous sub,
-which should return one or more new documents every time it is executed.
-This allows you to pass any iterator, wrapped in an anonymous sub:
-
-    my $iter = get_iterator_from_somewhere();
-
-    $bulk->reindex(
-        source => sub { $iter->next }
-    );
-
-=head2 Transforming docs on the fly
-
-The C<transform> parameter allows you to change documents on the fly,
-using a callback.  The callback receives the document as the only argument,
-and should return the updated document, or C<undef> if the document should
-not be indexed:
-
-    $bulk->reindex(
-        source      => { index => 'old_index' },
-        transform   => sub {
-            my $doc = shift;
-
-            # don't index doc marked as valid:false
-            return undef unless $doc->{_source}{valid};
-
-            # convert $tag to @tags
-            $doc->{_source}{tags} = [ delete $doc->{_source}{tag}];
-            return $doc
-        }
-    );
-
-=head2 Reindexing from another cluster
-
-By default, L</reindex()> expects the source and destination indices
-to be in the same cluster. To pull data from one cluster and index it into
-another, you can use two separate C<$es> objects:
-
-    $es_target  = Search::Elasticsearch->new( nodes => 'localhost:9200' );
-    $es_source  = Search::Elasticsearch->new( nodes => 'search1:9200' );
-
-    my $bulk = $es_targert->bulk_helper(
-        verbose => 1
-    )
-    -> reindex(
-          source => {
-              es    => $es_source,
-              index => 'my_index'
-          }
-       );
-
-=head2 Parents and routing
-
-If you are using parent-child relationships or custom C<routing> values,
-and you want to preserve these when you reindex your documents, then
-you will need to request these values specifically, as follows:
-
-    $bulk->reindex(
-        source => {
-            index   => 'old_index',
-            fields  => ['_source','_parent','_routing']
-        }
-    );
-
-=head2 Working with version numbers
-
-Every document in Elasticsearch has a current C<version> number, which
-is used for L<optimistic concurrency control|http://en.wikipedia.org/wiki/Optimistic_concurrency_control>,
-that is, to ensure that you don't overwrite changes that have been made
-by another process.
-
-All CRUD operations accept a C<version> parameter and a C<version_type>
-parameter which tells Elasticsearch that the change should only be made
-if the current document corresponds to these parameters. The
-C<version_type> parameter can have the following values:
-
-=over
-
-=item * C<internal>
-
-Use Elasticsearch version numbers.  Documents are only changed if the
-document in Elasticsearch has the B<same> C<version> number that is
-specified in the CRUD operation. After the change, the new
-version number is C<version+1>.
-
-=item * C<external>
-
-Use an external versioning system, such as timestamps or version numbers
-from an external database.  Documents are only changed if the document
-in Elasticsearch has a B<lower> C<version> number than the one
-specified in the CRUD operation. After the change, the new version
-number is C<version>.
-
-=back
-
-If you would like to reindex documents from one index to another, preserving
-the C<version> numbers from the original index, then you need the following:
-
-    $bulk->reindex(
-        source => {
-            index   => 'old_index',
-            version => 1,               # retrieve version numbers in search
-        },
-        version_type => 'external'      # use these "external" version numbers
-    );
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
-
-__END__
-
-# ABSTRACT: A helper module for the Bulk API and for reindexing
+L<Search::Elasticsearch::Client::7_0::Direct/update()>.
 
diff --git a/lib/Search/Elasticsearch/Client/2_0/Direct.pm b/lib/Search/Elasticsearch/Client/7_0/Direct.pm
similarity index 66%
rename from lib/Search/Elasticsearch/Client/2_0/Direct.pm
rename to lib/Search/Elasticsearch/Client/7_0/Direct.pm
index 7ca3ffd..4a1ea5e 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Direct.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct.pm
@@ -1,36 +1,51 @@
-package Search::Elasticsearch::Client::2_0::Direct;
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Client::7_0::Direct;
 
 use Moo;
-with 'Search::Elasticsearch::Client::2_0::Role::API';
+with 'Search::Elasticsearch::Client::7_0::Role::API';
 with 'Search::Elasticsearch::Role::Client::Direct';
 
-our $VERSION='6.81';
-use Search::Elasticsearch 6.00 ();
-
 use Search::Elasticsearch::Util qw(parse_params is_compat);
 use namespace::clean;
 
 sub _namespace {__PACKAGE__}
 
-has 'cluster'             => ( is => 'lazy', init_arg => undef );
-has 'nodes'               => ( is => 'lazy', init_arg => undef );
-has 'indices'             => ( is => 'lazy', init_arg => undef );
-has 'snapshot'            => ( is => 'lazy', init_arg => undef );
-has 'cat'                 => ( is => 'lazy', init_arg => undef );
-has 'tasks'               => ( is => 'lazy', init_arg => undef );
-has 'bulk_helper_class'   => ( is => 'rw' );
-has 'scroll_helper_class' => ( is => 'rw' );
-has '_bulk_class'         => ( is => 'lazy' );
-has '_scroll_class'       => ( is => 'lazy' );
-
-#===================================
-sub create {
-#===================================
-    my ( $self, $params ) = parse_params(@_);
-    my $defn = $self->api->{index};
-    $params->{op_type} = 'create';
-    $self->perform_request( { %$defn, name => 'create' }, $params );
-}
+has 'async_search'         => ( is => 'lazy', init_arg => undef );
+has 'autoscaling'          => ( is => 'lazy', init_arg => undef );
+has 'cat'                  => ( is => 'lazy', init_arg => undef );
+has 'ccr'                  => ( is => 'lazy', init_arg => undef );
+has 'cluster'              => ( is => 'lazy', init_arg => undef );
+has 'dangling_indices'     => ( is => 'lazy', init_arg => undef );
+has 'data_frame_transform_deprecated' => ( is => 'lazy', init_arg => undef );
+has 'enrich'               => ( is => 'lazy', init_arg => undef );
+has 'eql'                  => ( is => 'lazy', init_arg => undef );
+has 'graph'                => ( is => 'lazy', init_arg => undef );
+has 'ilm'                  => ( is => 'lazy', init_arg => undef );
+has 'indices'              => ( is => 'lazy', init_arg => undef );
+has 'ingest'               => ( is => 'lazy', init_arg => undef );
+has 'license'              => ( is => 'lazy', init_arg => undef );
+has 'migration'            => ( is => 'lazy', init_arg => undef );
+has 'ml'                   => ( is => 'lazy', init_arg => undef );
+has 'monitoring'           => ( is => 'lazy', init_arg => undef );
+has 'nodes'                => ( is => 'lazy', init_arg => undef );
+has 'rollup'               => ( is => 'lazy', init_arg => undef );
+has 'searchable_snapshots' => ( is => 'lazy', init_arg => undef );
+has 'security'             => ( is => 'lazy', init_arg => undef );
+has 'snapshot'             => ( is => 'lazy', init_arg => undef );
+has 'slm'                  => ( is => 'lazy', init_arg => undef );
+has 'sql'                  => ( is => 'lazy', init_arg => undef );
+has 'ssl'                  => ( is => 'lazy', init_arg => undef );
+has 'tasks'                => ( is => 'lazy', init_arg => undef );
+has 'transform'            => ( is => 'lazy', init_arg => undef );
+has 'watcher'              => ( is => 'lazy', init_arg => undef );
+has 'xpack'                => ( is => 'lazy', init_arg => undef );
+has 'bulk_helper_class'    => ( is => 'rw' );
+has 'scroll_helper_class'  => ( is => 'rw' );
+has '_bulk_class'          => ( is => 'lazy' );
+has '_scroll_class'        => ( is => 'lazy' );
 
 #===================================
 sub _build__bulk_class {
@@ -67,29 +82,44 @@ sub scroll_helper {
 }
 
 #===================================
-sub _build_cluster  { shift->_build_namespace('Cluster') }
-sub _build_nodes    { shift->_build_namespace('Nodes') }
-sub _build_indices  { shift->_build_namespace('Indices') }
-sub _build_snapshot { shift->_build_namespace('Snapshot') }
-sub _build_cat      { shift->_build_namespace('Cat') }
-sub _build_tasks    { shift->_build_namespace('Tasks') }
+sub _build_autoscaling          { shift->_build_namespace('Autoscaling') }
+sub _build_async_search         { shift->_build_namespace('AsyncSearch') }
+sub _build_cat                  { shift->_build_namespace('Cat') }
+sub _build_ccr                  { shift->_build_namespace('CCR') }
+sub _build_cluster              { shift->_build_namespace('Cluster') }
+sub _build_dangling_indices     { shift->_build_namespace('DanglingIndices') }
+sub _build_data_frame_transform_deprecated { shift->_build_namespace('DataFrameTransformDeprecated') }
+sub _build_enrich               { shift->_build_namespace('Enrich') }
+sub _build_eql                  { shift->_build_namespace('Eql') }
+sub _build_graph                { shift->_build_namespace('Graph') }
+sub _build_ilm                  { shift->_build_namespace('ILM') }
+sub _build_indices              { shift->_build_namespace('Indices') }
+sub _build_ingest               { shift->_build_namespace('Ingest') }
+sub _build_license              { shift->_build_namespace('License') }
+sub _build_migration            { shift->_build_namespace('Migration') }
+sub _build_ml                   { shift->_build_namespace('ML') }
+sub _build_monitoring           { shift->_build_namespace('Monitoring') }
+sub _build_nodes                { shift->_build_namespace('Nodes') }
+sub _build_rollup               { shift->_build_namespace('Rollup') }
+sub _build_searchable_snapshots { shift->_build_namespace('SearchableSnapshots') }
+sub _build_security             { shift->_build_namespace('Security') }
+sub _build_snapshot             { shift->_build_namespace('Snapshot') }
+sub _build_slm                  { shift->_build_namespace('Slm') }
+sub _build_sql                  { shift->_build_namespace('SQL') }
+sub _build_ssl                  { shift->_build_namespace('SSL') }
+sub _build_tasks                { shift->_build_namespace('Tasks') }
+sub _build_transform            { shift->_build_namespace('Transform') }
+sub _build_watcher              { shift->_build_namespace('Watcher') }
+sub _build_xpack                { shift->_build_namespace('XPack') }
 #===================================
 
 __PACKAGE__->_install_api('');
 
 1;
 
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Direct - Thin client with full support for Elasticsearch 2.x APIs
-
-=head1 VERSION
+__END__
 
-version 6.81
+# ABSTRACT: Thin client with full support for Elasticsearch 7.x APIs
 
 =head1 SYNOPSIS
 
@@ -97,7 +127,7 @@ Create a client:
 
     use Search::Elasticsearch;
     my $e = Search::Elasticsearch->new(
-        client => '2_0::Direct'
+        client => '7_0::Direct'
     );
 
 Index a doc:
@@ -139,6 +169,10 @@ Index-level requests:
     $e->indices->create( index => 'my_index' );
     $e->indices->delete( index => 'my_index' )
 
+Ingest pipeline requests:
+
+    $e->ingest->get_pipeline( id => 'apache-logs' );
+
 Cluster-level requests:
 
     $health = $e->cluster->health;
@@ -172,13 +206,21 @@ Task management:
     say $e->cat->allocation;
     say $e->cat->health;
 
+Cross-cluster replication requests:
+
+    say $e->ccr->follow;
+
+Index lifecycle management requests:
+
+    say $e->ilm->put_lifecycle;
+
 =head1 DESCRIPTION
 
-The L<Search::Elasticsearch::Client::2_0::Direct> class provides the
-Elasticsearch 2.x compatible client returned by:
+The L<Search::Elasticsearch::Client::7_0::Direct> class provides the
+Elasticsearch 7.x compatible client returned by:
 
     $e = Search::Elasticsearch->new(
-        client => "2_0::Direct"
+        client => "7_0::Direct"  # default
     );
 
 It is intended to be as close as possible to the native REST API that
@@ -191,6 +233,38 @@ L<bulk document CRUD|/BULK DOCUMENT CRUD METHODS> and L<search|/SEARCH METHODS>.
 It also provides access to clients for managing L<indices|/indices()>
 and the L<cluster|/cluster()>.
 
+=head1 PREVIOUS VERSIONS OF ELASTICSEARCH
+
+This version of the client supports the Elasticsearch 7.0 branch,
+which is not backwards compatible with earlier branches.
+
+If you need to talk to a version of Elasticsearch before 7.0.0, please
+install one of the following modules:
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Client::6_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::5_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::2_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::1_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::0_90>
+
+=back
+
 =head1 CONVENTIONS
 
 =head2 Parameter passing
@@ -222,11 +296,11 @@ as top level parameters:
 If you pass in a C<\%params> hash, then it will be included in the
 query string parameters without any error checking. The following:
 
-    $e->search( size => 10, params => { from => 5, size => 5 })
+    $e->search( size => 10, params => { from => 6, size => 6 })
 
 would result in this query string:
 
-    ?from=5&size=10
+    ?from=6&size=10
 
 =head2 Body parameter
 
@@ -243,6 +317,21 @@ UTF-8 bytes and passed as is:
 
     $e->indices->analyze( body => "The quick brown fox");
 
+=head2 Boolean parameters
+
+Elasticsearch 7.0.0 and above no longer accepts truthy and falsey values for booleans.  Instead,
+it will accept only a JSON C<true> or C<false>, or the string equivalents C<"true"> or C<"false">.
+
+In the Perl client, you can use the following values:
+
+=over
+
+=item * True: C<true>, C<\1>, or a L<JSON::PP::Boolean> object.
+
+=item * False: C<false>, C<\0>, or a L<JSON::PP::Boolean> object.
+
+=back
+
 =head2 Filter path parameter
 
 Any API which returns a JSON body accepts a C<filter_path> parameter
@@ -253,7 +342,7 @@ you can do:
 
     $e->search(
         query => {...},
-        filter_path => [ 'hits.total', 'hits.hits._source' ]
+        filter_paths => [ 'hits.total', 'hits.hits._source' ]
     );
 
 =head2 Ignore parameter
@@ -284,12 +373,12 @@ Multiple error codes can be specified with an array:
 =head2 C<bulk_helper_class>
 
 The class to use for the L</bulk_helper()> method. Defaults to
-L<Search::Elasticsearch::Client::2_0::Bulk>.
+L<Search::Elasticsearch::Client::7_0::Bulk>.
 
 =head2 C<scroll_helper_class>
 
 The class to use for the L</scroll_helper()> method. Defaults to
-L<Search::Elasticsearch::Client::2_0::Scroll>.
+L<Search::Elasticsearch::Client::7_0::Scroll>.
 
 =head1 GENERAL METHODS
 
@@ -311,29 +400,36 @@ response, otherwise it throws an error.
 
     $indices_client = $e->indices;
 
-Returns an L<Search::Elasticsearch::Client::2_0::Direct::Indices> object which can be used
+Returns a L<Search::Elasticsearch::Client::7_0::Direct::Indices> object which can be used
 for managing indices, eg creating, deleting indices, managing mapping,
 index settings etc.
 
+=head2 C<ingest()>
+
+    $ingest_client = $e->ingest;
+
+Returns a L<Search::Elasticsearch::Client::7_0::Direct::Ingest> object which can be used
+for managing ingest pipelines.
+
 =head2 C<cluster()>
 
     $cluster_client = $e->cluster;
 
-Returns an L<Search::Elasticsearch::Client::2_0::Direct::Cluster> object which can be used
+Returns a L<Search::Elasticsearch::Client::7_0::Direct::Cluster> object which can be used
 for managing the cluster, eg cluster-wide settings and cluster health.
 
 =head2 C<nodes()>
 
     $node_client = $e->nodes;
 
-Returns an L<Search::Elasticsearch::Client::2_0::Direct::Nodes> object which can be used
+Returns a L<Search::Elasticsearch::Client::7_0::Direct::Nodes> object which can be used
 to retrieve node info and stats.
 
 =head2 C<snapshot()>
 
     $snapshot_client = $e->snapshot;
 
-Returns an L<Search::Elasticsearch::Client::2_0::Direct::Snapshot> object which
+Returns a L<Search::Elasticsearch::Client::7_0::Direct::Snapshot> object which
 is used for managing backup repositories and creating and restoring
 snapshots.
 
@@ -341,17 +437,32 @@ snapshots.
 
     $tasks_client = $e->tasks;
 
-Returns an L<Search::Elasticsearch::Client::2_0::Direct::Tasks> object which
+Returns a L<Search::Elasticsearch::Client::7_0::Direct::Tasks> object which
 is used for accessing the task management API.
 
 =head2 C<cat()>
 
     $cat_client = $e->cat;
 
-Returns an L<Search::Elasticsearch::Client::2_0::Direct::Cat> object which can be used
+Returns a L<Search::Elasticsearch::Client::7_0::Direct::Cat> object which can be used
 to retrieve simple to read text info for debugging and monitoring an
 Elasticsearch cluster.
 
+=head2 C<ccr()>
+
+    $ccr_client = $e->ccr;
+
+Returns a L<Search::Elasticsearch::Client::7_0::Direct::CCR> object which can be used
+to handle cross-cluster replication requests.
+
+
+=head2 C<ilm()>
+
+    $ilm_client = $e->ilm;
+
+Returns a L<Search::Elasticsearch::Client::7_0::Direct::ILM> object which can be used
+to handle index lifecycle management requests.
+
 =head1 DOCUMENT CRUD METHODS
 
 These methods allow you to perform create, index, update and delete requests
@@ -371,16 +482,19 @@ The C<index()> method is used to index a new document or to reindex
 an existing document.
 
 Query string parameters:
-    C<consistency>,
+    C<error_trace>,
+    C<human>,
+    C<if_primary_term>,
+    C<if_seq_no>,
     C<op_type>,
     C<parent>,
+    C<pipeline>,
     C<refresh>,
     C<routing>,
     C<timeout>,
-    C<timestamp>,
-    C<ttl>,
     C<version>,
-    C<version_type>
+    C<version_type>,
+    C<wait_for_active_shards>
 
 See the L<index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html>
 for more information.
@@ -390,7 +504,7 @@ for more information.
     $response = $e->create(
         index   => 'index_name',        # required
         type    => 'type_name',         # required
-        id      => 'doc_id',            # optional, otherwise auto-generated
+        id      => 'doc_id',            # required
 
         body    => { document }         # required
     );
@@ -401,13 +515,13 @@ C<index>, C<type> and C<id> already exists.
 
 Query string parameters:
     C<consistency>,
+    C<error_trace>,
+    C<human>,
     C<op_type>,
     C<parent>,
     C<refresh>,
     C<routing>,
     C<timeout>,
-    C<timestamp>,
-    C<ttl>,
     C<version>,
     C<version_type>
 
@@ -427,14 +541,16 @@ C<index>, C<type> and C<id>, or will throw a C<Missing> error.
 
 Query string parameters:
     C<_source>,
-    C<_source_exclude>,
-    C<_source_include>,
-    C<fields>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<human>,
     C<parent>,
     C<preference>,
     C<realtime>,
     C<refresh>,
     C<routing>,
+    C<stored_fields>,
     C<version>,
     C<version_type>
 
@@ -456,8 +572,10 @@ plus the document metadata, ie the C<_index>, C<_type> etc.
 
 Query string parameters:
     C<_source>,
-    C<_source_exclude>,
-    C<_source_include>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<human>,
     C<parent>,
     C<preference>,
     C<realtime>,
@@ -481,11 +599,18 @@ The C<exists()> method returns C<1> if a document with the specified
 C<index>, C<type> and C<id> exists, or an empty string if it doesn't.
 
 Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<human>,
     C<parent>,
     C<preference>,
     C<realtime>,
     C<refresh>,
-    C<routing>
+    C<routing>,
+    C<version>,
+    C<version_type>
 
 See the L<exists docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html>
 for more information.
@@ -502,13 +627,17 @@ The C<delete()> method will delete the document with the specified
 C<index>, C<type> and C<id>, or will throw a C<Missing> error.
 
 Query string parameters:
-    C<consistency>,
+    C<error_trace>,
+    C<human>,
+    C<if_primary_term>,
+    C<if_seq_no>,
     C<parent>,
     C<refresh>,
     C<routing>,
     C<timeout>,
     C<version>,
-    C<version_type>
+    C<version_type>,
+    C<wait_for_active_shards>
 
 See the L<delete docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html>
 for more information.
@@ -543,16 +672,12 @@ C<index>, C<type> and C<id> if it exists. Updates can be performed either by:
         ...,
         body => {
             script => {
-                inline => "ctx._source.counter += incr",
-                params => { incr => 5 }
+                source => "ctx._source.counter += incr",
+                params => { incr => 6 }
             }
         }
     );
 
-Make sure you enable
-L<dynamic scripting|https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#enable-dynamic-scripting>
-and know its implications.
-
 =item * with an indexed script:
 
     $response = $e->update(
@@ -560,7 +685,8 @@ and know its implications.
         body => {
             script => {
                 id     => $id,
-                params => { incr => 5 }
+                lang   => 'painless',
+                params => { incr => 6 }
             }
         }
     );
@@ -575,8 +701,8 @@ for more information.
         body => {
             script => {
                 file   => 'counter',
-                lang   => 'groovy',
-                params => { incr => 5 }
+                lang   => 'painless',
+                params => { incr => 6 }
             }
         }
     );
@@ -587,22 +713,23 @@ for more information.
 =back
 
 Query string parameters:
-    C<consistency>,
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
     C<fields>,
+    C<human>,
+    C<if_primary_term>,
+    C<if_seq_no>,
     C<lang>,
     C<parent>,
-    C<realtime>,
     C<refresh>,
     C<retry_on_conflict>,
     C<routing>,
-    C<script>,
-    C<script_id>,
-    C<scripted_upsert>,
     C<timeout>,
-    C<timestamp>,
-    C<ttl>,
     C<version>,
-    C<version_type>
+    C<version_type>,
+    C<wait_for_active_shards>
 
 See the L<update docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html>
 for more information.
@@ -622,9 +749,10 @@ offsets and payloads for the specified document, assuming that termvectors
 have been enabled.
 
 Query string parameters:
-    C<dfs>,
+    C<error_trace>,
     C<field_statistics>,
     C<fields>,
+    C<human>,
     C<offsets>,
     C<parent>,
     C<payloads>,
@@ -654,7 +782,7 @@ that need to be made, bulk requests greatly improve performance.
         body    => [ actions ]          # required
     );
 
-See L<Search::Elasticsearch::Client::2_0::Bulk> and L</bulk_helper()> for a helper module that makes
+See L<Search::Elasticsearch::Client::7_0::Bulk> and L</bulk_helper()> for a helper module that makes
 bulk indexing simpler to use.
 
 The C<bulk()> method can perform multiple L</index()>, L</create()>,
@@ -678,6 +806,7 @@ eg:
     { update => { _index => 'index', _type => 'type', _id => 123 }},
     { script => "ctx._source.counter+=1" }
 
+
 Each action can include the same parameters that you would pass to
 the equivalent L</index()>, L</create()>, L</delete()> or L</update()>
 request, except that C<_index>, C<_type> and C<_id> must be specified with
@@ -704,7 +833,7 @@ For instance:
             { title => 'Foo' },
 
             # delete action
-            { delete => { _id => 125 }},
+            { delete => { _id => 126 }},
 
             # update action
             { update => { _id => 126 }},
@@ -716,11 +845,17 @@ Each action is performed separately. One failed action will not
 cause the others to fail as well.
 
 Query string parameters:
-    C<consistency>,
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
     C<fields>,
+    C<human>,
+    C<pipeline>,
     C<refresh>,
     C<routing>,
-    C<timeout>
+    C<timeout>,
+    C<wait_for_active_shards>
 
 See the L<bulk docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html>
 for more information.
@@ -730,7 +865,7 @@ for more information.
     $bulk_helper = $e->bulk_helper( @args );
 
 Returns a new instance of the class specified in the L</bulk_helper_class>,
-which defaults to L<Search::Elasticsearch::Client::2_0::Bulk>.
+which defaults to L<Search::Elasticsearch::Client::7_0::Bulk>.
 
 =head2 C<mget()>
 
@@ -771,12 +906,15 @@ C<ids> of the documents to retrieve:
 
 Query string parameters:
     C<_source>,
-    C<_source_exclude>,
-    C<_source_include>,
-    C<fields>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<human>,
     C<preference>,
     C<realtime>,
-    C<refresh>
+    C<refresh>,
+    C<routing>,
+    C<stored_fields>
 
 See the L<mget docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-get.html>
 for more information.
@@ -803,8 +941,10 @@ Runs multiple L</termvector()> requests in a single request, eg:
     );
 
 Query string parameters:
+    C<error_trace>,
     C<field_statistics>,
     C<fields>,
+    C<human>,
     C<ids>,
     C<offsets>,
     C<parent>,
@@ -864,30 +1004,38 @@ L<request body|http://www.elastic.co/guide/en/elasticsearch/reference/current/se
 
 Query string parameters:
     C<_source>,
-    C<_source_exclude>,
-    C<_source_include>,
+    C<_source_excludes>,
+    C<_source_includes>,
     C<allow_no_indices>,
+    C<allow_partial_search_results>,
     C<analyze_wildcard>,
     C<analyzer>,
+    C<batched_reduce_size>,
     C<default_operator>,
     C<df>,
+    C<docvalue_fields>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<explain>,
-    C<fielddata_fields>,
-    C<fields>,
     C<from>,
+    C<human>,
+    C<ignore_throttled>,
     C<ignore_unavailable>,
     C<lenient>,
-    C<lowercase_expanded_terms>,
+    C<max_concurrent_shard_requests>,
+    C<pre_filter_shard_size>,
     C<preference>,
     C<q>,
     C<request_cache>,
+    C<rest_total_hits_as_int>,
     C<routing>,
     C<scroll>,
     C<search_type>,
+    C<seq_no_primary_term>,
     C<size>,
     C<sort>,
     C<stats>,
+    C<stored_fields>,
     C<suggest_field>,
     C<suggest_mode>,
     C<suggest_size>,
@@ -895,6 +1043,8 @@ Query string parameters:
     C<terminate_after>,
     C<timeout>,
     C<track_scores>,
+    C<track_total_hits>,
+    C<typed_keys>,
     C<version>
 
 See the L<search reference|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html>
@@ -902,37 +1052,6 @@ for more information.
 
 Also see L<Search::Elasticsearch::Transport/send_get_body_as>.
 
-=head2 C<search_exists()>
-
-The C<search_exists()> method is a quick version of search which can be
-used to find out whether there are matching search results or not.
-It doesn't return any results itself.
-
-    $results = $e->search_exists(
-        index   => 'index' | \@indices,     # optional
-        type    => 'type'  | \@types,       # optional
-
-        body    => { search params }        # optional
-    );
-
-Query string parameters:
-    C<allow_no_indices>,
-    C<analyze_wildcard>,
-    C<analyzer>,
-    C<default_operator>,
-    C<df>,
-    C<expand_wildcards>,
-    C<ignore_unavailable>,
-    C<lenient>,
-    C<lowercase_expanded_terms>
-    C<min_score>,
-    C<preference>,
-    C<q>,
-    C<routing>
-
-See the L<search exists reference|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-exists.html>
-for more information.
-
 =head2 C<count()>
 
     $results = $e->count(
@@ -959,14 +1078,18 @@ Query string parameters:
     C<analyzer>,
     C<default_operator>,
     C<df>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
+    C<ignore_throttled>,
     C<ignore_unavailable>,
     C<lenient>,
     C<lowercase_expanded_terms>
     C<min_score>,
     C<preference>,
     C<q>,
-    C<routing>
+    C<routing>,
+    C<terminate_after>
 
 See the L<count docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html>
 for more information.
@@ -977,7 +1100,7 @@ for more information.
         index   => 'index' | \@indices,     # optional
         type    => 'type'  | \@types,       # optional
 
-        body    => { search params }        # optional
+        body    => { search params }        # required
     );
 
 Perform a search by specifying a template (either predefined or defined
@@ -985,7 +1108,7 @@ within the C<body>) and parameters to use with the template, eg:
 
     $results = $e->search_template(
         body => {
-            inline => {
+            source => {
                 query => {
                     match => {
                         "{{my_field}}" => "{{my_value}}"
@@ -996,7 +1119,7 @@ within the C<body>) and parameters to use with the template, eg:
             params => {
                 my_field => 'foo',
                 my_value => 'bar',
-                my_size  => 5
+                my_size  => 6
             }
         }
     );
@@ -1006,11 +1129,18 @@ for more information.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<explain>,
+    C<human>,
+    C<ignore_throttled>,
     C<ignore_unavailable>,
     C<preference>,
+    C<profile>,
+    C<rest_total_hits_as_int>,
     C<scroll>,
-    C<search_type>
+    C<search_type>,
+    C<typed_keys>
 
 =head2 C<render_search_template()>
 
@@ -1023,7 +1153,7 @@ Renders the template, filling in the passed-in parameters and returns the result
 
     $results = $e->render_search_template(
         body => {
-            inline => {
+            source => {
                 query => {
                     match => {
                         "{{my_field}}" => "{{my_value}}"
@@ -1034,7 +1164,7 @@ Renders the template, filling in the passed-in parameters and returns the result
             params => {
                 my_field => 'foo',
                 my_value => 'bar',
-                my_size  => 5
+                my_size  => 6
             }
         }
     );
@@ -1046,7 +1176,9 @@ for more information.
 
     $results = $e->scroll(
         scroll      => '1m',
-        scroll_id   => $id
+        body => {
+            scroll_id   => $id
+        }
     );
 
 When a L</search()> has been performed with the
@@ -1054,14 +1186,13 @@ C<scroll> parameter, the C<scroll()>
 method allows you to keep pulling more results until the results
 are exhausted.
 
-B<NOTE:> you will almost always want to set the
-C<search_type> to C<scan> in your
-original C<search()> request.
-
-See L</scroll_helper()> and L<Search::Elasticsearch::Client::2_0::Scroll> for a helper utility
+See L</scroll_helper()> and L<Search::Elasticsearch::Client::7_0::Scroll> for a helper utility
 which makes managing scroll requests much easier.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<rest_total_hits_as_int>,
     C<scroll>,
     C<scroll_id>
 
@@ -1072,13 +1203,9 @@ for more information.
 =head2 C<clear_scroll()>
 
     $response = $e->clear_scroll(
-        scroll_id => $id | \@ids    # required
-    );
-
-Or
-
-    $response = $e->clear_scroll(
-        body => $id
+        body => {
+            scroll_id => $id | \@ids    # required
+        }
     );
 
 The C<clear_scroll()> method can clear unfinished scroll requests, freeing
@@ -1089,7 +1216,8 @@ up resources on the server.
     $scroll_helper = $e->scroll_helper( @args );
 
 Returns a new instance of the class specified in the L</scroll_helper_class>,
-which defaults to L<Search::Elasticsearch::Client::2_0::Scroll>.
+which defaults to L<Search::Elasticsearch::Client::7_0::Scroll>.
+
 
 =head2 C<msearch()>
 
@@ -1121,11 +1249,58 @@ request).  For instance:
     );
 
 Query string parameters:
-    C<search_type>
+    C<error_trace>,
+    C<human>,
+    C<max_concurrent_searches>,
+    C<max__concurrent_shard_requests>,
+    C<pre_filter_shard_size>,
+    C<rest_total_hits_as_int>,
+    C<search_type>,
+    C<typed_keys>
 
 See the L<msearch docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html>
 for more information.
 
+=head2 C<msearch_template()>
+
+    $results = $e->msearch_template(
+        index   => 'default_index' | \@indices,     # optional
+        type    => 'default_type'  | \@types,       # optional
+
+        body    => [ search_templates ]             # required
+    );
+
+The C<msearch_template()> method allows you to perform multiple searches in a single
+request using search templates.  Similar to the L</bulk()> request, each search
+request in the C<body> consists of two hashes: the metadata hash then the search request
+hash (the same data that you'd specify in the C<body> of a L</search()>
+request).  For instance:
+
+    $results = $e->msearch(
+        index   => 'default_index',
+        type    => ['default_type_1', 'default_type_2'],
+        body => [
+            # uses defaults
+            {},
+            { source => { query => { match => { user => "{{user}}" }}} params => { user => 'joe' }},
+
+            # uses a custom index
+            { index => 'not_the_default_index' },
+            { source => { query => { match => { user => "{{user}}" }}} params => { user => 'joe' }},
+        ]
+    );
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<max_concurrent_searches>,
+    C<rest_total_hits_as_int>,
+    C<search_type>,
+    C<typed_keys>
+
+See the L<msearch-template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html>
+for more information.
+
 =head2 C<explain()>
 
     $response = $e->explain(
@@ -1153,50 +1328,48 @@ For instance:
 
 Query string parameters:
     C<_source>,
-    C<_source_exclude>,
-    C<_source_include>,
+    C<_source_excludes>,
+    C<_source_includes>,
     C<analyze_wildcard>,
     C<analyzer>,
     C<default_operator>,
     C<df>,
-    C<fields>,
+    C<error_trace>,
+    C<human>,
     C<lenient>,
-    C<lowercase_expanded_terms>,
     C<parent>,
     C<preference>,
     C<q>,
-    C<routing>
+    C<routing>,
+    C<stored_fields>
 
 See the L<explain docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-explain.html>
 for more information.
 
-=head2 C<field_stats()>
+=head2 C<field_caps()>
 
-    $response = $e->field_stats(
+    $response = $e->field_caps(
         index   => 'index'   | \@indices,   # optional
-        fields  => 'field'   | \@fields,    # optional
-        level   => 'cluster' | 'indices',   # optional
         body    => { filters }              # optional
     );
 
-The C<field-stats> API returns statistical properties of a field
-(such as min and max values) without executing a search.
+The C<field-caps> API returns field types and abilities, merged across indices.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<fields>,
-    C<ignore_unavailable>,
-    C<level>
+    C<human>,
+    C<ignore_unavailable>
 
-See the L<field-stats docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-field-stats.html>
+See the L<field-caps docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-field-caps.html>
 for more information.
 
 =head2 C<search_shards()>
 
     $response = $e->search_shards(
         index   => 'index' | \@indices,     # optional
-        type    => 'type'  | \@types,       # optional
     )
 
 The C<search_shards()> method returns information about which shards on
@@ -1204,7 +1377,9 @@ which nodes will execute a search request.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>,
     C<local>,
     C<preference>,
@@ -1213,84 +1388,55 @@ Query string parameters:
 See the L<search-shards docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-shards.html>
 for more information.
 
-=head1 CRUD-BY-QUERY METHODS
-
-=head2 C<delete_by_query()>
+=head2 C<rank_eval()>
 
-    $response = $e->delete_by_query(
+    $result = $e->rank_eval(
         index   => 'index' | \@indices,     # optional
-        type    => 'type'  | \@types,       # optional,
-        body    => { delete-by-query }      # optional
+        body    => {...}                    # required
     );
 
-The C<delete_by_query()> method (available with the
-L<delete-by-query plugin|https://www.elastic.co/guide/en/elasticsearch/plugins/current/plugins-delete-by-query.html>)
-deletes all documents which match the specified query.
+The ranking evaluation API provides a way to execute test cases to determine whether search results
+are improving or worsening.
 
 Query string parameters:
     C<allow_no_indices>,
-    C<analyzer>,
-    C<default_operator>,
-    C<df>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<filter_path>,
-    C<ignore_unavailable>,
-    C<q>,
-    C<routing>,
-    C<timeout>
+    C<human>,
+    C<ignore_unavailable>
 
-See the L<delete-by-query docs|https://www.elastic.co/guide/en/elasticsearch/plugins/current/plugins-delete-by-query.html>
+See the L<rank-eval docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html>
 for more information.
 
-=head2 C<reindex()>
-
-    $response = $e->reindex(
-        body => { reindex }     # required
-    );
-
-The C<reindex()> API is used to index documents from one index or multiple indices
-to a new index.
-
-Query string parameters:
-    C<consistency>,
-    C<refresh>,
-    C<requests_per_second>,
-    C<timeout>,
-    C<wait_for_completion>
-
-See the L<reindex docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html>
-for more information.
+=head1 CRUD-BY-QUERY METHODS
 
-=head2 C<update_by_query()>
+=head2 C<delete_by_query()>
 
-    $response = $e->update_by_query(
+    $response = $e->delete_by_query(
         index   => 'index' | \@indices,     # optional
         type    => 'type'  | \@types,       # optional,
-        body    => { update-by-query }      # optional
+        body    => { delete-by-query }      # required
     );
 
-The C<update_by_query()> API is used to bulk update documents from one index or
-multiple indices using a script.
+The C<delete_by_query()> method deletes all documents which match the specified query.
 
 Query string parameters:
     C<_source>,
-    C<_source_exclude>,
-    C<_source_include>,
+    C<_source_excludes>,
+    C<_source_includes>,
     C<allow_no_indices>,
     C<analyze_wildcard>,
     C<analyzer>,
     C<conflicts>,
-    C<consistency>,
     C<default_operator>,
     C<df>,
+    C<error_trace>,
     C<expand_wildcards>,
-    C<explain>,
-    C<fielddata_fields>,
-    C<fields>,
     C<from>,
+    C<human>,
     C<ignore_unavailable>,
     C<lenient>,
-    C<lowercase_expanded_terms>,
     C<preference>,
     C<q>,
     C<refresh>,
@@ -1302,328 +1448,226 @@ Query string parameters:
     C<search_timeout>,
     C<search_type>,
     C<size>,
+    C<slices>,
     C<sort>,
     C<stats>,
-    C<suggest_field>,
-    C<suggest_mode>,
-    C<suggest_size>,
-    C<suggest_text>,
     C<terminate_after>,
-    C<timeout>,
-    C<track_scores>,
     C<version>,
-    C<version_type>,
+    C<timeout>,
+    C<wait_for_active_shards>,
     C<wait_for_completion>
 
-See the L<update_by_query docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html>
+See the L<delete-by-query docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html>
 for more information.
 
-=head2 C<reindex_rethrottle>
+=head2 C<delete_by_query_rethrottle()>
 
-    $response = $e->reindex_rethrottle(
-        task_id             => 'task_id',       # required
-        requests_per_second => $req_per_second
+    $response = $e->delete_by_query_rethrottle(
+        task_id             => 'id'         # required
+        requests_per_second => num
     );
 
-The C<reindex_rethrottle()> API is used to dynamically update the throtting
-of an existing reindex request, identified by C<task_id>.
+The C<delete_by_query_rethrottle()> API is used to dynamically update the throtting
+of an existing delete-by-query request, identified by C<task_id>.
 
 Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
     C<requests_per_second>
 
-See the L<reindex docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html>
+See the L<delete-by-query-rethrottle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html>
 for more information.
 
-=head1 PERCOLATION METHODS
+=head2 C<reindex()>
 
-=head2 C<percolate()>
+    $response = $e->reindex(
+        body => { reindex }     # required
+    );
 
-    $results = $e->percolate(
-        index   => 'my_index',      # required
-        type    => 'my_type',       # required
+The C<reindex()> API is used to index documents from one index or multiple indices
+to a new index.
 
-        body    => { percolation }  # required
-    );
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<refresh>,
+    C<requests_per_second>,
+    C<slices>,
+    C<timeout>,
+    C<wait_for_active_shards>,
+    C<wait_for_completion>
 
-Percolation is search inverted: instead of finding docs which match a
-particular query, it finds queries which match a particular document, eg
-for I<alert-me-when> functionality.
+See the L<reindex docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html>
+for more information.
 
-The C<percolate()> method runs a percolation request to find the
-queries matching a particular document. In the C<body> you should pass the
-C<_source> field of the document under the C<doc> key:
+=head2 C<reindex_rethrottle()>
 
-    $results = $e->percolate(
-        index   => 'my_index',
-        type    => 'my_type',
-        body    => {
-            doc => {
-                title => 'Elasticsearch rocks'
-            }
-        }
+    $response = $e->delete_by_query_rethrottle(
+        task_id => 'id',            # required
+        requests_per_second => num
     );
 
+The C<reindex_rethrottle()> API is used to dynamically update the throtting
+of an existing reindex request, identified by C<task_id>.
+
 Query string parameters:
-    C<allow_no_indices>,
-    C<expand_wildcards>,
-    C<ignore_unavailable>,
-    C<percolate_format>,
-    C<percolate_index>,
-    C<percolate_preference>,
-    C<percolate_routing>,
-    C<percolate_type>,
-    C<preference>,
-    C<routing>,
-    C<version>,
-    C<version_type>
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<requests_per_second>
 
-See the L<percolate docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-percolate.html>
+See the L<reindex-rethrottle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html>
 for more information.
 
-=head2 C<count_percolate()>
 
-    $results = $e->count_percolate(
-        index   => 'my_index',      # required
-        type    => 'my_type',       # required
+=head2 C<update_by_query()>
 
-        body    => { percolation }  # required
+    $response = $e->update_by_query(
+        index   => 'index' | \@indices,     # optional
+        type    => 'type'  | \@types,       # optional,
+        body    => { update-by-query }      # optional
     );
 
-The L</count_percolate()> request works just like the L</percolate()>
-request except that it returns a count of all matching queries, instead
-of the queries themselves.
-
-    $results = $e->count_percolate(
-        index   => 'my_index',
-        type    => 'my_type',
-        body    => {
-            doc => {
-                title => 'Elasticsearch rocks'
-            }
-        }
-    );
+The C<update_by_query()> API is used to bulk update documents from one index or
+multiple indices using a script.
 
 Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
     C<allow_no_indices>,
+    C<analyze_wildcard>,
+    C<analyzer>,
+    C<conflicts>,
+    C<default_operator>,
+    C<df>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<from>,
+    C<human>,
     C<ignore_unavailable>,
-    C<percolate_index>,
-    C<percolate_type>,
+    C<lenient>,
+    C<pipeline>,
     C<preference>,
+    C<q>,
+    C<refresh>,
+    C<request_cache>,
+    C<requests_per_second>,
     C<routing>,
+    C<scroll>,
+    C<scroll_size>,
+    C<search_timeout>,
+    C<search_type>,
+    C<size>,
+    C<slices>,
+    C<sort>,
+    C<stats>,
+    C<terminate_after>,
+    C<timeout>,
     C<version>,
-    C<version_type>
+    C<version_type>,
+    C<wait_for_active_shards>,
+    C<wait_for_completion>
 
-See the L<percolate docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-percolate.html>
+See the L<update_by_query docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html>
 for more information.
 
-=head2 C<mpercolate()>
-
-    $results = $e->mpercolate(
-        index   => 'my_index',               # required if type
-        type    => 'my_type',                # optional
+=head2 C<update_by_query_rethrottle()>
 
-        body    => [ percolation requests ]  # required
+    $response = $e->update_by_query_rethrottle(
+        task_id             => 'id'         # required
+        requests_per_second => num
     );
 
-Multi-percolation allows multiple L</percolate()> requests to be run
-in a single request.
-
-    $results = $e->mpercolate(
-        index   => 'my_index',
-        type    => 'my_type',
-        body    => [
-            # first request
-            { percolate => {
-                index => 'twitter',
-                type  => 'tweet'
-            }},
-            { doc => {message => 'some_text' }},
-
-            # second request
-            { percolate => {
-                index => 'twitter',
-                type  => 'tweet',
-                id    => 1
-            }},
-            {},
-        ]
-    );
+The C<update_by_query_rethrottle()> API is used to dynamically update the throtting
+of an existing update-by-query request, identified by C<task_id>.
 
 Query string parameters:
-    C<allow_no_indices>,
-    C<expand_wildcards>,
-    C<ignore_unavailable>
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<requests_per_second>
 
-See the L<mpercolate docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-percolate.html>
+See the L<update-by-query-rethrottle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html>
 for more information.
 
-=head2 C<suggest()>
-
-    $results = $e->suggest(
-        index   => 'index' | \@indices,     # optional
-
-        body    => { suggest request }      # required
-    );
-
-The C<suggest()> method is used to run
-L<did-you-mean|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesteres-phrase.html>
-or L<search-as-you-type|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-completion.html>
-suggestion requests, which can also be run as part of a L</search()> request.
-
-    $results = $e->suggest(
-        index   => 'my_index',
-        body    => {
-            my_suggestions => {
-                phrase  => {
-                    text    => 'johnny walker',
-                    field   => 'title'
-                }
-            }
-        }
-    );
-
-Query string parameters:
-    C<allow_no_indices>,
-    C<expand_wildcards>,
-    C<ignore_unavailable>,
-    C<preference>,
-    C<routing>
 
 =head1 INDEXED SCRIPT METHODS
 
-If dynamic scripting is enabled, Elasticsearch allows you to store scripts in an internal index known as
-C<.scripts> and reference them by id. The methods to manage indexed scripts are as follows:
+Elasticsearch allows you to store scripts in the cluster state
+and reference them by id. The methods to manage indexed scripts are as follows:
 
 =head2 C<put_script()>
 
     $result  = $e->put_script(
-        lang => 'lang',     # required
-        id   => 'id',       # required
-        body => { script }  # required
+        id      => 'id',       # required
+        context => $context,   # optional
+        body    => { script }  # required
     );
 
-The C<put_script()> method is used to store a script in the C<.scripts> index. For instance:
+The C<put_script()> method is used to store a script in the cluster state. For instance:
 
     $result  = $e->put_scripts(
-        lang => 'groovy',
         id   => 'hello_world',
         body => {
-          script => q(return "hello world");
+          script => {
+            lang   => 'painless',
+            source => q(return "hello world")
+          }
         }
     );
 
 Query string parameters:
-    C<op_type>,
-    C<version>,
-    C<version_type>
+    C<error_trace>,
+    C<human>
+
 
 See the L<indexed scripts docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_indexed_scripts> for more.
 
 =head2 C<get_script()>
 
     $script = $e->get_script(
-        lang => 'lang',     # required
         id   => 'id',       # required
     );
 
-Retrieve the indexed script from the C<.scripts> index.
+Retrieve the indexed script from the cluster state.
 
 Query string parameters:
-    C<version>,
-    C<version_type>
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>
 
 See the L<indexed scripts docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_indexed_scripts> for more.
 
 =head2 C<delete_script()>
 
     $script = $e->delete_script(
-        lang => 'lang',     # required
         id   => 'id',       # required
     );
 
-Delete the indexed script from the C<.scripts> index.
+Delete the indexed script from the cluster state.
 
 Query string parameters:
-    C<version>,
-    C<version_type>
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
 
 See the L<indexed scripts docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_indexed_scripts> for more.
 
-=head1 INDEXED SEARCH TEMPLATE METHODS
-
-Mustache templates can be used to create search requests.  These templates can
-be stored in the C<.scripts> index and retrieved by ID. The methods to
-manage indexed scripts are as follows:
+=head2 C<scripts_painless_execute()>
 
-=head2 C<put_template()>
-
-    $result  = $e->put_template(
-        id   => 'id',                       # required
-        body => { template } || "template"  # required
+    $result = $e->scripts_painless_execute(
+        body => {...}   # required
     );
 
-The C<put_template()> method is used to store a template in the C<.scripts> index.
-For instance:
-
-    $result  = $e->put_template(
-        id   => 'hello_world',
-        body => {
-          template => {
-            query => {
-              match => {
-                title => "hello world"
-              }
-            }
-          }
-      }
-    );
-
-Query string parameters: None
-
-See the L<indexed search template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html#_pre_registered_template> for more.
-
-=head2 C<get_template()>
-
-    $script = $e->get_template(
-        id   => 'id',       # required
-    );
-
-Retrieve the indexed template from the C<.scripts> index.
+The Painless execute API allows an arbitrary script to be executed and a result to be returned.
 
 Query string parameters:
-    C<version>,
-    C<version_type>
-
-See the L<indexed search template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html#_pre_registered_template> for more.
-
-=head2 C<delete_template()>
-
-    $script = $e->delete_template(
-        id   => 'id',       # required
-    );
-
-Delete the indexed template from the C<.scripts> index.
-
-Query string parameters: None
-
-See the L<indexed search template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html#_pre_registered_template> for more.
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
-
-__END__
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
 
-# ABSTRACT: Thin client with full support for Elasticsearch 2.x APIs
+See the L<painless execution docs|https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-execute-api.html> for more.
 
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/AsyncSearch.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/AsyncSearch.pm
new file mode 100644
index 0000000..1a03293
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/AsyncSearch.pm
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::AsyncSearch;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('async_search');
+
+1;
+
+__END__
+
+# ABSTRACT: Async Search feature of Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+The full documentation for Async Search is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/async-search.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    $response = $es->async_search->get(
+        id => $id  # required
+    )
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Autoscaling.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Autoscaling.pm
new file mode 100644
index 0000000..1310fba
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Autoscaling.pm
@@ -0,0 +1,43 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Autoscaling;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('autoscaling');
+
+1;
+
+__END__
+
+# ABSTRACT: Autoscaling feature of Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+The full documentation for Autoscaling is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/7.x/autoscaling-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    $response = $es->autoscaling->get();
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/CCR.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/CCR.pm
new file mode 100644
index 0000000..bef6c7f
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/CCR.pm
@@ -0,0 +1,228 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::CCR;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('ccr');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing cross-cluster replication APIs for Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+This module provides methods to use the cross-cluster replication feature.
+
+The full documentation for CCR is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    $response = $es->ccr->follow(
+        index   => $index,  # required
+        body    => {...}    # required
+    )
+
+The C<follow()> method creates a new follower index that is configured to follow the referenced leader index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<wait_for_active_shards>
+
+See the L<CCR follow docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-put-follow.html>
+for more information.
+
+
+=head2 C<pause_follow()>
+
+    $response = $es->ccr->pause_follow(
+        index   => $index,  # required
+    )
+
+The C<pause_follow()> method pauses following of an index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR pause follow docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-post-pause-follow.html>
+for more information.
+
+
+=head2 C<resume_follow()>
+
+    $response = $es->ccr->resume_follow(
+        index   => $index,  # required
+    )
+
+The C<resume_follow()> method resumes following of an index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR resume follow docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-post-resume-follow.html>
+for more information.
+
+
+=head2 C<unfollow()>
+
+    $response = $es->ccr->unfollow(
+        index   => $index,  # required
+    )
+
+The C<unfollow()> method converts a follower index into a normal index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR unfollow docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-post-unfollow.html>
+for more information.
+
+
+=head2 C<forget_follower()>
+
+    $response = $es->ccr->forget_follower(
+        index   => $index,  # required
+    )
+
+The C<forget_follower()> method removes the follower retention leases from the leader.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR forget_follower docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-post-forget-follower.html>
+for more information.
+
+=head1 STATS METHODS
+
+=head2 C<stats()>
+
+    $response = $es->ccr->stats()
+
+The C<stats()> method returns all stats related to cross-cluster replication.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR stats docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-stats.html>
+for more information.
+
+=head2 C<follow_stats()>
+
+    $response = $es->ccr->follow_stats(
+        index   => $index | \@indices,  # optional
+    )
+
+The C<follow_stats()> method returns shard-level stats about follower indices.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR follow stats docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-follow-stats.html>
+for more information.
+
+
+=head2 C<follow_info()>
+
+    $response = $es->ccr->follow_info(
+        index   => $index | \@indices,  # optional
+    )
+
+The C<follow_info()> method returns the parameters and the status for each follower index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR follow info docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-follow-info.html>
+for more information.
+
+=head1 AUTO-FOLLOW METHODS
+
+=head2 C<put_auto_follow_pattern()>
+
+    $response = $es->ccr->put_auto_follow_pattern(
+        name    => $name    # required
+    )
+
+The C<put_auto_follow_pattern()> method creates a new named collection of auto-follow patterns against the remote cluster specified in the request body.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR put auto follow pattern docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-put-auto-follow-pattern.html>
+for more information.
+
+
+=head2 C<get_auto_follow_pattern()>
+
+    $response = $es->ccr->get_auto_follow_pattern(
+        name    => $name    # optional
+    )
+
+The C<get_auto_follow_pattern()> method retrieves a named collection of auto-follow patterns, or all patterns.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR get auto follow pattern docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-auto-follow-pattern.html>
+for more information.
+
+=head2 C<delete_auto_follow_pattern()>
+
+    $response = $es->ccr->delete_auto_follow_pattern(
+        name    => $name    # required
+    )
+
+The C<delete_auto_follow_pattern()> method deletes a named collection of auto-follow patterns.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR delete auto follow pattern docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-delete-auto-follow-pattern.html>
+for more information.
+
+
diff --git a/lib/Search/Elasticsearch/Client/2_0/Direct/Cat.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Cat.pm
similarity index 76%
rename from lib/Search/Elasticsearch/Client/2_0/Direct/Cat.pm
rename to lib/Search/Elasticsearch/Client/7_0/Direct/Cat.pm
index a1b3cd0..5f85b52 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Direct/Cat.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Cat.pm
@@ -1,7 +1,11 @@
-package Search::Elasticsearch::Client::2_0::Direct::Cat;
-$Search::Elasticsearch::Client::2_0::Direct::Cat::VERSION = '6.81';
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Client::7_0::Direct::Cat;
+
 use Moo;
-with 'Search::Elasticsearch::Client::2_0::Role::API';
+with 'Search::Elasticsearch::Client::7_0::Role::API';
 with 'Search::Elasticsearch::Role::Client::Direct';
 use Search::Elasticsearch::Util qw(parse_params);
 use namespace::clean;
@@ -16,19 +20,25 @@ sub help {
     $self->perform_request( $defn, $params );
 }
 
-1;
-
-=pod
+#===================================
+around 'perform_request' => sub {
+#===================================
+    my $orig = shift;
+    my $self = shift;
+    my ( $defn, $params ) = parse_params(@_);
+    if ( $params->{help} && $params->{help} ne 'false' ) {
+        $defn = { %$defn, parts => {} };
+    }
 
-=encoding UTF-8
+    return $orig->( $self, $defn, $params );
 
-=head1 NAME
+};
 
-Search::Elasticsearch::Client::2_0::Direct::Cat - A client for running cat debugging requests
+1;
 
-=head1 VERSION
+__END__
 
-version 6.81
+# ABSTRACT: A client for running cat debugging requests
 
 =head1 DESCRIPTION
 
@@ -63,6 +73,7 @@ Accepts a list of column names to be output, eg:
 Formats byte-based values as bytes (C<b>), kilobytes (C<k>), megabytes
 (C<m>) or gigabytes (C<g>)
 
+
 =back
 
 It does L<Search::Elasticsearch::Role::Client::Direct>.
@@ -85,10 +96,14 @@ Returns information about index aliases, optionally limited to the specified
 index/alias names.
 
 Query string parameters:
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat aliases docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-aliases.html>
@@ -105,10 +120,14 @@ state of disk usage.
 
 Query string parameters:
     C<bytes>,
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat allocation docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-allocation.html>
@@ -124,10 +143,14 @@ Provides quick access to the document count of the entire cluster, or
 individual indices.
 
 Query string parameters:
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat count docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-count.html>
@@ -144,10 +167,14 @@ fields) loaded into fielddata.
 
 Query string parameters:
     C<bytes>,
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat fielddata docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-fielddata.html>
@@ -161,12 +188,15 @@ Provides a snapshot of how shards have located around the cluster and the
 state of disk usage.
 
 Query string parameters:
-    C<bytes>,
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
     C<ts>,
+    C<s>,
     C<v>
 
 See the L<cat health docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-health.html>
@@ -183,11 +213,16 @@ or individual indices
 
 Query string parameters:
     C<bytes>,
+    C<error_trace>,
+    C<format>,
     C<h>,
+    C<health>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
     C<pri>,
+    C<s>,
     C<v>
 
 See the L<cat indices docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-indices.html>
@@ -200,10 +235,14 @@ for more information.
 Displays the master’s node ID, bound IP address, and node name.
 
 Query string parameters:
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat master docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-master.html>
@@ -216,10 +255,14 @@ for more information.
 Returns the node attributes set per node.
 
 Query string parameters:
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat nodeattrs docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-nodeattrs.html>
@@ -232,10 +275,14 @@ for more information.
 Provides a snapshot of all of the nodes in your cluster.
 
 Query string parameters:
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat nodes docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-nodes.html>
@@ -248,10 +295,14 @@ for more information.
 Returns any cluster-level tasks which are queued on the master.
 
 Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<human>,
     C<local>,
     C<master_timeout>,
     C<h>,
     C<help>,
+    C<s>,
     C<v>
 
 See the L<cat pending-tasks docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-pending-tasks.html>
@@ -264,10 +315,14 @@ for more information.
 Returns information about plugins installed on each node.
 
 Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<human>,
     C<local>,
     C<master_timeout>,
     C<h>,
     C<help>,
+    C<s>,
     C<v>
 
 See the L<cat plugins docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-plugins.html>
@@ -286,9 +341,13 @@ stuck, try it to see if there’s any movement using C<recovery()>.
 
 Query string parameters:
     C<bytes>,
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat recovery docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-recovery.html>
@@ -301,10 +360,14 @@ for more information.
 Provides a list of registered snapshot repositories.
 
 Query string parameters:
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat repositories docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-repositories.html>
@@ -319,8 +382,13 @@ for more information.
 Provides low level information about the segments in the shards of an index.
 
 Query string parameters:
+    C<bytes>,
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
+    C<s>,
     C<v>
 
 See the L<cat shards docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-segments.html>
@@ -336,10 +404,15 @@ Provides a detailed view of what nodes contain which shards, the state and
 size of each shard.
 
 Query string parameters:
+    C<bytes>,
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat shards docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-shards.html>
@@ -348,21 +421,71 @@ for more information.
 =head2 C<snapshots()>
 
     say $e->cat->snapshots(
-        repository => 'repository' | \@repositories # required
+        repository => 'repository' | \@repositories # optional
     )
 
 Provides a list of all snapshots that belong to the specified repositories.
 
 Query string parameters:
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<ignore_unavailable>,
     C<master_timeout>,
+    C<s>,
     C<v>
 
 See the L<cat snapshots docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-snapshots.html>
 for more information.
 
+=head2 C<tasks()>
+
+    say $e->cat->tasks()
+
+Provides a list of node-level tasks.
+
+Query string parameters:
+    C<actions>,
+    C<detailed>,
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<node_id>,
+    C<parent_node>,
+    C<parent_task>,
+    C<s>,
+    C<v>
+
+See the L<cat tasks docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
+for more information.
+
+=head2 C<templates()>
+
+    say $e->cat->templates(
+        name => $name # optional
+    )
+
+Provides a list of index templates.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat templates docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/templates.html>
+for more information.
+
+
 =head2 C<thread_pool()>
 
     say $e->cat->thread_pool(
@@ -374,31 +497,17 @@ C<queue> and C<rejected> statistics are returned for the C<bulk>, C<index> and
 C<search> thread pools.
 
 Query string parameters:
-    C<full_id>,
+    C<error_trace>,
+    C<format>,
     C<h>,
     C<help>,
+    C<human>,
     C<local>,
     C<master_timeout>,
+    C<size>,
+    C<s>,
     C<v>
 
 See the L<cat thread_pool docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-thread-pool.html>
 for more information.
 
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
-
-__END__
-
-# ABSTRACT: A client for running cat debugging requests
-
diff --git a/lib/Search/Elasticsearch/Client/2_0/Direct/Cluster.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Cluster.pm
similarity index 67%
rename from lib/Search/Elasticsearch/Client/2_0/Direct/Cluster.pm
rename to lib/Search/Elasticsearch/Client/7_0/Direct/Cluster.pm
index aff91c4..1e13e50 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Direct/Cluster.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Cluster.pm
@@ -1,23 +1,32 @@
-package Search::Elasticsearch::Client::2_0::Direct::Cluster;
-$Search::Elasticsearch::Client::2_0::Direct::Cluster::VERSION = '6.81';
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Cluster;
+
 use Moo;
-with 'Search::Elasticsearch::Client::2_0::Role::API';
+with 'Search::Elasticsearch::Client::7_0::Role::API';
 with 'Search::Elasticsearch::Role::Client::Direct';
 __PACKAGE__->_install_api('cluster');
 
 1;
 
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Direct::Cluster - A client for running cluster-level requests
-
-=head1 VERSION
+__END__
 
-version 6.81
+# ABSTRACT: A client for running cluster-level requests
 
 =head1 DESCRIPTION
 
@@ -40,13 +49,17 @@ health, returning C<red>, C<yellow> or C<green> to indicate the state
 of the cluster, indices or shards.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<level>,
     C<local>,
     C<master_timeout>,
     C<timeout>,
     C<wait_for_active_shards>,
+    C<wait_for_events>,
+    C<wait_for_no_initializing_shards>,
+    C<wait_for_no_relocating_shards>,
     C<wait_for_nodes>,
-    C<wait_for_relocating_shards>,
     C<wait_for_status>
 
 See the L<cluster health docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html>
@@ -61,6 +74,7 @@ for more information.
 Returns high-level cluster stats, optionally limited to the listed nodes.
 
 Query string parameters:
+    C<error_trace>,
     C<flat_settings>,
     C<human>,
     C<timeout>
@@ -76,7 +90,10 @@ The C<get_settings()> method is used to retrieve cluster-wide settings that
 have been set with the L</put_settings()> method.
 
 Query string parameters:
+    C<error_trace>,
     C<flat_settings>,
+    C<human>,
+    C<include_defaults>,
     C<master_timeout>,
     C<timeout>
 
@@ -95,12 +112,14 @@ For instance:
 
     $response = $e->cluster->put_settings(
         body => {
-            transient => { "discovery.zen.minimum_master_nodes" => 5 }
+            transient => { "discovery.zen.minimum_master_nodes" => 6 }
         }
     );
 
 Query string parameters:
-    C<flat_settings>
+    C<error_trace>,
+    C<flat_settings>,
+    C<human>
 
 See the L<cluster settings docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-update-settings.html>
  for more information.
@@ -127,15 +146,36 @@ parameter.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<flat_settings>,
+    C<human>,
     C<ignore_unavailable>,
     C<local>,
-    C<master_timeout>
+    C<master_timeout>,
+    C<wait_for_metadata_version>,
+    C<wait_for_timeout>
 
 See the L<cluster state docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-state.html>
 for more information.
 
+=head2 C<allocation_explain()>
+
+    $response = $e->cluster->allocation_explain(
+        body => { ... shard selectors ...}  # optional
+    );
+
+Returns information about why a shard is allocated or unallocated or why.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<include_disk_info>,
+    C<include_yes_decisions>
+
+See the L<cluster allocation explain docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-allocation-explain.html>
+for more information.
+
 =head2 C<pending_tasks()>
 
     $response = $e->cluster->pending_tasks();
@@ -143,6 +183,8 @@ for more information.
 Returns a list of cluster-level tasks still pending on the master node.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<local>,
     C<master_timeout>
 
@@ -155,6 +197,7 @@ for more information.
         body => { commands }
     );
 
+
 The C<reroute()> method is used to manually reallocate shards from one
 node to another.  The C<body> should contain the C<commands> indicating
 which changes should be made. For instance:
@@ -179,29 +222,29 @@ which changes should be made. For instance:
 
 Query string parameters:
     C<dry_run>,
+    C<error_trace>,
     C<explain>,
+    C<human>,
     C<master_timeout>,
     C<metric>,
+    C<retry_failed>,
     C<timeout>
 
 See the L<reroute docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-reroute.html>
 for more information.
 
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
+=head2 C<remote_info()>
 
-=head1 COPYRIGHT AND LICENSE
+    $response = $e->cluster->remote_info();
 
-This software is Copyright (c) 2020 by Elasticsearch BV.
+The C<remote_info()> API retrieves all of the configured remote cluster information.
 
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
+Query string parameters:
+    C<error_trace>,
+    C<human>
 
-=cut
+See the L<remote_info docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-remote-info.html>
+for more information.
 
-__END__
 
-# ABSTRACT: A client for running cluster-level requests
 
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/DanglingIndices.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/DanglingIndices.pm
new file mode 100644
index 0000000..02dd59c
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/DanglingIndices.pm
@@ -0,0 +1,43 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::DanglingIndices;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('dangling_indices');
+
+1;
+
+__END__
+
+# ABSTRACT: Dangling indices feature of Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+The full documentation for Dangling Indices is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/master/indices.html#dangling-indices-api>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    $response = $es->dangling_indices->list_dangling_indices();
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/DataFrame.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/DataFrame.pm
new file mode 100644
index 0000000..fe5b5c5
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/DataFrame.pm
@@ -0,0 +1,55 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::DataFrame;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('data_frame');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Transform API for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->data_frame->explore(...);
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<data_frame>
+namespace, to support the API for the
+L<Transform|https://www.elastic.co/guide/en/elasticsearch/reference/7.3/transform-apis.html> plugin for Elasticsearch.
+
+=head1 METHODS
+
+The full documentation for the Transform plugin is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/7.3/transform-apis.html>
+
+=head2 C<explore()>
+
+    $response = $es->data_frame->put_data_frame_transform(
+        transform_id => $transform_id,
+        body  => {...}
+    );
+
+The C<put_data_frame_transform()> instantiates a transform.
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/DataFrameTransformDeprecated.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/DataFrameTransformDeprecated.pm
new file mode 100644
index 0000000..7ee0517
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/DataFrameTransformDeprecated.pm
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::DataFrameTransformDeprecated;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('data_frame_transform_deprecated');
+
+1;
+
+__END__
+
+# ABSTRACT: Data Frame Transform Deprecated feature of Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+The full documentation for Data Frame Transform Deprecated is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/7.x/transform-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->data_frame_transform_deprecated->get_transform_stats(
+        'transform_id' => $transform_id
+    );
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Enrich.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Enrich.pm
new file mode 100644
index 0000000..161cd74
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Enrich.pm
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Enrich;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('enrich');
+
+1;
+
+__END__
+
+# ABSTRACT: Enrich feature of Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+The full documentation for Enrich feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/enrich-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->enrich->get_policy(
+        'name' => $name
+    );
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Eql.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Eql.pm
new file mode 100644
index 0000000..4686290
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Eql.pm
@@ -0,0 +1,46 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Eql;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('eql');
+
+1;
+
+__END__
+
+# ABSTRACT: Eql feature of Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+The full documentation for Eql feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/eql-search-api.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->eql->search(
+        'index' => $index,
+        'body'  => {...}
+    );
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Graph.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Graph.pm
new file mode 100644
index 0000000..1084f59
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Graph.pm
@@ -0,0 +1,66 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Graph;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('graph');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Graph API for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->graph->explore(...);
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<graph>
+namespace, to support the API for the
+L<Graph|https://www.elastic.co/guide/en/x-pack/current/xpack-graph.html> plugin for Elasticsearch.
+
+=head1 METHODS
+
+The full documentation for the Graph plugin is available here:
+L<https://www.elastic.co/guide/en/graph/current/index.html>
+
+=head2 C<explore()>
+
+    $response = $es->graph->explore(
+        index => $index | \@indices,        # optional
+        type  => $type  | \@types,          # optional
+        body  => {...}
+    )
+
+The C<explore()> method allows you to discover vertices and connections which relate
+to your query.
+
+See the L<explore docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/graph-explore-api.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<routing>,
+    C<timeout>
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/ILM.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/ILM.pm
new file mode 100644
index 0000000..809e2eb
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/ILM.pm
@@ -0,0 +1,221 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::ILM;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('ilm');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing index lifecycle management APIs for Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+This module provides methods to use the index lifecycle management feature.
+
+The full documentation for ILM is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/index-lifecycle-management.html>
+
+=head1 POLICY METHODS
+
+=head2 C<put_lifecycle()>
+
+    $response = $es->ilm->put_lifecycle(
+        policy  => $policy  # required
+        body    => {...}    # required
+    )
+
+The C<put_lifecycle()> method creates or updates a lifecycle policy.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM put_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-put-lifecycle.html>
+for more information.
+
+=head2 C<put_lifecycle()>
+
+    $response = $es->ilm->put_lifecycle(
+        policy  => $policy  # required
+        body    => {...}    # required
+    )
+
+The C<put_lifecycle()> method creates or updates a lifecycle policy.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM put_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-put-lifecycle.html>
+for more information.
+
+
+=head2 C<get_lifecycle()>
+
+    $response = $es->ilm->get_lifecycle(
+        policy  => $policy  # required
+    )
+
+The C<get_lifecycle()> method retrieves the specified policy
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM get_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-get-lifecycle.html>
+for more information.
+
+=head2 C<delete_lifecycle()>
+
+    $response = $es->ilm->delete_lifecycle(
+        policy  => $policy  # required
+    )
+
+The C<delete_lifecycle()> method deletes the specified policy
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM delete_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-remove-lifecycle.html>
+for more information.
+
+=head1 INDEX MANAGEMENT METHODS
+
+=head2 C<move_to_step()>
+
+    $response = $es->ilm->move_to_step(
+        index  => $index,       # required
+        body   => {...}         # required
+    )
+
+The C<move_to_step()> method triggers execution of a specific step in the lifecycle policy.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM move_to_step docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-move-to-step.html>
+for more information.
+
+
+=head2 C<retry()>
+
+    $response = $es->ilm->retry(
+        index  => $index,       # required
+    )
+
+The C<retry()> method retries executing the policy for an index that is in the ERROR step.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM retry docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-retry.html>
+for more information.
+
+
+=head2 C<remove_lifecycle()>
+
+    $response = $es->ilm->remove_lifecycle(
+        index  => $index  # required
+    )
+
+The C<remove_lifecycle()> method removes a lifecycle from the specified index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM remove_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-remove-lifecycle.html>
+for more information.
+
+=head2 C<explain_lifecycle()>
+
+    $response = $es->ilm->explain_lifecycle(
+        index  => $index  # required
+    )
+
+The C<explain_lifecycle()> method returns information about the index’s current lifecycle state.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM explain_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-explain-lifecycle.html>
+for more information.
+
+
+=head1 OPERATION MANAGEMENT APIS
+
+=head2 C<status()>
+
+    $response = $es->ilm->status;
+
+The C<status()> method returns the current operating mode for ILM.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM status docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-get-status.html>
+for more information.
+
+=head2 C<start()>
+
+    $response = $es->ilm->start;
+
+The C<start()> method starts the index lifecycle management process.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM start docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-start.html>
+for more information.
+
+=head2 C<stop()>
+
+    $response = $es->ilm->stop;
+
+The C<stop()> method stops the index lifecycle management process.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM stop docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-stop.html>
+for more information.
diff --git a/lib/Search/Elasticsearch/Client/2_0/Direct/Indices.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Indices.pm
similarity index 79%
rename from lib/Search/Elasticsearch/Client/2_0/Direct/Indices.pm
rename to lib/Search/Elasticsearch/Client/7_0/Direct/Indices.pm
index 5776d89..78e56fc 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Direct/Indices.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Indices.pm
@@ -1,29 +1,38 @@
-package Search::Elasticsearch::Client::2_0::Direct::Indices;
-$Search::Elasticsearch::Client::2_0::Direct::Indices::VERSION = '6.81';
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Indices;
+
 use Moo;
-with 'Search::Elasticsearch::Client::2_0::Role::API';
+with 'Search::Elasticsearch::Client::7_0::Role::API';
 with 'Search::Elasticsearch::Role::Client::Direct';
 __PACKAGE__->_install_api('indices');
 
 1;
 
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Direct::Indices - A client for running index-level requests
-
-=head1 VERSION
+__END__
 
-version 6.81
+# ABSTRACT: A client for running index-level requests
 
 =head1 DESCRIPTION
 
 This module provides methods to make index-level requests, such as
 creating and deleting indices, managing type mappings, index settings,
-warmers, index templates and aliases.
+index templates and aliases.
 
 It does L<Search::Elasticsearch::Role::Client::Direct>.
 
@@ -37,17 +46,21 @@ It does L<Search::Elasticsearch::Role::Client::Direct>.
         body  => {                      # optional
             index settings
             mappings
-            warmers
+            aliases
         }
     );
 
 The C<create()> method is used to create an index. Optionally, index
-settings, type mappings and index warmers can be added at the same time.
+settings, type mappings, and aliases can be added at the same time.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<include_type_name>,
     C<master_timeout>,
     C<timeout>,
-    C<update_all_types>
+    C<update_all_types>,
+    C<wait_for_active_shards>
 
 See the L<create index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html>
 for more information.
@@ -55,22 +68,24 @@ for more information.
 =head2 C<get()>
 
     $response = $e->indices->get(
-        index   => 'index'   | \@indices    # optional
-        feature => 'feature' | \@features   # optional
+        index   => 'index'   | \@indices    # required
     );
 
-Returns the aliases, settings, mappings, and warmers for the specified indices.
-The C<feature> parameter can be set to none or more of: C<_settings>, C<_mappings>,
-C<_warmers> and C<_aliases>.
+Returns the aliases, settings, and mappings for the specified indices.
 
 See the L<get index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-index.html>.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<flat_settings>,
     C<human>,
     C<ignore_unavailable>,
-    C<local>
+    C<include_defaults>,
+    C<include_type_name>,
+    C<local>,
+    C<master_timeout>
 
 =head2 C<exists()>
 
@@ -83,8 +98,12 @@ whether the specified index or indices exist.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<flat_settings>,
+    C<human>,
     C<ignore_unavailable>,
+    C<include_defaults>,
     C<local>
 
 See the L<index exists docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-indices-exists.html>
@@ -100,7 +119,9 @@ The C<delete()> method deletes the specified indices.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>,
     C<master_timeout>,
     C<timeout>
@@ -119,7 +140,9 @@ but allowing them to be reopened later.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>
     C<master_timeout>,
     C<timeout>
@@ -137,14 +160,129 @@ The C<open()> method opens closed indices.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>
     C<master_timeout>,
-    C<timeout>
+    C<timeout>,
+    C<wait_for_active_shards>
 
 See the L<open index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html>
 for more information.
 
+=head2 C<rollover()>
+
+    $response = $e->indices->rollover(
+        alias     => $alias,                    # required
+        new_index => $index,                    # optional
+        body      => { rollover conditions }    # optional
+    );
+
+Rollover an index pointed to by C<alias> if it meets rollover conditions
+(eg max age, max docs) to a new index name.
+
+Query string parameters:
+    C<dry_run>,
+    C<error_trace>,
+    C<human>,
+    C<include_type_name>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<rollover index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-rollover-index.html>
+for more information.
+
+=head2 C<shrink()>
+
+    $response = $e->shrink(
+        index  => $index,                           # required
+        target => $target,                          # required
+        body   => { mappings, settings aliases }    # optional
+    );
+
+The shrink API shrinks the shards of an index down to a single shard (or to a factor
+of the original shards).
+
+Query string parameters:
+    C<copy_settings>,
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<shrink index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-shrink-index.html>
+for more information.
+
+=head2 C<split()>
+
+    $response = $e->split(
+        index  => $index,                           # required
+        target => $target,                          # required
+    );
+
+The split API splits a shard into multiple shards.
+
+Query string parameters:
+    C<copy_settings>,
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<split index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-split-index.html>
+for more information.
+
+=head2 C<freeze()>
+
+    $response = $e->indices->freeze(
+        $index => $index    # required
+    );
+
+The C<freeze()> API is used to freeze an index, which puts it in a state which has almost no
+overhead on the cluster.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<filter_path>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<freeze index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/freeze-index-api.html>
+for more information.
+
+=head2 C<unfreeze()>
+
+    $response = $e->indices->unfreeze(
+        $index => $index    # required
+    );
+
+The C<unfreeze()> API is used to return a frozen index to its normal state.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<filter_path>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<unfreeze index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/freeze-index-api.html>
+for more information.
+
 =head2 C<clear_cache()>
 
     $response = $e->indices->clear_cache(
@@ -156,12 +294,13 @@ or id cache for the specified indices.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<fielddata>,
     C<fields>,
+    C<human>,
     C<ignore_unavailable>,
     C<query>,
-    C<recycler>,
     C<request>
 
 See the L<clear_cache docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-clearcache.html>
@@ -179,8 +318,9 @@ happens automatically once every second by default.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
-    C<force>,
+    C<human>,
     C<ignore_unavailable>
 
 See the L<refresh index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html>
@@ -198,8 +338,10 @@ This process normally happens automatically.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<force>,
+    C<human>,
     C<ignore_unavailable>,
     C<wait_if_ongoing>
 
@@ -228,7 +370,9 @@ for more information.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>
 
 =head2 C<forcemerge()>
@@ -243,40 +387,17 @@ with care, and only on indices that are no longer being updated.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<flush>,
+    C<human>,
     C<ignore_unavailable>,
     C<max_num_segments>,
-    C<only_expunge_deletes>,
-    C<wait_for_merge>
+    C<only_expunge_deletes>
 
 See the L<forcemerge docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html>
 for more information.
 
-=head2 C<optimize()>
-
-The C<optimize()> method is deprecated in 2.x and will be replaced by L<forcemerge()>;
-
-    $response = $e->indices->optimize(
-        index => 'index' | \@indices    # optional
-    );
-
-The C<optimize()> method rewrites all the data in an index into at most
-C<max_num_segments>.  This is a very heavy operation and should only be run
-with care, and only on indices that are no longer being updated.
-
-Query string parameters:
-    C<allow_no_indices>,
-    C<expand_wildcards>,
-    C<flush>,
-    C<ignore_unavailable>,
-    C<max_num_segments>,
-    C<only_expunge_deletes>,
-    C<wait_for_merge>
-
-See the L<optimize index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-optimize.html>
-for more information.
-
 =head2 C<get_upgrade()>
 
     $response = $e->indices->get_upgrade(
@@ -288,6 +409,7 @@ upgraded, which can be done with the C<upgrade()> method.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<human>,
     C<ignore_unavailable>
@@ -305,7 +427,9 @@ The C<upgrade()> method upgrades all segments in the specified indices to the la
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>,
     C<only_ancient_segments>,
     C<wait_for_completion>
@@ -319,7 +443,7 @@ for more information.
 
     $response = $e->indices->put_mapping(
         index => 'index' | \@indices    # optional,
-        type  => 'type',                # required
+        type  => 'type',                # optional
 
         body  => { mapping }            # required
     )
@@ -345,8 +469,11 @@ For instance:
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>,
+    C<include_type_name>,
     C<master_timeout>,
     C<timeout>,
     C<update_all_types>
@@ -354,6 +481,7 @@ Query string parameters:
 See the L<put_mapping docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html>
 for more information.
 
+
 =head2 C<get_mapping()>
 
     $result = $e->indices->get_mapping(
@@ -366,9 +494,13 @@ all types in one, more or all indices.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>,
-    C<local>
+    C<include_type_name>,
+    C<local>,
+    C<master_timeout>
 
 See the L<get_mapping docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html>
 for more information.
@@ -388,9 +520,12 @@ all fields in one, more or all types and indices.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>,
     C<include_defaults>,
+    C<include_type_name>,
     C<local>
 
 See the L<get_mapping docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html>
@@ -408,7 +543,9 @@ in all specified indices, and returns C<1> or the empty string.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>,
     C<local>
 
@@ -436,29 +573,14 @@ index aliases atomically. For instance:
     );
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>,
     C<timeout>
 
 See the L<update_aliases docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html>
 for more information.
 
-=head2 C<get_aliases()>
-
-    $result = $e->indices->get_aliases(
-        index   => 'index' | \@indices,     # optional
-        alias   => 'alias' | \@aliases      # optional
-    );
-
-The C<get_aliases()> method returns a list of aliases per index for all
-the specified indices.
-
-Query string parameters:
-    C<local>,
-    C<timeout>
-
-See the L<get_aliases docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html>
-for more information.
-
 =head2 C<put_alias()>
 
     $response = $e->indices->put_alias(
@@ -479,6 +601,8 @@ The C<put_alias()> method creates an index alias. For instance:
     );
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>,
     C<timeout>
 
@@ -497,7 +621,9 @@ aliases in the specified indices.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>,
     C<local>
 
@@ -508,7 +634,7 @@ for more information.
 
     $bool = $e->indices->exists_alias(
         index   => 'index' | \@indices,     # optional
-        name    => 'alias' | \@aliases      # optional
+        name    => 'alias' | \@aliases      # required
     );
 
 The C<exists_alias()> method returns C<1> or the empty string depending on
@@ -516,7 +642,9 @@ whether the specified aliases exist in the specified indices.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
+    C<human>,
     C<ignore_unavailable>,
     C<local>
 
@@ -534,6 +662,8 @@ The C<delete_alias()> method deletes one or more aliases from one or more
 indices.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>,
     C<timeout>
 
@@ -561,10 +691,14 @@ indices or all indices. For instance:
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<flat_settings>,
+    C<human>,
     C<ignore_unavailable>,
-    C<master_timeout>
+    C<master_timeout>,
+    C<preserve_existing>,
+    C<timeout>
 
 See the L<put_settings docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html>
 for more information.
@@ -581,11 +715,14 @@ indices or all indices.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<flat_settings>,
     C<human>,
     C<ignore_unavailable>,
-    C<local>
+    C<include_defaults>,
+    C<local>,
+    C<master_timeout>
 
 See the L<get_settings docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-settings.html>
 for more information.
@@ -603,13 +740,13 @@ The C<put_template()> method is used to create or update index templates.
 
 Query string parameters:
     C<create>,
+    C<error_trace>,
     C<flat_settings>,
+    C<human>,
+    C<include_type_name>,
     C<master_timeout>,
-    C<op_type>,
     C<order>,
-    C<timeout>,
-    C<version>,
-    C<version_type>
+    C<timeout>
 
 See the L<put_template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html>
 for more information.
@@ -623,7 +760,10 @@ for more information.
 The C<get_template()> method is used to retrieve a named template.
 
 Query string parameters:
+    C<error_trace>,
     C<flat_settings>,
+    C<human>,
+    C<include_type_name>,
     C<local>,
     C<master_timeout>
 
@@ -633,12 +773,15 @@ for more information.
 =head2 C<exists_template()>
 
     $result = $e->indices->exists_template(
-        name  => 'template'                 # required
+        name  => 'template'                 # optional
     );
 
 The C<exists_template()> method is used to check whether the named template exists.
 
 Query string parameters:
+    C<error_trace>,
+    C<flat_settings>,
+    C<human>,
     C<local>,
     C<master_timeout>
 
@@ -654,6 +797,8 @@ for more information.
 The C<delete_template()> method is used to delete a named template.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>,
     C<timeout>,
     C<version>,
@@ -662,73 +807,6 @@ Query string parameters:
 See the L<delete_template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html>
 for more information.
 
-=head1 WARMER METHODS
-
-=head2 C<put_warmer()>
-
-    $response = $e->indices->put_warmer(
-        index   => 'index' | \@indices,     # optional
-        type    => 'type'  | \@types,       # optional
-        name    => 'warmer',                # required
-
-        body    => { warmer defn }          # required
-    );
-
-The C<put_warmer()> method is used to create or update named warmers which
-are used to I<warm up> new segments in the index before they are exposed
-to user searches.  For instance:
-
-    $response = $e->indices->put_warmer(
-        index   => 'my_index',
-        name    => 'date_field_warmer',
-        body    => {
-            sort => 'date'
-        }
-    );
-
-Query string parameters:
-    C<allow_no_indices>,
-    C<expand_wildcards>,
-    C<ignore_unavailable>,
-    C<master_timeout>,
-    C<request_cache>
-
-See the L<put_warmer docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-warmers.html>
-for more information.
-
-=head2 C<get_warmer()>
-
-    $response = $e->indices->get_warmer(
-        index   => 'index'  | \@indices,    # optional
-        type    => 'type'   | \@types,      # optional
-        name    => 'warmer' | \@warmers,    # optional
-    );
-
-The C<get_warmer()> method is used to retrieve warmers by name.
-
-Query string parameters:
-    C<allow_no_indices>,
-    C<expand_wildcards>,
-    C<ignore_unavailable>,
-    C<local>
-
-See the L<get_warmer docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-warmers.html>
-for more information.
-
-=head2 C<delete_warmer()>
-
-    $response = $e->indices->get_warmer(
-        index   => 'index'  | \@indices,    # required
-        name    => 'warmer' | \@warmers,    # required
-    );
-
-The C<delete_warmer()> method is used to delete warmers by name.
-
-Query string parameters:
-    C<master_timeout>
-
-See the L<delete_warmer docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-warmers.html>
-for more information.
 
 =head1 STATS METHODS
 
@@ -760,15 +838,17 @@ Allowed metrics are:
     C<request_cache>,
     C<search>,
     C<segments>,
-    C<store>,
-    C<warmer>
+    C<store>
+
 
 Query string parameters:
     C<completion_fields>,
+    C<error_trace>,
     C<fielddata_fields>,
     C<fields>,
     C<groups>,
     C<human>,
+    C<include_segment_file_sizes>,
     C<level>,
     C<types>
 
@@ -786,6 +866,7 @@ Provides insight into on-going shard recoveries.
 Query string parameters:
     C<active_only>,
     C<detailed>,
+    C<error_trace>,
     C<human>
 
 See the L<recovery docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-recovery.html>
@@ -802,6 +883,7 @@ that an index contains.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<human>,
     C<ignore_unavailable>,
@@ -821,6 +903,7 @@ copies of which shards, whether the shards are allocated or not.
 
 Query string parameters:
     C<allow_no_indices>,
+    C<error_trace>,
     C<expand_wildcards>,
     C<human>,
     C<ignore_unavailable>,
@@ -844,16 +927,8 @@ with a particular index or field - and returns the tokens.  Very useful
 for debugging analyzer configurations.
 
 Query string parameters:
-    C<analyzer>,
-    C<attributes>,
-    C<char_filter>,
-    C<explain>,
-    C<field>,
-    C<filter>,
-    C<format>,
-    C<prefer_local>,
-    C<text>,
-    C<tokenizer>
+    C<error_trace>,
+    C<human>
 
 See the L<analyze docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html>
 for more information.
@@ -872,37 +947,22 @@ whether the query is valid or not.  Most useful when C<explain> is set
 to C<true>, in which case it includes an execution plan in the output.
 
 Query string parameters:
+    C<all_shards>,
     C<allow_no_indices>,
     C<analyze_wildcard>,
     C<analyzer>,
     C<default_operator>,
     C<df>,
+    C<error_trace>,
     C<explain>,
     C<expand_wildcards>,
     C<ignore_unavailable>,
     C<lenient>,
-    C<lowercase_expanded_terms>
     C<q>,
     C<rewrite>
 
 See the L<validate_query docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-validate.html>
 for more information.
 
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
 
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
-
-__END__
-
-# ABSTRACT: A client for running index-level requests
 
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Ingest.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Ingest.pm
new file mode 100644
index 0000000..be9d6d6
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Ingest.pm
@@ -0,0 +1,130 @@
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Client::7_0::Direct::Ingest;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+__PACKAGE__->_install_api('ingest');
+
+1;
+
+__END__
+
+# ABSTRACT: A client for accessing the Ingest API
+
+=head1 DESCRIPTION
+
+This module provides methods to access the Ingest API, such as creating,
+getting, deleting and simulating ingest pipelines.
+
+It does L<Search::Elasticsearch::Role::Client::Direct>.
+
+=head1 METHODS
+
+=head2 C<put_pipeline()>
+
+    $response = $e->ingest->put_pipeline(
+        id   => $id,                # required
+        body => { pipeline defn }   # required
+    );
+
+The C<put_pipeline()> method creates or updates a pipeline with the specified ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<put pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/put-pipeline-api.html>
+for more information.
+
+=head2 C<get_pipeline()>
+
+    $response = $e->ingest->get_pipeline(
+        id   => \@id,               # optional
+    );
+
+The C<get_pipeline()> method returns pipelines with the specified IDs (or all pipelines).
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<get pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/get-pipeline-api.html>
+for more information.
+
+=head2 C<delete_pipeline()>
+
+    $response = $e->ingest->delete_pipeline(
+        id   => $id,                # required
+    );
+
+The C<delete_pipeline()> method deletes the pipeline with the specified ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<delete pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-pipeline-api.html>
+for more information.
+
+=head2 C<simulate()>
+
+    $response = $e->ingest->put_pipeline(
+        id   => $id,                # optional
+        body => { simulate args }   # required
+    );
+
+The C<simulate()> method executes the pipeline specified by ID or inline in the body
+against the docs provided in the body and provides debugging output of the execution
+process.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<verbose>
+
+See the L<simulate pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/simulate-pipeline-api.html>
+for more information.
+
+
+=head2 C<simulate()>
+
+    $response = $e->ingest->put_pipeline(
+        id   => $id,                # optional
+        body => { simulate args }   # required
+    );
+
+The C<simulate()> method executes the pipeline specified by ID or inline in the body
+against the docs provided in the body and provides debugging output of the execution
+process.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<verbose>
+
+See the L<simulate pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/simulate-pipeline-api.html>
+for more information.
+
+=head2 C<processor_grok>
+
+    $response = $e->inges->processor_grok()
+
+Retrieves the configured patterns associated with the Grok processor.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<grok processor docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html>
+for more information.
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/License.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/License.pm
new file mode 100644
index 0000000..a1e3d31
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/License.pm
@@ -0,0 +1,134 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::License;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('license');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing License API for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->license->get();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<license>
+namespace, to support the API for the License plugin for Elasticsearch.
+
+=head1 METHODS
+
+The full documentation for the License plugin is available here:
+L<https://www.elastic.co/guide/en/x-pack/current/license-management.html>
+
+=head2 C<get()>
+
+    $response = $es->license->get()
+
+The C<get()> method returns the currently installed license.
+
+See the L<license.get docs|https://www.elastic.co/guide/en/x-pack/current/listing-licenses.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<local>
+
+=head2 C<post()>
+
+    $response = $es->license->post(
+        body     => {...}          # required
+    );
+
+The C<post()> method adds or updates the license for the cluster. The C<body>
+can be passed as JSON or as a string.
+
+See the L<license.put docs|https://www.elastic.co/guide/en/x-pack/current/installing-license.html>
+for more information.
+
+Query string parameters:
+    C<acknowledge>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_basic_status()>
+
+    $response = $es->license->get_basic_status()
+
+This API enables you to check the status of your basic license.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<get-basic-status docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/get-basic-status.html> for more.
+
+=head2 C<post_start_basic()>
+
+    $response = $es->license->post_start_basic()
+
+This API enables you to  initiate an indefinite basic license, which gives access to all the basic features.
+
+Query string parameters:
+    C<acknowledge>,
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<post-start-basic docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/start-basic.html> for more.
+
+
+=head2 C<get_trial_status()>
+
+    $response = $es->license->get_trial_status()
+
+This API enables you to check the status of your trial license.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<get-trial-status docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/get-trial-status.html> for more.
+
+=head2 C<post_start_trial()>
+
+    $response = $es->license->post_start_trial()
+
+This API enables you to upgrade from a basic license to a 30-day trial license, which gives
+access to the platinum features.
+
+Query string parameters:
+    C<acknowledge>,
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<post-start-trial docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/start-trial.html> for more.
+
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/ML.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/ML.pm
new file mode 100644
index 0000000..296e75d
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/ML.pm
@@ -0,0 +1,842 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::ML;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('ml');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing ML API for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->ml->start_datafeed(...)
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with an C<ml>
+namespace, to support the
+L<Machine Learning APIs|https://www.elastic.co/guide/en/x-pack/7.0/xpack-ml.html>.
+
+The full documentation for the ML feature is available here:
+L<https://www.elastic.co/guide/en/x-pack/7.0/xpack-ml.html>
+
+=head1 DATAFEED METHODS
+
+=head2 C<put_datafeed()>
+
+    $response = $es->ml->put_datafeed(
+        datafeed_id => $id      # required
+        body        => {...}    # required
+    )
+
+The C<put_datafeed()> method enables you to instantiate a datafeed.
+
+See the L<put_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_datafeed()>
+
+    $response = $es->xpack->ml->delete_datafeed(
+        datafeed_id => $id      # required
+    )
+
+The C<delete_datafeed()> method enables you to delete a datafeed.
+
+See the L<delete_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<force>,
+    C<human>
+
+=head2 C<start_datafeed()>
+
+    $response = $es->ml->start_datafeed(
+        datafeed_id => $id      # required
+    )
+
+The C<start_datafeed()> method enables you to start a datafeed.
+
+See the L<start_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-start-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<end>,
+    C<error_trace>,
+    C<human>,
+    C<start>,
+    C<timeout>
+
+=head2 C<stop_datafeed()>
+
+    $response = $es->ml->stop_datafeed(
+        datafeed_id => $id      # required
+    )
+
+The C<stop_datafeed()> method enables you to stop a datafeed.
+
+See the L<stop_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-stop-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_datafeeds>,
+    C<error_trace>,
+    C<force>,
+    C<human>,
+    C<timeout>
+
+=head2 C<get_datafeeds()>
+
+    $response = $es->ml->get_datafeeds(
+        datafeed_id => $id      # optional
+    )
+
+The C<get_datafeeds()> method enables you to retrieve configuration information for datafeeds.
+
+See the L<get_datafeeds docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_datafeeds>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_datafeed_stats()>
+
+    $response = $es->ml->get_datafeed_stats(
+        datafeed_id => $id      # optional
+    )
+
+The C<get_datafeed_stats()> method enables you to retrieve configuration information for datafeeds.
+
+See the L<get_datafeed_stats docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-datafeed-stats.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_datafeeds>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<preview_datafeed()>
+
+    $response = $es->ml->preview_datafeed(
+        datafeed_id => $id      # required
+    )
+
+The C<preview_datafeed()> method enables you to preview a datafeed.
+
+See the L<preview_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-preview-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<update_datafeed()>
+
+    $response = $es->ml->update_datafeed(
+        datafeed_id => $id      # required
+        body        => {...}    # required
+    )
+
+The C<update_datafeed()> method enables you to update certain properties of a datafeed.
+
+See the L<update_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 JOB METHODS
+
+=head2 C<put_job()>
+
+    $response = $es->ml->put_job(
+        job_id => $id           # required
+        body        => {...}    # required
+    )
+
+The C<put_job()> method enables you to instantiate a job.
+
+See the L<put_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-job.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_job()>
+
+    $response = $es->ml->delete_job(
+        job_id => $id           # required
+    )
+
+The C<delete_job()> method enables you to delete a job.
+
+See the L<delete_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-job.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<force>,
+    C<human>,
+    C<wait_for_completion>
+
+=head2 C<open_job()>
+
+    $response = $es->ml->open_job(
+        job_id => $id           # required
+    )
+
+The C<open_job()> method enables you to open a closed job.
+
+See the L<open_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-open-job.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<close_job()>
+
+    $response = $es->ml->close_job(
+        job_id => $id           # required
+    )
+
+The C<close_job()> method enables you to close an open job.
+
+See the L<close_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-close-job.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_jobs>,
+    C<error_trace>,
+    C<force>,
+    C<human>,
+    C<timeout>
+
+=head2 C<get_jobs()>
+
+    $response = $es->ml->get_jobs(
+        job_id => $id           # optional
+    )
+
+The C<get_jobs()> method enables you to retrieve configuration information for jobs.
+
+See the L<get_jobs docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-job.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_jobs>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_job_stats()>
+
+    $response = $es->ml->get_jobs_stats(
+        job_id => $id           # optional
+    )
+
+The C<get_jobs_stats()> method enables you to retrieve usage information for jobs.
+
+See the L<get_job_statss docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-job-stats.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_jobs>,
+    C<error_trace>,
+    C<human>
+
+
+=head2 C<flush_job()>
+
+    $response = $es->ml->flush_job(
+        job_id => $id           # required
+    )
+
+The C<flush_job()> method forces any buffered data to be processed by the job.
+
+See the L<flush_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-flush-job.html>
+for more information.
+
+Query string parameters:
+    C<advance_time>,
+    C<calc_interm>,
+    C<end>,
+    C<error_trace>,
+    C<human>,
+    C<skip_time>,
+    C<start>
+
+=head2 C<post_data()>
+
+    $response = $es->ml->post_data(
+        job_id => $id           # required
+        body   => [data]        # required
+    )
+
+The C<post_data()> method enables you to send data to an anomaly detection job for analysis.
+
+See the L<post_data docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-post-data.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<reset_end>,
+    C<reset_start>
+
+=head2 C<update_job()>
+
+    $response = $es->ml->update_job(
+        job_id => $id           # required
+        body        => {...}    # required
+    )
+
+The C<update_job()> method enables you to update certain properties of a job.
+
+See the L<update_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-job.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_expired_data>
+
+    $response = $es->ml->delete_expired_data(
+    )
+
+The C<delete_expired_data()> method deletes expired machine learning data.
+
+See the L<delete_expired_data docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-expired-data.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+
+=head1 CALENDAR METHODS
+
+=head2 C<put_calendar()>
+
+    $response = $es->ml->put_calendar(
+        calendar_id => $id      # required
+        body        => {...}    # optional
+    )
+
+The C<put_calendar()> method creates a new calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<put calendar docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-calendar.html>
+for more information.
+
+=head2 C<delete_calendar()>
+
+    $response = $es->ml->delete_calendar(
+        calendar_id => $id      # required
+    )
+
+The C<delete_calendar()> method deletes the specified calendar
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<delete_calendar docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar.html>
+for more information.
+
+=head2 C<put_calendar_job()>
+
+    $response = $es->ml->put_calendar_job(
+        calendar_id => $id,     # required
+        job_id      => $id      # required
+    )
+
+The C<put_calendar_job()> method adds a job to a calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<put_calendar_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-calendar-job.html>
+for more information.
+
+=head2 C<delete_calendar_job()>
+
+    $response = $es->ml->delete_calendar_job(
+        calendar_id => $id,     # required
+        job_id      => $id      # required
+    )
+
+The C<delete_calendar_job()> method deletes a job from a calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<delete_calendar_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar-job.html>
+for more information.
+
+=head2 C<put_calendar_event()>
+
+    $response = $es->ml->post_calendar_events(
+        calendar_id => $id,     # required
+        body        => {...}    # required
+    )
+
+The C<post_calendar_events()> method adds scheduled events to a calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<post_calendar_events docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-post-calendar-events.html>
+for more information.
+
+
+=head2 C<delete_calendar_event()>
+
+    $response = $es->ml->delete_calendar_event(
+        calendar_id => $id,     # required
+        event_id    => $id      # required
+    )
+
+The C<delete_calendar_event()> method deletes an event from a calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<delete_calendar_event docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar-event.html>
+for more information.
+
+=head2 C<get_calendars()>
+
+    $response = $es->ml->get_calendars(
+        calendar_id => $id,     # optional
+    )
+
+The C<get_calendars()> method returns the specified calendar or all calendars.
+
+Query string parameters:
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<size>
+
+See the L<get_calendars docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-calendar-event.html>
+for more information.
+
+=head2 C<get_calendar_events()>
+
+    $response = $es->ml->get_calendar_events(
+        calendar_id => $id,     # required
+    )
+
+The C<get_calendar_events()> method retrieves events from a calendar.
+
+Query string parameters:
+    C<end>,
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<job_id>,
+    C<size>,
+    C<start>
+
+See the L<get_calendar_events docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-calendar-event.html>
+for more information.
+
+=head1 FILTER METHODS
+
+=head2 C<put_filter()>
+
+    $response = $es->ml->put_filter(
+        filter_id   => $id,     # required
+        body        => {...}    # required
+    )
+
+The C<put_filter()> method creates a named filter.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<put_filter docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-filter.html>
+for more information.
+
+=head2 C<update_filter()>
+
+    $response = $es->ml->update_filter(
+        filter_id   => $id,     # required
+        body        => {...}    # required
+    )
+
+The C<update_filter()> method updates the description of a filter, adds items, or removes items.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<update_filter docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-filter.html>
+for more information.
+
+=head2 C<get_filters()>
+
+    $response = $es->ml->get_filters(
+        filter_id   => $id,     # optional
+    )
+
+The C<get_filters()> method retrieves a named filter or all filters.
+
+Query string parameters:
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<size>
+
+See the L<get_filters docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-filters.html>
+for more information.
+
+=head2 C<delete_filter()>
+
+    $response = $es->ml->delete_filter(
+        filter_id   => $id,     # required
+    )
+
+The C<delete_filter()> method deletes a named filter.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<delete_filters docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-filter.html>
+for more information.
+
+=head1 FORECAST METHODS
+
+=head2 C<forecast()>
+
+    $response = $es->ml->forecast(
+        job_id      => $id      # required
+    )
+
+The C<forecast()> method enables you to create a new forecast
+
+See the L<forecast docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-forecast.html>
+for more information.
+
+Query string parameters:
+    C<duration>,
+    C<error_trace>,
+    C<expires_in>,
+    C<human>
+
+=head2 C<delete_forecast()>
+
+    $response = $es->ml->delete_forecast(
+        forecast_id => $id,     # required
+        job_id      => $id      # required
+    )
+
+The C<delete_forecast()> method enables you to delete an existing forecast.
+
+See the L<delete_forecast docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-forecast.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_forecasts>,
+    C<error_trace>,
+    C<human>,
+    C<timeout>
+
+=head1 MODEL SNAPSHOT METHODS
+
+=head2 C<delete_model_snapshot()>
+
+    $response = $es->ml->delete_model_snapshot(
+        snapshot_id => $id      # required
+    )
+
+The C<delete_model_snapshot()> method enables you to delete an existing model snapshot.
+
+See the L<delete_model_snapshot docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-snapshot.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_model_snapshots()>
+
+    $response = $es->ml->get_model_snapshots(
+        job_id      => $job_id,         # required
+        snapshot_id => $snapshot_id     # optional
+    )
+
+The C<get_model_snapshots()> method enables you to retrieve information about model snapshots.
+
+See the L<get_model_snapshots docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-snapshot.html>
+for more information.
+
+Query string parameters:
+    C<desc>,
+    C<end>,
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<size>,
+    C<sort>,
+    C<start>
+
+=head2 C<revert_model_snapshot()>
+
+    $response = $es->ml->revert_model_snapshot(
+        job_id      => $job_id,         # required
+        snapshot_id => $snapshot_id     # required
+    )
+
+The C<revert_model_snapshots()> method enables you to revert to a specific snapshot.
+
+See the L<revert_model_snapshot docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-revert-snapshot.html>
+for more information.
+
+Query string parameters:
+    C<delete_intervening_results>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<update_model_snapshot()>
+
+    $response = $es->ml->update_model_snapshot(
+        job_id      => $job_id,         # required
+        snapshot_id => $snapshot_id     # required
+    )
+
+The C<update_model_snapshots()> method enables you to update certain properties of a snapshot.
+
+See the L<update_model_snapshot docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-snapshot.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 RESULT METHODS
+
+=head2 C<get_buckets()>
+
+    $response = $es->ml->get_buckets(
+        job_id      => $job_id,         # required
+        timestamp   => $timestamp       # optional
+    )
+
+The C<get_buckets()> method enables you to retrieve job results for one or more buckets.
+
+See the L<get_buckets docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-bucket.html>
+for more information.
+
+Query string parameters:
+    C<anomaly_score>,
+    C<desc>,
+    C<end>,
+    C<error_trace>,
+    C<exclude_interim>,
+    C<expand>,
+    C<from>,
+    C<human>,
+    C<size>,
+    C<sort>,
+    C<start>
+
+=head2 C<get_overall_buckets()>
+
+    $response = $es->ml->get_overall_buckets(
+        job_id      => $job_id,         # required
+    )
+
+The C<get_overall_buckets()> method retrieves overall bucket results that summarize the bucket results of multiple jobs.
+
+See the L<get_overall_buckets docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-overall-buckets.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_jobs>,
+    C<bucket_span>,
+    C<end>,
+    C<error_trace>,
+    C<exclude_interim>,
+    C<human>,
+    C<overall_score>,
+    C<start>,
+    C<top_n>
+
+=head2 C<get_categories()>
+
+    $response = $es->ml->get_categories(
+        job_id      => $job_id,         # required
+        category_id => $category_id     # optional
+    )
+
+The C<get_categories()> method enables you to retrieve job results for one or more categories.
+
+See the L<get_categories docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-category.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<size>
+
+
+=head2 C<get_influencers()>
+
+    $response = $es->ml->get_influencers(
+        job_id      => $job_id,         # required
+    )
+
+The C<get_influencers()> method enables you to retrieve job results for one or more influencers.
+
+See the L<get_influencers docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-influencer.html>
+for more information.
+
+Query string parameters:
+    C<desc>,
+    C<end>,
+    C<error_trace>,
+    C<exclude_interim>,
+    C<expand>,
+    C<from>,
+    C<human>,
+    C<influencer_score>,
+    C<size>,
+    C<sort>,
+    C<start>
+
+=head2 C<get_records()>
+
+    $response = $es->ml->get_records(
+        job_id      => $job_id,         # required
+    )
+
+The C<get_records()> method enables you to retrieve anomaly records for a job.
+
+See the L<get_records docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-record.html>
+for more information.
+
+Query string parameters:
+    C<desc>,
+    C<end>,
+    C<error_trace>,
+    C<exclude_interim>,
+    C<expand>,
+    C<from>,
+    C<human>,
+    C<record_score>,
+    C<size>,
+    C<sort>,
+    C<start>
+
+=head1 FILE STRUCTURE METHODS
+
+=head2 C<find_file_structure>
+
+
+    $response = $es->ml->find_file_structure(
+        body    => { ... },         # required
+    )
+
+The C<find_file_structure()> method finds the structure of a text file which contains data
+that is suitable to be ingested into Elasticsearch.
+
+See the L<find_file_structure docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-find-file-structure.html>
+for more information.
+
+Query string parameters:
+    C<charset>,
+    C<column_names>,
+    C<delimiter>,
+    C<error_trace>,
+    C<explain>,
+    C<format>,
+    C<grok_pattern>,
+    C<has_header_row>,
+    C<human>,
+    C<lines_to_sample>,
+    C<quote>,
+    C<should_trim_fields>,
+    C<timeout>,
+    C<timestamp_field>,
+    C<timestamp_format>
+
+
+
+=head1 INFO METHODS
+
+
+=head2 C<info>
+
+    $response = $es->ml->info();
+
+The C<info()> method returns defaults and limits used by machine learning.
+
+See the L<find_file_structure docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/get-ml-info.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 UPGRADE METHODS
+
+=head2 C<set_upgrade_mode>
+
+    $response = $es->ml->set_upgrade_mode();
+
+The C<set_upgrade_mode()> method sets a cluster wide C<upgrade_mode> setting that prepares
+machine learning indices for an upgrade.
+
+See the L<set_upgrade_mode docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-set-upgrade-mode.html>
+for more information.
+
+Query string parameters:
+    C<enabled>,
+    C<error_trace>,
+    C<human>,
+    C<timeout>
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Migration.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Migration.pm
new file mode 100644
index 0000000..557ed83
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Migration.pm
@@ -0,0 +1,102 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Migration;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('migration');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Migration API for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->migration->deprecations();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<migration>
+namespace, to support the API
+L<Migration APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api.html>.
+
+=head1 METHODS
+
+The full documentation for the Migration APIs is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api.html>
+
+=head2 C<deprecations()>
+
+    $response = $es->migration->deprecations(
+        index => $index      # optional
+    )
+
+The C<deprecations()> API is to be used to retrieve information about different cluster, node,
+and index level settings that use deprecated features that will be removed or changed in the
+next major version.
+
+See the L<deprecations docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-deprecation.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_assistance()>
+
+    $response = $es->migration->get_assistance(
+        index => $index | \@indices      # optional
+    )
+
+The C<get_assistance()> API analyzes existing indices in the cluster and returns the information
+about indices that require some changes before the cluster can be upgraded to the next major version.
+
+See the L<get_assistance docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-assistance.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>
+
+=head2 C<upgrade()>
+
+    $response = $es->migration->upgrade(
+        index => $index       # required
+    )
+
+The C<upgrade()> API performs the upgrade of internal indices to make them compatible with the
+next major version.
+
+See the L<upgrade() docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-upgrade.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<wait_for_completion>
+
+
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Monitoring.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Monitoring.pm
new file mode 100644
index 0000000..8ec1fed
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Monitoring.pm
@@ -0,0 +1,35 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Monitoring;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('monitoring');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Monitoring for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->monitoring( body => {...} )
\ No newline at end of file
diff --git a/lib/Search/Elasticsearch/Client/2_0/Direct/Nodes.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Nodes.pm
similarity index 60%
rename from lib/Search/Elasticsearch/Client/2_0/Direct/Nodes.pm
rename to lib/Search/Elasticsearch/Client/7_0/Direct/Nodes.pm
index 78d4b8e..17aef05 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Direct/Nodes.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Nodes.pm
@@ -1,23 +1,32 @@
-package Search::Elasticsearch::Client::2_0::Direct::Nodes;
-$Search::Elasticsearch::Client::2_0::Direct::Nodes::VERSION = '6.81';
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Nodes;
+
 use Moo;
-with 'Search::Elasticsearch::Client::2_0::Role::API';
+with 'Search::Elasticsearch::Client::7_0::Role::API';
 with 'Search::Elasticsearch::Role::Client::Direct';
 __PACKAGE__->_install_api('nodes');
 
 1;
 
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Direct::Nodes - A client for running node-level requests
-
-=head1 VERSION
+__END__
 
-version 6.81
+# ABSTRACT: A client for running node-level requests
 
 =head1 DESCRIPTION
 
@@ -28,6 +37,7 @@ It does L<Search::Elasticsearch::Role::Client::Direct>.
 
 =head1 METHODS
 
+
 =head2 C<info()>
 
     $response = $e->nodes->info(
@@ -52,6 +62,7 @@ Allowed metrics are:
     C<transport>
 
 Query string parameters:
+    C<error_trace>,
     C<flat_settings>,
     C<human>
 
@@ -79,6 +90,7 @@ Allowed metrics are:
     C<breaker>,
     C<fs>,
     C<http>,
+    C<include_segment_file_sizes>,
     C<indices>,
     C<jvm>,
     C<network>,
@@ -89,11 +101,11 @@ Allowed metrics are:
     C<transport>
 
 If the C<indices> metric (or C<_all>) is specified, then
-L<indices_stats|Search::Elasticsearch::Client::2_0::Direct::Indices/indices_stats()>
+L<indices_stats|Search::Elasticsearch::Client::7_0::Direct::Indices/indices_stats()>
 information is returned on a per-node basis. Which indices stats are
 returned can be controlled with the C<index_metric> parameter:
 
-    $response = $e->cluster->node_stats(
+    $response = $e->nodes->stats(
         node_id       => 'node_1',
         metric        => ['indices','fs']
         index_metric  => ['docs','fielddata']
@@ -118,8 +130,10 @@ Allowed index metrics are:
     C<store>,
     C<warmer>
 
+
 Query string parameters:
     C<completion_fields>,
+    C<error_trace>,
     C<fielddata_fields>,
     C<fields>,
     C<groups>,
@@ -127,7 +141,7 @@ Query string parameters:
     C<level>,
     C<types>
 
-See the L<node_stats docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-stats.html>
+See the L<stats docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-stats.html>
 for more information.
 
 =head2 C<hot_threads()>
@@ -140,6 +154,8 @@ The C<hot_threads()> method is a useful tool for diagnosing busy nodes. It
 takes a snapshot of which threads are consuming the most CPU.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<ignore_idle_threads>,
     C<interval>,
     C<snapshots>,
@@ -150,21 +166,38 @@ Query string parameters:
 See the L<hot_threads docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-hot-threads.html>
 for more information.
 
-=head1 AUTHOR
+=head2 C<reload_secure_settings()>
+
+    $response = $e->nodes->reload_secure_settings(
+        node_id => $node_id | \@node_ids    # optional
+    );
 
-Enrico Zimuel <enrico.zimuel@elastic.co>
+The C<reload_secure_settings()> API will reload the reloadable settings stored in the keystore
+on each node.
 
-=head1 COPYRIGHT AND LICENSE
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<timeout>
 
-This software is Copyright (c) 2020 by Elasticsearch BV.
+See the L<reload-secure-settings docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/secure-settings.html>
+for more information.
 
-This is free software, licensed under:
+=head2 C<usage()>
 
-  The Apache License, Version 2.0, January 2004
+    $response = $e->nodes->usage(
+        node_id => $node_id | \@node_ids       # optional
+        metric  => $metric  | \@metrics        # optional
+    )
 
-=cut
+The C<usage()> API retrieve sinformation on the usage of features for each node.
 
-__END__
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<timeout>
 
-# ABSTRACT: A client for running node-level requests
+See the L<nodes_usage docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-usage.html>
+for more information.
 
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Rollup.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Rollup.pm
new file mode 100644
index 0000000..53021dd
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Rollup.pm
@@ -0,0 +1,186 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Rollup;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('rollup');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Rollups for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->rollup->search( body => {...} )
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<rollup>
+namespace, to support the
+L<Rollup APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-apis.html>.
+
+The full documentation for the Rollups feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-rollup.html>
+
+
+=head1 GENERAL METHODS
+
+=head2 C<search()>
+
+    $response = $es->rollup->search(
+        index   => $index | \@indices,      # optional
+        body    => {...}                    # optional
+    )
+
+The C<search()> method executes a normal search but can join the results from ordinary indices with
+those from rolled up indices.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<typed_keys>
+
+See the L<rollup search docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-search.html>
+for more information.
+
+=head1 JOB METHODS
+
+=head2 C<put_job()>
+
+    $response = $es->rollup->put_job(
+        id      => $id,                     # required
+        body    => {...}                    # optional
+    )
+
+The C<put_job()> method creates a rollup job which will rollup matching indices to a rolled up index
+in the background.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup create job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-put-job.html>
+for more information.
+
+=head2 C<delete_job()>
+
+    $response = $es->rollup->delete_job(
+        id      => $id,                     # required
+    )
+
+The C<delete_job()> method deletes a rollup job by ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup delete job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-delete-job.html>
+for more information.
+
+=head2 C<get_jobs()>
+
+    $response = $es->rollup->get_jobs(
+        id      => $id,     # optional
+    )
+
+The C<get_job()> method retrieves a rollup job by ID, or all jobs if not specified.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup get jobs docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-get-job.html>
+for more information.
+
+=head2 C<start_job()>
+
+    $response = $es->rollup->start_job(
+        id      => $id,     # required
+    )
+
+The C<start_job()> method starts the specified rollup job.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup start job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-start-job.html>
+for more information.
+
+=head2 C<stop_job()>
+
+    $response = $es->rollup->stop_job(
+        id      => $id,     # required
+    )
+
+The C<stop_job()> method stops the specified rollup job.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup stop job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-stop-job.html>
+for more information.
+
+=head1 DATA METHODS
+
+=head2 C<get_rollup_caps()>
+
+    $response = $es->rollup->get_rollup_caps(
+        id => $index    # optional
+    )
+
+The C<get_rollup_caps()> method returns the capabilities of any rollup jobs that have been configured for a specific index or index pattern.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<get rollup caps docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-get-rollup-caps.html>
+for more information.
+
+=head2 C<get_rollup_index_caps()>
+
+    $response = $es->rollup->get_rollup_index_caps(
+        id => $index    # optional
+    )
+
+The C<get_rollup_index_caps()> method returns the rollup capabilities of all jobs inside of a rollup index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<get rollup index caps docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-get-rollup-index-caps.html>
+for more information.
+
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/SQL.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/SQL.pm
new file mode 100644
index 0000000..a8a0140
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/SQL.pm
@@ -0,0 +1,96 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::SQL;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('sql');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing SQL for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->sql->query( body => {...} )
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with an C<sql>
+namespace, to support the
+L<SQL APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest.html>.
+
+The full documentation for the SQL feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-sql.html>
+
+=head1 GENERAL METHODS
+
+=head2 C<query()>
+
+    $response = $es->sql->query(
+        body    => {...} # required
+    )
+
+The C<query()> method executes an SQL query and returns the results.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<format>,
+    C<human>
+
+See the L<query docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest.html>
+for more information.
+
+=head2 C<translate()>
+
+    $response = $es->sql->translate(
+        body    => {...} # required
+    )
+
+The C<translate()> method takes an SQL query and returns the query DSL which would be executed.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<translate docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-translate.html>
+for more information.
+
+=head2 C<clear_cursor()>
+
+    $response = $es->sql->clear_cursor(
+        body    => {...} # required
+    )
+
+The C<clear_cursor()> method cleans up an ongoing scroll request.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<query docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest.html>
+for more information.
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/SSL.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/SSL.pm
new file mode 100644
index 0000000..34233cb
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/SSL.pm
@@ -0,0 +1,49 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::SSL;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('ssl');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing SSL for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->ssl->certificates()
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with an C<ssl>
+namespace, to support the
+L<SSL APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-ssl.html>.
+
+=head1 GENERAL METHODS
+
+=head2 C<certificates()>
+
+    $response = $es->ssl->certificates()
+
+The C<certificates()> method returns all the certificate information on a single node of Elasticsearch.
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/SearchableSnapshots copy.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/SearchableSnapshots copy.pm
new file mode 100644
index 0000000..28cdcf1
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/SearchableSnapshots copy.pm	
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::SearchableSnapshots;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('searchable_snapshots');
+
+1;
+
+__END__
+
+# ABSTRACT: Searchable Snapshots feature of Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+The full documentation for Searchable Snapshots feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/searchable-snapshots-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->searchable_snapshots->repository_stats(
+        'repository' => $repository
+    );
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Security.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Security.pm
new file mode 100644
index 0000000..8cc98b4
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Security.pm
@@ -0,0 +1,461 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Security;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('security');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Security API for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->security->authenticate();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<security>
+namespace, to support the
+L<Security APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api.html>.
+
+The full documentation for the Security feature is available here:
+L<https://www.elastic.co/guide/en/x-pack/current/xpack-security.html>
+
+=head1 GENERAL METHODS
+
+=head2 C<authenticate()>
+
+    $response = $es->security->authenticate()
+
+The C<authenticate()> method checks that the C<userinfo> is correct and returns
+a list of which roles are assigned to the user.
+
+See the L<authenticate docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-authenticate.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<clear_cached_realms()>
+
+    $response = $es->security->clear_cached_realms(
+        realms => $realms       # required  (comma-separated string)
+    );
+
+The C<clear_cached_realms()> method clears the caches for the specified realms
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<usernames>
+
+See the L<clear_cached_realms docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-cache.html>
+for more information.
+
+
+=head1 USER METHODS
+
+=head2 C<put_user()>
+
+    $response = $es->security->put_user(
+        username => $username,     # required
+        body     => {...}          # required
+    );
+
+The C<put_user()> method creates a new user or updates an existing user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_user()>
+
+    $response = $es->security->get_user(
+        username => $username | \@usernames     # optional
+    );
+
+The C<get_user()> method retrieves info for the specified users (or all users).
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_user()>
+
+    $response = $es->security->delete_user(
+        username => $username       # required
+    );
+
+The C<delete_user()> method deletes the specified user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<change_password()>
+
+    $response = $es->security->change_password(
+        username => $username       # optional
+        body => {
+            password => $password   # required
+        }
+    )
+
+The C<change_password()> method changes the password for the specified user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+=head2 C<disable_user()>
+
+    $response = $es->security->disable_user(
+        username => $username       # required
+    );
+
+The C<disable_user()> method disables the specified user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<enable_user()>
+
+    $response = $es->security->enable_user(
+        username => $username       # required
+    );
+
+The C<enable_user()> method enables the specified user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 ROLE METHODS
+
+=head2 C<put_role()>
+
+    $response = $es->security->put_role(
+        name => $name,             # required
+        body     => {...}          # required
+    );
+
+The C<put_role()> method creates a new role or updates an existing role.
+
+See the L<Role Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_role()>
+
+    $response = $es->security->get_role(
+        name => $name | \@names     # optional
+    );
+
+The C<get_role()> method retrieves info for the specified roles (or all roles).
+
+See the L<Role Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_role()>
+
+    $response = $es->security->delete_role(
+        name => $name       # required
+    );
+
+The C<delete_role()> method deletes the specified role.
+
+See the L<Role Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<clear_cached_roles()>
+
+    $response = $es->security->clear_cached_roles(
+        names => $names       # required  (comma-separated string)
+    );
+
+The C<clear_cached_roles()> method clears the caches for the specified roles.
+
+See the L<Role Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+
+=head1 ROLE MAPPING METHODS
+
+=head2 C<put_role_mapping()>
+
+    $response = $es->security->put_role_mapping(
+        name => $name,             # required
+        body     => {...}          # required
+    );
+
+The C<put_role_mapping()> method creates a new role mapping or updates an existing role mapping.
+
+See the L<Role Mapping docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_role_mapping()>
+
+    $response = $es->security->get_role_mapping(
+        name => $name,             # optional
+    );
+
+The C<get_role_mapping()> method retrieves one or more role mappings.
+
+See the L<Role Mapping docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_role_mapping()>
+
+    $response = $es->security->delete_role_mapping(
+        name => $name,             # required
+    );
+
+The C<delete_role_mapping()> method deletes a role mapping.
+
+See the L<Role Mapping docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 TOKEN METHODS
+
+=head2 C<get_token()>
+
+    $response = $es->security->get_token(
+        body     => {...}          # required
+    );
+
+The C<get_token()> method enables you to create bearer tokens for access without
+requiring basic authentication.
+
+See the L<Token Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-tokens.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<invalidate_token()>
+
+    $response = $es->security->invalidate_token(
+        body     => {...}          # required
+    );
+
+The C<invalidate_token()> method enables you to invalidate bearer tokens for access without
+requiring basic authentication.
+
+See the L<Token Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-tokens.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 API KEY METHODS
+
+=head2 C<create_api_key()>
+
+    $response = $es->security->create_api_key(
+        body    => {...}            # required
+    )
+
+The C<create_api_key()> API is used to create API keys which can be used for access instead
+of basic authentication.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<refresh>
+
+See the L<Create API Key docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html> for more.
+
+=head2 C<get_api_key()>
+
+    $response = $es->security->get_api_key(
+        id          => $id,         # optional
+        name        => $name,       # optional
+        realm_name  => $realm,      # optional
+        username    => $username    # optional
+    )
+
+The C<get_api_key()> API is used to get information about an API key.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<id>,
+    C<name>,
+    C<realm_name>,
+    C<username>
+
+See the L<Get API Key docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-api-key.html> for more.
+
+=head2 C<invalidate_api_key()>
+
+    $response = $es->security->invalidate_api_key(
+        id          => $id,         # optional
+        name        => $name,       # optional
+        realm_name  => $realm,      # optional
+        username    => $username    # optional
+    )
+
+The C<invalidate_api_key()> API is used to invalidate an API key.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<id>,
+    C<name>,
+    C<realm_name>,
+    C<username>
+
+See the L<Invalidate API Key docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-api-key.html> for more.
+
+=head1 USER PRIVILEGE METHODS
+
+=head2 C<get_user_privileges()>
+
+    $response = $es->get_user_privileges();
+
+ The C<get_user_privileges()> method retrieves the privileges granted to the current user.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+=head2 C<has_privileges()>
+    $response = $es->has_privileges(
+        user    => $user,   # optional
+        body    => {...}    # required
+    );
+
+ The C<has_privileges()> method checks whether the current or specified user has the listed privileges.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<Has Privileges docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-has-privileges.html> for more.
+
+
+=head1 APPLICATION PRIVILEGE METHODS
+
+=head2 C<put_privileges()>
+
+    $response = $es->put_privileges(
+        application     => $application,    # required
+        name            => $name,           # required
+        body            => {...}            # required
+    );
+
+ The C<put_privileges()> method creates or updates the named privilege for a particular application.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<refresh>
+
+See the L<Create or Update Application Privileges docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html> for more.
+
+=head2 C<get_privileges()>
+
+    $response = $es->get_privileges(
+        application     => $application,    # required
+        name            => $name,           # required
+    );
+
+ The C<get_privileges()> method retrieves the named privilege for a particular application.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<Get Application Privileges docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-privileges.html> for more.
+
+=head2 C<delete_privileges()>
+
+    $response = $es->delete_privileges(
+        application     => $application,    # required
+        name            => $name,           # required
+    );
+
+ The C<delete_privileges()> method deletes the named privilege for a particular application.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<refresh>
+
+See the L<Delete Application Privileges docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-privilege.html> for more.
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Slm.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Slm.pm
new file mode 100644
index 0000000..ebca867
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Slm.pm
@@ -0,0 +1,43 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Slm;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('slm');
+
+1;
+
+__END__
+
+# ABSTRACT: Snapshot lifecycle management feature of Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+The full documentation for Snapshot lifecycle management feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-lifecycle-management-api.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->slm->get_status();
+
diff --git a/lib/Search/Elasticsearch/Client/2_0/Direct/Snapshot.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Snapshot.pm
similarity index 85%
rename from lib/Search/Elasticsearch/Client/2_0/Direct/Snapshot.pm
rename to lib/Search/Elasticsearch/Client/7_0/Direct/Snapshot.pm
index c29f813..1a0bf5a 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Direct/Snapshot.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Snapshot.pm
@@ -1,23 +1,19 @@
-package Search::Elasticsearch::Client::2_0::Direct::Snapshot;
-$Search::Elasticsearch::Client::2_0::Direct::Snapshot::VERSION = '6.81';
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Client::7_0::Direct::Snapshot;
+
 use Moo;
-with 'Search::Elasticsearch::Client::2_0::Role::API';
+with 'Search::Elasticsearch::Client::7_0::Role::API';
 with 'Search::Elasticsearch::Role::Client::Direct';
 __PACKAGE__->_install_api('snapshot');
 
 1;
 
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Direct::Snapshot - A client for managing snapshot/restore
-
-=head1 VERSION
+__END__
 
-version 6.81
+# ABSTRACT: A client for managing snapshot/restore
 
 =head1 DESCRIPTION
 
@@ -29,6 +25,7 @@ It does L<Search::Elasticsearch::Role::Client::Direct>.
 
 =head1 METHODS
 
+
 =head2 C<create_repository()>
 
     $e->snapshot->create_repository(
@@ -39,6 +36,8 @@ It does L<Search::Elasticsearch::Role::Client::Direct>.
 Create a repository for backups.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>,
     C<timeout>,
     C<verify>
@@ -55,6 +54,8 @@ for more information.
 Retrieve existing repositories.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<local>,
     C<master_timeout>
 
@@ -70,6 +71,8 @@ for more information.
 Verify existing repository.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>,
     C<timeout>
 
@@ -85,6 +88,8 @@ for more information.
 Delete repositories by name.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>,
     C<timeout>
 
@@ -104,6 +109,8 @@ Create a snapshot of the whole cluster or individual indices in the named
 repository.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>,
     C<wait_for_completion>
 
@@ -117,7 +124,11 @@ Query string parameters:
 Retrieve snapshots in the named repository.
 
 Query string parameters:
-    C<master_timeout>
+    C<error_trace>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>,
+    C<verbose>
 
 See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
 for more information.
@@ -132,11 +143,14 @@ for more information.
 Delete snapshot in the named repository.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>
 
 See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
 for more information.
 
+
 =head2 C<restore()>
 
     $e->snapshot->restore(
@@ -149,6 +163,8 @@ for more information.
 Restore a named snapshot.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
     C<master_timeout>,
     C<wait_for_completion>
 
@@ -165,26 +181,10 @@ for more information.
 Returns status information about the specified snapshots.
 
 Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<ignore_unavailable>,
     C<master_timeout>
 
 See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
 for more information.
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
-
-__END__
-
-# ABSTRACT: A client for managing snapshot/restore
-
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Tasks.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Tasks.pm
new file mode 100644
index 0000000..a12cce7
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Tasks.pm
@@ -0,0 +1,97 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Tasks;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+__PACKAGE__->_install_api('tasks');
+
+1;
+
+__END__
+
+# ABSTRACT: A client for accessing the Task Management API
+
+=head1 DESCRIPTION
+
+This module provides methods to access the Task Management API, such as listing
+tasks and cancelling tasks.
+
+It does L<Search::Elasticsearch::Role::Client::Direct>.
+
+=head1 METHODS
+
+=head2 C<list()>
+
+    $response = $e->tasks->list(
+        task_id => $task_id  # optional
+    );
+
+The C<list()> method returns all running tasks or, if a C<task_id> is specified, info
+about that task.
+
+Query string parameters:
+    C<actions>,
+    C<detailed>,
+    C<error_trace>,
+    C<group_by>,
+    C<human>,
+    C<nodes>,
+    C<parent_task_id>,
+    C<timeout>,
+    C<wait_for_completion>
+
+See the L<task management docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
+for more information.
+
+=head2 C<get()>
+
+    $response = $e->tasks->get(
+        task_id => $task_id  # required
+    );
+
+The C<get()> method returns the task with the specified ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<wait_for_completion>
+
+See the L<task management docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
+for more information.
+
+=head2 C<cancel()>
+
+    $response = $e->tasks->cancel(
+        task_id => $task_id  # required
+    );
+
+The C<cancel()> method attempts to cancel the specified C<task_id> or multiple tasks.
+
+Query string parameters:
+    C<actions>,
+    C<error_trace>,
+    C<human>,
+    C<nodes>,
+    C<parent_task_id>,
+    C<timeout>
+
+See the L<task management docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
+for more information.
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Transform.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Transform.pm
new file mode 100644
index 0000000..68ef244
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Transform.pm
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Transform;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('transform');
+
+1;
+
+__END__
+
+# ABSTRACT: Transform feature of Search::Elasticsearch 7.x
+
+=head2 DESCRIPTION
+
+The full documentation for Transform feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/transform-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->transform->get_transform(
+        'transform_id' => $transform_id
+    );
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/Watcher.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/Watcher.pm
new file mode 100644
index 0000000..973d292
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/Watcher.pm
@@ -0,0 +1,225 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::Watcher;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('watcher');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Watcher API for Search::Elasticsearch 7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->watcher->start();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<watcher>
+namespace, to support the
+L<Watcher APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api.html>.
+
+=head1 METHODS
+
+The full documentation for the Watcher feature is available here:
+L<https://www.elastic.co/guide/en/x-pack/current/xpack-alerting.html>
+
+=head2 C<put_watch()>
+
+    $response = $es->watcher->put_watch(
+        id    => $watch_id,     # required
+        body  => {...}
+    );
+
+The C<put_watch()> method is used to register a new watcher or to update
+an existing watcher.
+
+See the L<put_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-put-watch.html>
+for more information.
+
+Query string parameters:
+    C<active>,
+    C<error_trace>,
+    C<human>,
+    C<if_primary_term>,
+    C<if_seq_no>,
+    C<master_timeout>,
+    C<version>
+
+=head2 C<get_watch()>
+
+    $response = $es->watcher->get_watch(
+        id    => $watch_id,     # required
+    );
+
+The C<get_watch()> method is used to retrieve a watch by ID.
+
+See the L<get_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-get-watch.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_watch()>
+
+    $response = $es->watcher->delete_watch(
+        id    => $watch_id,     # required
+    );
+
+The C<delete_watch()> method is used to delete a watch by ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<force>,
+    C<human>,
+    C<master_timeout>
+
+See the L<delete_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-delete-watch.html>
+for more information.
+
+=head2 C<execute_watch()>
+
+    $response = $es->watcher->execute_watch(
+        id    => $watch_id,     # optional
+        body  => {...}          # optional
+    );
+
+The C<execute_watch()> method forces the execution of a previously
+registered watch.  Optional parameters may be passed in the C<body>.
+
+Query string parameters:
+    C<debug>,
+    C<error_trace>,
+    C<human>
+
+See the L<execute_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-execute-watch.html>
+for more information.
+
+=head2 C<ack_watch()>
+
+    $response = $es->watcher->ack_watch(
+        watch_id => $watch_id,                  # required
+        action_id => $action_id | \@action_ids  # optional
+    );
+
+The C<ack_watch()> method is used to manually throttle the execution of
+a watch.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>
+
+See the L<ack_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-ack-watch.html>
+for more information.
+
+=head2 C<activate_watch()>
+
+    $response = $es->watcher->activate_watch(
+        watch_id => $watch_id,                  # required
+    );
+
+The C<activate_watch()> method is used to activate a deactive watch.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>
+
+See the L<activate_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-activate-watch.html>
+for more information.
+
+=head2 C<deactivate_watch()>
+
+    $response = $es->watcher->deactivate_watch(
+        watch_id => $watch_id,                  # required
+    );
+
+The C<deactivate_watch()> method is used to deactivate an active watch.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>
+
+See the L<deactivate_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-deactivate-watch.html>
+for more information.
+
+=head2 C<stats()>
+
+    $response = $es->watcher->stats(
+        metric => $metric       # optional
+    );
+
+The C<stats()> method returns information about the status of the watcher plugin.
+
+See the L<stats docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-stats.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<stop()>
+
+    $response = $es->watcher->stop();
+
+The C<stop()> method stops the watcher service if it is running.
+
+See the L<stop docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-stop.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<start()>
+
+    $response = $es->watcher->start();
+
+The C<start()> method starts the watcher service if it is not already running.
+
+See the L<start docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-start.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<restart()>
+
+    $response = $es->watcher->restart();
+
+The C<restart()> method stops then starts the watcher service.
+
+See the L<restart docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-restart.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+
+
diff --git a/lib/Search/Elasticsearch/Client/7_0/Direct/XPack.pm b/lib/Search/Elasticsearch/Client/7_0/Direct/XPack.pm
new file mode 100644
index 0000000..83e74de
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Direct/XPack.pm
@@ -0,0 +1,60 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Direct::XPack;
+
+use Moo;
+with 'Search::Elasticsearch::Client::7_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('xpack');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing XPack APIs for Search::Elasticsearch v7.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->xpack->info();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<xpack>
+namespace.
+
+=head1 METHODS
+
+=head2 C<info()>
+
+    my $response = $es->xpack->info();
+
+Provides general information about the installed X-Pack features.
+
+See the L<info|https://www.elastic.co/guide/en/elasticsearch/reference/current/info-api.html>
+for more information.
+
+=head2 C<usage()>
+
+    my $response = $es->xpack->usage();
+
+Provides usage information about the installed X-Pack features.
+
+See the L<usage|https://www.elastic.co/guide/en/elasticsearch/reference/current/usage-api.html>
+for more information.
\ No newline at end of file
diff --git a/lib/Search/Elasticsearch/Client/7_0/Role/API.pm b/lib/Search/Elasticsearch/Client/7_0/Role/API.pm
new file mode 100644
index 0000000..aafdbaf
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/Role/API.pm
@@ -0,0 +1,6379 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Role::API;
+
+use Moo::Role;
+with 'Search::Elasticsearch::Role::API';
+
+use Search::Elasticsearch::Util qw(throw);
+use namespace::clean;
+
+has 'api_version' => ( is => 'ro', default => '7_0' );
+
+our %API;
+
+#===================================
+sub api {
+#===================================
+    my $name = $_[1] || return \%API;
+    return $API{$name}
+        || throw( 'Internal', "Unknown api name ($name)" );
+}
+
+#===================================
+%API = (
+#===================================
+
+    'bulk.metadata' => {
+        params => {
+            '_index'                 => '_index',
+            'index'                  => '_index',
+            '_id'                    => '_id',
+            'id'                     => '_id',
+            'pipeline'               => 'pipeline',
+            'routing'                => 'routing',
+            '_routing'               => 'routing',
+            'parent'                 => 'parent',
+            '_parent'                => 'parent',
+            'timestamp'              => 'timestamp',
+            '_timestamp'             => 'timestamp',
+            'ttl'                    => 'ttl',
+            '_ttl'                   => 'ttl',
+            'version'                => 'version',
+            '_version'               => 'version',
+            'version_type'           => 'version_type',
+            '_version_type'          => 'version_type',
+            'if_seq_no'              => 'if_seq_no',
+            'if_primary_term'        => 'if_primary_term',
+            'lang'                   => 'lang',
+            'require_alias'          => 'require_alias',
+            'refresh'                => 'refresh',
+            'retry_on_conflict'      => 'retru_on_conflict',
+            'wait_for_active_shards' => 'wait_for_active_shards',
+            '_source'                => '_source',
+            '_source_excludes'       => '_source_excludes',
+            '_source_includes'       => '_source_includes',
+            'timeout'                => 'timeout'
+        }
+    },
+    'bulk.update' => {
+        params => [
+            '_source',          '_source_includes',
+            '_source_excludes', 'detect_noop',
+            'doc',              'doc_as_upsert',
+            'fields',           'retry_on_conflict',
+            'scripted_upsert',  'script',
+            'upsert',           'lang',
+            'params'
+        ]
+    },
+    'bulk.required' => { params => ['index'] },
+
+#=== AUTOGEN - START ===
+
+    'bulk' => {
+        body   => { required => 1 },
+        doc    => "docs-bulk",
+        method => "POST",
+        parts  => { index => {}, type => {} },
+        paths  => [
+            [ { index => 0, type => 1 }, "{index}", "{type}", "_bulk" ],
+            [ { index => 0 }, "{index}", "_bulk" ],
+            [ {}, "_bulk" ],
+        ],
+        qs => {
+            _source                => "list",
+            _source_excludes       => "list",
+            _source_includes       => "list",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            pipeline               => "string",
+            refresh                => "enum",
+            require_alias          => "boolean",
+            routing                => "string",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+        serialize => "bulk",
+    },
+
+    'clear_scroll' => {
+        body   => {},
+        doc    => "clear-scroll-api",
+        method => "DELETE",
+        parts  => { scroll_id => { multi => 1 } },
+        paths  => [
+            [ { scroll_id => 2 }, "_search", "scroll", "{scroll_id}" ],
+            [ {}, "_search", "scroll" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'close_point_in_time' => {
+        body   => {},
+        doc    => "point-in-time-api",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_pit" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'count' => {
+        body   => {},
+        doc    => "search-count",
+        method => "POST",
+        parts  => { index => { multi => 1 }, type => { multi => 1 } },
+        paths  => [
+            [ { index => 0, type => 1 }, "{index}", "{type}", "_count" ],
+            [ { index => 0 }, "{index}", "_count" ],
+            [ {}, "_count" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            analyze_wildcard   => "boolean",
+            analyzer           => "string",
+            default_operator   => "enum",
+            df                 => "string",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_throttled   => "boolean",
+            ignore_unavailable => "boolean",
+            lenient            => "boolean",
+            min_score          => "number",
+            preference         => "string",
+            q                  => "string",
+            routing            => "list",
+            terminate_after    => "number",
+        },
+    },
+
+    'create' => {
+        body   => { required => 1 },
+        doc    => "docs-index_",
+        method => "PUT",
+        parts  => { id => {}, index => {}, type => {} },
+        paths  => [
+            [   { id => 2, index => 0, type => 1 },
+                "{index}", "{type}", "{id}", "_create",
+            ],
+            [ { id => 2, index => 0 }, "{index}", "_create", "{id}" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            pipeline               => "string",
+            refresh                => "enum",
+            routing                => "string",
+            timeout                => "time",
+            version                => "number",
+            version_type           => "enum",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'delete' => {
+        doc    => "docs-delete",
+        method => "DELETE",
+        parts  => { id => {}, index => {}, type => {} },
+        paths  => [
+            [   { id => 2, index => 0, type => 1 }, "{index}",
+                "{type}",                           "{id}"
+            ],
+            [ { id => 2, index => 0 }, "{index}", "_doc", "{id}" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            if_primary_term        => "number",
+            if_seq_no              => "number",
+            refresh                => "enum",
+            routing                => "string",
+            timeout                => "time",
+            version                => "number",
+            version_type           => "enum",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'delete_by_query' => {
+        body   => { required => 1 },
+        doc    => "docs-delete-by-query",
+        method => "POST",
+        parts  => {
+            index => { multi => 1, required => 1 },
+            type  => { multi => 1 }
+        },
+        paths => [
+            [   { index => 0, type => 1 }, "{index}",
+                "{type}",                  "_delete_by_query",
+            ],
+            [ { index => 0 }, "{index}", "_delete_by_query" ],
+        ],
+        qs => {
+            _source                => "list",
+            _source_excludes       => "list",
+            _source_includes       => "list",
+            allow_no_indices       => "boolean",
+            analyze_wildcard       => "boolean",
+            analyzer               => "string",
+            conflicts              => "enum",
+            default_operator       => "enum",
+            df                     => "string",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            from                   => "number",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            lenient                => "boolean",
+            max_docs               => "number",
+            preference             => "string",
+            q                      => "string",
+            refresh                => "boolean",
+            request_cache          => "boolean",
+            requests_per_second    => "number",
+            routing                => "list",
+            scroll                 => "time",
+            scroll_size            => "number",
+            search_timeout         => "time",
+            search_type            => "enum",
+            size                   => "number",
+            slices                 => "number|string",
+            sort                   => "list",
+            stats                  => "list",
+            terminate_after        => "number",
+            timeout                => "time",
+            version                => "boolean",
+            wait_for_active_shards => "string",
+            wait_for_completion    => "boolean",
+        },
+    },
+
+    'delete_by_query_rethrottle' => {
+        doc    => "docs-delete-by-query",
+        method => "POST",
+        parts  => { task_id => {} },
+        paths  => [
+            [   { task_id => 1 }, "_delete_by_query",
+                "{task_id}",      "_rethrottle",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            requests_per_second => "number",
+        },
+    },
+
+    'delete_script' => {
+        doc    => "modules-scripting",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 1 }, "_scripts", "{id}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'exists' => {
+        doc    => "docs-get",
+        method => "HEAD",
+        parts  => { id => {}, index => {}, type => {} },
+        paths  => [
+            [   { id => 2, index => 0, type => 1 }, "{index}",
+                "{type}",                           "{id}"
+            ],
+            [ { id => 2, index => 0 }, "{index}", "_doc", "{id}" ],
+        ],
+        qs => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            refresh          => "boolean",
+            routing          => "string",
+            stored_fields    => "list",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'exists_source' => {
+        doc    => "docs-get",
+        method => "HEAD",
+        parts  => { id => {}, index => {}, type => {} },
+        paths  => [
+            [   { id => 2, index => 0, type => 1 },
+                "{index}", "{type}", "{id}", "_source",
+            ],
+            [ { id => 2, index => 0 }, "{index}", "_source", "{id}" ],
+        ],
+        qs => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            refresh          => "boolean",
+            routing          => "string",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'explain' => {
+        body  => {},
+        doc   => "search-explain",
+        parts => { id => {}, index => {}, type => {} },
+        paths => [
+            [   { id => 2, index => 0, type => 1 }, "{index}",
+                "{type}",                           "{id}",
+                "_explain",
+            ],
+            [ { id => 2, index => 0 }, "{index}", "_explain", "{id}" ],
+        ],
+        qs => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            analyze_wildcard => "boolean",
+            analyzer         => "string",
+            default_operator => "enum",
+            df               => "string",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            lenient          => "boolean",
+            preference       => "string",
+            q                => "string",
+            routing          => "string",
+            stored_fields    => "list",
+        },
+    },
+
+    'field_caps' => {
+        body  => {},
+        doc   => "search-field-caps",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_field_caps" ],
+            [ {}, "_field_caps" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            fields             => "list",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_unmapped   => "boolean",
+        },
+    },
+
+    'get' => {
+        doc   => "docs-get",
+        parts => { id => {}, index => {}, type => {} },
+        paths => [
+            [   { id => 2, index => 0, type => 1 }, "{index}",
+                "{type}",                           "{id}"
+            ],
+            [ { id => 2, index => 0 }, "{index}", "_doc", "{id}" ],
+        ],
+        qs => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            refresh          => "boolean",
+            routing          => "string",
+            stored_fields    => "list",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'get_script' => {
+        doc   => "modules-scripting",
+        parts => { id => {} },
+        paths => [ [ { id => 1 }, "_scripts", "{id}" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'get_script_context' => {
+        doc   => "painless-contexts",
+        parts => {},
+        paths => [ [ {}, "_script_context" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'get_script_languages' => {
+        doc   => "modules-scripting",
+        parts => {},
+        paths => [ [ {}, "_script_language" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'get_source' => {
+        doc   => "docs-get",
+        parts => { id => {}, index => {}, type => {} },
+        paths => [
+            [   { id => 2, index => 0, type => 1 },
+                "{index}", "{type}", "{id}", "_source",
+            ],
+            [ { id => 2, index => 0 }, "{index}", "_source", "{id}" ],
+        ],
+        qs => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            refresh          => "boolean",
+            routing          => "string",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'index' => {
+        body   => { required => 1 },
+        doc    => "docs-index_",
+        method => "POST",
+        parts  => { id => {}, index => {}, type => {} },
+        paths  => [
+            [   { id => 2, index => 0, type => 1 }, "{index}",
+                "{type}",                           "{id}"
+            ],
+            [ { id    => 2, index => 0 }, "{index}", "_doc", "{id}" ],
+            [ { index => 0, type  => 1 }, "{index}", "{type}" ],
+            [ { index => 0 }, "{index}", "_doc" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            if_primary_term        => "number",
+            if_seq_no              => "number",
+            op_type                => "enum",
+            pipeline               => "string",
+            refresh                => "enum",
+            require_alias          => "boolean",
+            routing                => "string",
+            timeout                => "time",
+            version                => "number",
+            version_type           => "enum",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'info' => {
+        doc   => "index",
+        parts => {},
+        paths => [ [ {} ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'mget' => {
+        body  => { required => 1 },
+        doc   => "docs-multi-get",
+        parts => { index => {}, type => {} },
+        paths => [
+            [ { index => 0, type => 1 }, "{index}", "{type}", "_mget" ],
+            [ { index => 0 }, "{index}", "_mget" ],
+            [ {}, "_mget" ],
+        ],
+        qs => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            refresh          => "boolean",
+            routing          => "string",
+            stored_fields    => "list",
+        },
+    },
+
+    'msearch' => {
+        body  => { required => 1 },
+        doc   => "search-multi-search",
+        parts => { index => { multi => 1 }, type => { multi => 1 } },
+        paths => [
+            [ { index => 0, type => 1 }, "{index}", "{type}", "_msearch" ],
+            [ { index => 0 }, "{index}", "_msearch" ],
+            [ {}, "_msearch" ],
+        ],
+        qs => {
+            ccs_minimize_roundtrips       => "boolean",
+            error_trace                   => "boolean",
+            filter_path                   => "list",
+            human                         => "boolean",
+            max_concurrent_searches       => "number",
+            max_concurrent_shard_requests => "number",
+            pre_filter_shard_size         => "number",
+            rest_total_hits_as_int        => "boolean",
+            search_type                   => "enum",
+            typed_keys                    => "boolean",
+        },
+        serialize => "bulk",
+    },
+
+    'msearch_template' => {
+        body  => { required => 1 },
+        doc   => "search-multi-search",
+        parts => { index => { multi => 1 }, type => { multi => 1 } },
+        paths => [
+            [   { index => 0, type => 1 }, "{index}",
+                "{type}",                  "_msearch",
+                "template",
+            ],
+            [ { index => 0 }, "{index}", "_msearch", "template" ],
+            [ {}, "_msearch", "template" ],
+        ],
+        qs => {
+            ccs_minimize_roundtrips => "boolean",
+            error_trace             => "boolean",
+            filter_path             => "list",
+            human                   => "boolean",
+            max_concurrent_searches => "number",
+            rest_total_hits_as_int  => "boolean",
+            search_type             => "enum",
+            typed_keys              => "boolean",
+        },
+        serialize => "bulk",
+    },
+
+    'mtermvectors' => {
+        body  => {},
+        doc   => "docs-multi-termvectors",
+        parts => { index => {}, type => {} },
+        paths => [
+            [   { index => 0, type => 1 }, "{index}",
+                "{type}",                  "_mtermvectors"
+            ],
+            [ { index => 0 }, "{index}", "_mtermvectors" ],
+            [ {}, "_mtermvectors" ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            field_statistics => "boolean",
+            fields           => "list",
+            filter_path      => "list",
+            human            => "boolean",
+            ids              => "list",
+            offsets          => "boolean",
+            payloads         => "boolean",
+            positions        => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            routing          => "string",
+            term_statistics  => "boolean",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'open_point_in_time' => {
+        doc    => "point-in-time-api",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}", "_pit" ], [ {}, "_pit" ] ],
+        qs     => {
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            keep_alive         => "string",
+            preference         => "string",
+            routing            => "string",
+        },
+    },
+
+    'ping' => {
+        doc    => "index",
+        method => "HEAD",
+        parts  => {},
+        paths  => [ [ {} ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'put_script' => {
+        body   => { required => 1 },
+        doc    => "modules-scripting",
+        method => "PUT",
+        parts  => { context => {}, id => {} },
+        paths  => [
+            [ { context => 2, id => 1 }, "_scripts", "{id}", "{context}" ],
+            [ { id => 1 }, "_scripts", "{id}" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'rank_eval' => {
+        body  => { required => 1 },
+        doc   => "search-rank-eval",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_rank_eval" ],
+            [ {}, "_rank_eval" ]
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            search_type        => "enum",
+        },
+    },
+
+    'reindex' => {
+        body   => { required => 1 },
+        doc    => "docs-reindex",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_reindex" ] ],
+        qs     => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            max_docs               => "number",
+            refresh                => "boolean",
+            requests_per_second    => "number",
+            scroll                 => "time",
+            slices                 => "number|string",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+            wait_for_completion    => "boolean",
+        },
+    },
+
+    'reindex_rethrottle' => {
+        doc    => "docs-reindex",
+        method => "POST",
+        parts  => { task_id => {} },
+        paths  =>
+            [ [ { task_id => 1 }, "_reindex", "{task_id}", "_rethrottle" ] ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            requests_per_second => "number",
+        },
+    },
+
+    'render_search_template' => {
+        body  => {},
+        doc   => "",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_render", "template", "{id}" ],
+            [ {}, "_render", "template" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'scripts_painless_execute' => {
+        body  => {},
+        doc   => "painless-execute-api",
+        parts => {},
+        paths => [ [ {}, "_scripts", "painless", "_execute" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'scroll' => {
+        body  => {},
+        doc   => "",
+        parts => { scroll_id => {} },
+        paths => [
+            [ { scroll_id => 2 }, "_search", "scroll", "{scroll_id}" ],
+            [ {}, "_search", "scroll" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            rest_total_hits_as_int => "boolean",
+            scroll                 => "time",
+        },
+    },
+
+    'search' => {
+        body  => {},
+        doc   => "search-search",
+        parts => { index => { multi => 1 }, type => { multi => 1 } },
+        paths => [
+            [ { index => 0, type => 1 }, "{index}", "{type}", "_search" ],
+            [ { index => 0 }, "{index}", "_search" ],
+            [ {}, "_search" ],
+        ],
+        qs => {
+            _source                       => "list",
+            _source_excludes              => "list",
+            _source_includes              => "list",
+            allow_no_indices              => "boolean",
+            allow_partial_search_results  => "boolean",
+            analyze_wildcard              => "boolean",
+            analyzer                      => "string",
+            batched_reduce_size           => "number",
+            ccs_minimize_roundtrips       => "boolean",
+            default_operator              => "enum",
+            df                            => "string",
+            docvalue_fields               => "list",
+            error_trace                   => "boolean",
+            expand_wildcards              => "enum",
+            explain                       => "boolean",
+            filter_path                   => "list",
+            from                          => "number",
+            human                         => "boolean",
+            ignore_throttled              => "boolean",
+            ignore_unavailable            => "boolean",
+            lenient                       => "boolean",
+            max_concurrent_shard_requests => "number",
+            pre_filter_shard_size         => "number",
+            preference                    => "string",
+            q                             => "string",
+            request_cache                 => "boolean",
+            rest_total_hits_as_int        => "boolean",
+            routing                       => "list",
+            scroll                        => "time",
+            search_type                   => "enum",
+            seq_no_primary_term           => "boolean",
+            size                          => "number",
+            sort                          => "list",
+            stats                         => "list",
+            stored_fields                 => "list",
+            suggest_field                 => "string",
+            suggest_mode                  => "enum",
+            suggest_size                  => "number",
+            suggest_text                  => "string",
+            terminate_after               => "number",
+            timeout                       => "time",
+            track_scores                  => "boolean",
+            track_total_hits              => "boolean",
+            typed_keys                    => "boolean",
+            version                       => "boolean",
+        },
+    },
+
+    'search_shards' => {
+        doc   => "search-shards",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_search_shards" ],
+            [ {}, "_search_shards" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            local              => "boolean",
+            preference         => "string",
+            routing            => "string",
+        },
+    },
+
+    'search_template' => {
+        body  => { required => 1 },
+        doc   => "search-template",
+        parts => { index => { multi => 1 }, type => { multi => 1 } },
+        paths => [
+            [   { index => 0, type => 1 }, "{index}",
+                "{type}",                  "_search",
+                "template",
+            ],
+            [ { index => 0 }, "{index}", "_search", "template" ],
+            [ {}, "_search", "template" ],
+        ],
+        qs => {
+            allow_no_indices        => "boolean",
+            ccs_minimize_roundtrips => "boolean",
+            error_trace             => "boolean",
+            expand_wildcards        => "enum",
+            explain                 => "boolean",
+            filter_path             => "list",
+            human                   => "boolean",
+            ignore_throttled        => "boolean",
+            ignore_unavailable      => "boolean",
+            preference              => "string",
+            profile                 => "boolean",
+            rest_total_hits_as_int  => "boolean",
+            routing                 => "list",
+            scroll                  => "time",
+            search_type             => "enum",
+            typed_keys              => "boolean",
+        },
+    },
+
+    'termvectors' => {
+        body  => {},
+        doc   => "docs-termvectors",
+        parts => { id => {}, index => { required => 1 }, type => {} },
+        paths => [
+            [   { id => 2, index => 0, type => 1 }, "{index}",
+                "{type}",                           "{id}",
+                "_termvectors",
+            ],
+            [ { id => 2, index => 0 }, "{index}", "_termvectors", "{id}" ],
+            [   { index => 0, type => 1 }, "{index}", "{type}",
+                "_termvectors"
+            ],
+            [ { index => 0 }, "{index}", "_termvectors" ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            field_statistics => "boolean",
+            fields           => "list",
+            filter_path      => "list",
+            human            => "boolean",
+            offsets          => "boolean",
+            payloads         => "boolean",
+            positions        => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            routing          => "string",
+            term_statistics  => "boolean",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'update' => {
+        body   => { required => 1 },
+        doc    => "docs-update",
+        method => "POST",
+        parts  => { id => {}, index => {}, type => {} },
+        paths  => [
+            [   { id => 2, index => 0, type => 1 },
+                "{index}", "{type}", "{id}", "_update",
+            ],
+            [ { id => 2, index => 0 }, "{index}", "_update", "{id}" ],
+        ],
+        qs => {
+            _source                => "list",
+            _source_excludes       => "list",
+            _source_includes       => "list",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            if_primary_term        => "number",
+            if_seq_no              => "number",
+            lang                   => "string",
+            refresh                => "enum",
+            require_alias          => "boolean",
+            retry_on_conflict      => "number",
+            routing                => "string",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'update_by_query' => {
+        body   => {},
+        doc    => "docs-update-by-query",
+        method => "POST",
+        parts  => {
+            index => { multi => 1, required => 1 },
+            type  => { multi => 1 }
+        },
+        paths => [
+            [   { index => 0, type => 1 }, "{index}",
+                "{type}",                  "_update_by_query",
+            ],
+            [ { index => 0 }, "{index}", "_update_by_query" ],
+        ],
+        qs => {
+            _source                => "list",
+            _source_excludes       => "list",
+            _source_includes       => "list",
+            allow_no_indices       => "boolean",
+            analyze_wildcard       => "boolean",
+            analyzer               => "string",
+            conflicts              => "enum",
+            default_operator       => "enum",
+            df                     => "string",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            from                   => "number",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            lenient                => "boolean",
+            max_docs               => "number",
+            pipeline               => "string",
+            preference             => "string",
+            q                      => "string",
+            refresh                => "boolean",
+            request_cache          => "boolean",
+            requests_per_second    => "number",
+            routing                => "list",
+            scroll                 => "time",
+            scroll_size            => "number",
+            search_timeout         => "time",
+            search_type            => "enum",
+            size                   => "number",
+            slices                 => "number|string",
+            sort                   => "list",
+            stats                  => "list",
+            terminate_after        => "number",
+            timeout                => "time",
+            version                => "boolean",
+            version_type           => "boolean",
+            wait_for_active_shards => "string",
+            wait_for_completion    => "boolean",
+        },
+    },
+
+    'update_by_query_rethrottle' => {
+        doc    => "docs-update-by-query",
+        method => "POST",
+        parts  => { task_id => {} },
+        paths  => [
+            [   { task_id => 1 }, "_update_by_query",
+                "{task_id}",      "_rethrottle",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            requests_per_second => "number",
+        },
+    },
+
+    'async_search.delete' => {
+        doc    => "async-search",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 1 }, "_async_search", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'async_search.get' => {
+        doc   => "async-search",
+        parts => { id => {} },
+        paths => [ [ { id => 1 }, "_async_search", "{id}" ] ],
+        qs    => {
+            error_trace                 => "boolean",
+            filter_path                 => "list",
+            human                       => "boolean",
+            keep_alive                  => "time",
+            typed_keys                  => "boolean",
+            wait_for_completion_timeout => "time",
+        },
+    },
+
+    'async_search.status' => {
+        doc   => "async-search",
+        parts => { id => {} },
+        paths => [ [ { id => 2 }, "_async_search", "status", "{id}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'async_search.submit' => {
+        body   => {},
+        doc    => "async-search",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [ { index => 0 }, "{index}", "_async_search" ],
+            [ {}, "_async_search" ],
+        ],
+        qs => {
+            _source                       => "list",
+            _source_excludes              => "list",
+            _source_includes              => "list",
+            allow_no_indices              => "boolean",
+            allow_partial_search_results  => "boolean",
+            analyze_wildcard              => "boolean",
+            analyzer                      => "string",
+            batched_reduce_size           => "number",
+            default_operator              => "enum",
+            df                            => "string",
+            docvalue_fields               => "list",
+            error_trace                   => "boolean",
+            expand_wildcards              => "enum",
+            explain                       => "boolean",
+            filter_path                   => "list",
+            from                          => "number",
+            human                         => "boolean",
+            ignore_throttled              => "boolean",
+            ignore_unavailable            => "boolean",
+            keep_alive                    => "time",
+            keep_on_completion            => "boolean",
+            lenient                       => "boolean",
+            max_concurrent_shard_requests => "number",
+            preference                    => "string",
+            q                             => "string",
+            request_cache                 => "boolean",
+            routing                       => "list",
+            search_type                   => "enum",
+            seq_no_primary_term           => "boolean",
+            size                          => "number",
+            sort                          => "list",
+            stats                         => "list",
+            stored_fields                 => "list",
+            suggest_field                 => "string",
+            suggest_mode                  => "enum",
+            suggest_size                  => "number",
+            suggest_text                  => "string",
+            terminate_after               => "number",
+            timeout                       => "time",
+            track_scores                  => "boolean",
+            track_total_hits              => "boolean",
+            typed_keys                    => "boolean",
+            version                       => "boolean",
+            wait_for_completion_timeout   => "time",
+        },
+    },
+
+    'autoscaling.delete_autoscaling_policy' => {
+        doc    => "autoscaling-delete-autoscaling-policy",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_autoscaling", "policy", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'autoscaling.get_autoscaling_capacity' => {
+        doc   => "autoscaling-get-autoscaling-capacity",
+        parts => {},
+        paths => [ [ {}, "_autoscaling", "capacity" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'autoscaling.get_autoscaling_policy' => {
+        doc   => "autoscaling-get-autoscaling-policy",
+        parts => { name => {} },
+        paths => [ [ { name => 2 }, "_autoscaling", "policy", "{name}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'autoscaling.put_autoscaling_policy' => {
+        body   => { required => 1 },
+        doc    => "autoscaling-put-autoscaling-policy",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_autoscaling", "policy", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'cat.aliases' => {
+        doc   => "cat-alias",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 2 }, "_cat", "aliases", "{name}" ],
+            [ {}, "_cat", "aliases" ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            expand_wildcards => "enum",
+            filter_path      => "list",
+            format           => "string",
+            h                => "list",
+            help             => "boolean",
+            human            => "boolean",
+            local            => "boolean",
+            s                => "list",
+            v                => "boolean",
+        },
+    },
+
+    'cat.allocation' => {
+        doc   => "cat-allocation",
+        parts => { node_id => { multi => 1 } },
+        paths => [
+            [ { node_id => 2 }, "_cat", "allocation", "{node_id}" ],
+            [ {}, "_cat", "allocation" ],
+        ],
+        qs => {
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.count' => {
+        doc   => "cat-count",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "count", "{index}" ],
+            [ {}, "_cat", "count" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            v           => "boolean",
+        },
+    },
+
+    'cat.fielddata' => {
+        doc   => "cat-fielddata",
+        parts => { fields => { multi => 1 } },
+        paths => [
+            [ { fields => 2 }, "_cat", "fielddata", "{fields}" ],
+            [ {}, "_cat", "fielddata" ],
+        ],
+        qs => {
+            bytes       => "enum",
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            v           => "boolean",
+        },
+    },
+
+    'cat.health' => {
+        doc   => "cat-health",
+        parts => {},
+        paths => [ [ {}, "_cat", "health" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            time        => "enum",
+            ts          => "boolean",
+            v           => "boolean",
+        },
+    },
+
+    'cat.help' => {
+        doc   => "cat",
+        parts => {},
+        paths => [ [ {}, "_cat" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+        },
+    },
+
+    'cat.indices' => {
+        doc   => "cat-indices",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "indices", "{index}" ],
+            [ {}, "_cat", "indices" ],
+        ],
+        qs => {
+            bytes                     => "enum",
+            error_trace               => "boolean",
+            expand_wildcards          => "enum",
+            filter_path               => "list",
+            format                    => "string",
+            h                         => "list",
+            health                    => "enum",
+            help                      => "boolean",
+            human                     => "boolean",
+            include_unloaded_segments => "boolean",
+            local                     => "boolean",
+            master_timeout            => "time",
+            pri                       => "boolean",
+            s                         => "list",
+            time                      => "enum",
+            v                         => "boolean",
+        },
+    },
+
+    'cat.master' => {
+        doc   => "cat-master",
+        parts => {},
+        paths => [ [ {}, "_cat", "master" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.ml_data_frame_analytics' => {
+        doc   => "cat-dfanalytics",
+        parts => { id => {} },
+        paths => [
+            [ { id => 4 }, "_cat", "ml", "data_frame", "analytics", "{id}" ],
+            [ {}, "_cat", "ml", "data_frame", "analytics" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.ml_datafeeds' => {
+        doc   => "cat-datafeeds",
+        parts => { datafeed_id => {} },
+        paths => [
+            [   { datafeed_id => 3 }, "_cat",
+                "ml",                 "datafeeds",
+                "{datafeed_id}"
+            ],
+            [ {}, "_cat", "ml", "datafeeds" ],
+        ],
+        qs => {
+            allow_no_datafeeds => "boolean",
+            allow_no_match     => "boolean",
+            error_trace        => "boolean",
+            filter_path        => "list",
+            format             => "string",
+            h                  => "list",
+            help               => "boolean",
+            human              => "boolean",
+            s                  => "list",
+            time               => "enum",
+            v                  => "boolean",
+        },
+    },
+
+    'cat.ml_jobs' => {
+        doc   => "cat-anomaly-detectors",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 3 }, "_cat", "ml", "anomaly_detectors",
+                "{job_id}"
+            ],
+            [ {}, "_cat", "ml", "anomaly_detectors" ],
+        ],
+        qs => {
+            allow_no_jobs  => "boolean",
+            allow_no_match => "boolean",
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.ml_trained_models' => {
+        doc   => "cat-trained-model",
+        parts => { model_id => {} },
+        paths => [
+            [   { model_id => 3 }, "_cat",
+                "ml",              "trained_models",
+                "{model_id}"
+            ],
+            [ {}, "_cat", "ml", "trained_models" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            from           => "int",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            s              => "list",
+            size           => "int",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.nodeattrs' => {
+        doc   => "cat-nodeattrs",
+        parts => {},
+        paths => [ [ {}, "_cat", "nodeattrs" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.nodes' => {
+        doc   => "cat-nodes",
+        parts => {},
+        paths => [ [ {}, "_cat", "nodes" ] ],
+        qs    => {
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            full_id        => "boolean",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.pending_tasks' => {
+        doc   => "cat-pending-tasks",
+        parts => {},
+        paths => [ [ {}, "_cat", "pending_tasks" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.plugins' => {
+        doc   => "cat-plugins",
+        parts => {},
+        paths => [ [ {}, "_cat", "plugins" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.recovery' => {
+        doc   => "cat-recovery",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "recovery", "{index}" ],
+            [ {}, "_cat", "recovery" ],
+        ],
+        qs => {
+            active_only => "boolean",
+            bytes       => "enum",
+            detailed    => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            time        => "enum",
+            v           => "boolean",
+        },
+    },
+
+    'cat.repositories' => {
+        doc   => "cat-repositories",
+        parts => {},
+        paths => [ [ {}, "_cat", "repositories" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.segments' => {
+        doc   => "cat-segments",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "segments", "{index}" ],
+            [ {}, "_cat", "segments" ],
+        ],
+        qs => {
+            bytes       => "enum",
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            v           => "boolean",
+        },
+    },
+
+    'cat.shards' => {
+        doc   => "cat-shards",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "shards", "{index}" ],
+            [ {}, "_cat", "shards" ],
+        ],
+        qs => {
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.snapshots' => {
+        doc   => "cat-snapshots",
+        parts => { repository => { multi => 1 } },
+        paths => [
+            [ { repository => 2 }, "_cat", "snapshots", "{repository}" ],
+            [ {}, "_cat", "snapshots" ],
+        ],
+        qs => {
+            error_trace        => "boolean",
+            filter_path        => "list",
+            format             => "string",
+            h                  => "list",
+            help               => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            s                  => "list",
+            time               => "enum",
+            v                  => "boolean",
+        },
+    },
+
+    'cat.tasks' => {
+        doc   => "tasks",
+        parts => {},
+        paths => [ [ {}, "_cat", "tasks" ] ],
+        qs    => {
+            actions        => "list",
+            detailed       => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            nodes          => "list",
+            parent_task_id => "string",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.templates' => {
+        doc   => "cat-templates",
+        parts => { name => {} },
+        paths => [
+            [ { name => 2 }, "_cat", "templates", "{name}" ],
+            [ {}, "_cat", "templates" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.thread_pool' => {
+        doc   => "cat-thread-pool",
+        parts => { thread_pool_patterns => { multi => 1 } },
+        paths => [
+            [   { thread_pool_patterns => 2 }, "_cat",
+                "thread_pool",                 "{thread_pool_patterns}",
+            ],
+            [ {}, "_cat", "thread_pool" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            size           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.transforms' => {
+        doc   => "cat-transforms",
+        parts => { transform_id => {} },
+        paths => [
+            [ { transform_id => 2 }, "_cat", "transforms", "{transform_id}" ],
+            [ {}, "_cat", "transforms" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            from           => "int",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            s              => "list",
+            size           => "int",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'ccr.delete_auto_follow_pattern' => {
+        doc    => "ccr-delete-auto-follow-pattern",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_ccr", "auto_follow", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.follow' => {
+        body   => { required => 1 },
+        doc    => "ccr-put-follow",
+        method => "PUT",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ccr", "follow" ] ],
+        qs     => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'ccr.follow_info' => {
+        doc   => "ccr-get-follow-info",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}", "_ccr", "info" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.follow_stats' => {
+        doc   => "ccr-get-follow-stats",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}", "_ccr", "stats" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.forget_follower' => {
+        body   => { required => 1 },
+        doc    => "ccr-post-forget-follower",
+        method => "POST",
+        parts  => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_ccr", "forget_follower" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.get_auto_follow_pattern' => {
+        doc   => "ccr-get-auto-follow-pattern",
+        parts => { name => {} },
+        paths => [
+            [ { name => 2 }, "_ccr", "auto_follow", "{name}" ],
+            [ {}, "_ccr", "auto_follow" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.pause_auto_follow_pattern' => {
+        doc    => "ccr-pause-auto-follow-pattern",
+        method => "POST",
+        parts  => { name => {} },
+        paths  =>
+            [ [ { name => 2 }, "_ccr", "auto_follow", "{name}", "pause" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.pause_follow' => {
+        doc    => "ccr-post-pause-follow",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ccr", "pause_follow" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.put_auto_follow_pattern' => {
+        body   => { required => 1 },
+        doc    => "ccr-put-auto-follow-pattern",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_ccr", "auto_follow", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.resume_auto_follow_pattern' => {
+        doc    => "ccr-resume-auto-follow-pattern",
+        method => "POST",
+        parts  => { name => {} },
+        paths  =>
+            [ [ { name => 2 }, "_ccr", "auto_follow", "{name}", "resume" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.resume_follow' => {
+        body   => {},
+        doc    => "ccr-post-resume-follow",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ccr", "resume_follow" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.stats' => {
+        doc   => "ccr-get-stats",
+        parts => {},
+        paths => [ [ {}, "_ccr", "stats" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.unfollow' => {
+        doc    => "ccr-post-unfollow",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ccr", "unfollow" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'cluster.allocation_explain' => {
+        body  => {},
+        doc   => "cluster-allocation-explain",
+        parts => {},
+        paths => [ [ {}, "_cluster", "allocation", "explain" ] ],
+        qs    => {
+            error_trace           => "boolean",
+            filter_path           => "list",
+            human                 => "boolean",
+            include_disk_info     => "boolean",
+            include_yes_decisions => "boolean",
+        },
+    },
+
+    'cluster.delete_component_template' => {
+        doc    => "indices-component-template",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_component_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'cluster.delete_voting_config_exclusions' => {
+        doc    => "voting-config-exclusions",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_cluster", "voting_config_exclusions" ] ],
+        qs     => {
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            wait_for_removal => "boolean",
+        },
+    },
+
+    'cluster.exists_component_template' => {
+        doc    => "indices-component-template",
+        method => "HEAD",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_component_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'cluster.get_component_template' => {
+        doc   => "indices-component-template",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 1 }, "_component_template", "{name}" ],
+            [ {}, "_component_template" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'cluster.get_settings' => {
+        doc   => "cluster-update-settings",
+        parts => {},
+        paths => [ [ {}, "_cluster", "settings" ] ],
+        qs    => {
+            error_trace      => "boolean",
+            filter_path      => "list",
+            flat_settings    => "boolean",
+            human            => "boolean",
+            include_defaults => "boolean",
+            master_timeout   => "time",
+            timeout          => "time",
+        },
+    },
+
+    'cluster.health' => {
+        doc   => "cluster-health",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cluster", "health", "{index}" ],
+            [ {}, "_cluster", "health" ],
+        ],
+        qs => {
+            error_trace                     => "boolean",
+            expand_wildcards                => "enum",
+            filter_path                     => "list",
+            human                           => "boolean",
+            level                           => "enum",
+            local                           => "boolean",
+            master_timeout                  => "time",
+            timeout                         => "time",
+            wait_for_active_shards          => "string",
+            wait_for_events                 => "enum",
+            wait_for_no_initializing_shards => "boolean",
+            wait_for_no_relocating_shards   => "boolean",
+            wait_for_nodes                  => "string",
+            wait_for_status                 => "enum",
+        },
+    },
+
+    'cluster.pending_tasks' => {
+        doc   => "cluster-pending",
+        parts => {},
+        paths => [ [ {}, "_cluster", "pending_tasks" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'cluster.post_voting_config_exclusions' => {
+        doc    => "voting-config-exclusions",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_cluster", "voting_config_exclusions" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            node_ids    => "string",
+            node_names  => "string",
+            timeout     => "time",
+        },
+    },
+
+    'cluster.put_component_template' => {
+        body   => { required => 1 },
+        doc    => "indices-component-template",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_component_template", "{name}" ] ],
+        qs     => {
+            create         => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'cluster.put_settings' => {
+        body   => { required => 1 },
+        doc    => "cluster-update-settings",
+        method => "PUT",
+        parts  => {},
+        paths  => [ [ {}, "_cluster", "settings" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            flat_settings  => "boolean",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'cluster.remote_info' => {
+        doc   => "cluster-remote-info",
+        parts => {},
+        paths => [ [ {}, "_remote", "info" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'cluster.reroute' => {
+        body   => {},
+        doc    => "cluster-reroute",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_cluster", "reroute" ] ],
+        qs     => {
+            dry_run        => "boolean",
+            error_trace    => "boolean",
+            explain        => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            metric         => "list",
+            retry_failed   => "boolean",
+            timeout        => "time",
+        },
+    },
+
+    'cluster.state' => {
+        doc   => "cluster-state",
+        parts => { index => { multi => 1 }, metric => { multi => 1 } },
+        paths => [
+            [   { index => 3, metric => 2 }, "_cluster",
+                "state",                     "{metric}",
+                "{index}",
+            ],
+            [ { metric => 2 }, "_cluster", "state", "{metric}" ],
+            [ {}, "_cluster", "state" ],
+        ],
+        qs => {
+            allow_no_indices          => "boolean",
+            error_trace               => "boolean",
+            expand_wildcards          => "enum",
+            filter_path               => "list",
+            flat_settings             => "boolean",
+            human                     => "boolean",
+            ignore_unavailable        => "boolean",
+            local                     => "boolean",
+            master_timeout            => "time",
+            wait_for_metadata_version => "number",
+            wait_for_timeout          => "time",
+        },
+    },
+
+    'cluster.stats' => {
+        doc   => "cluster-stats",
+        parts => { node_id => { multi => 1 } },
+        paths => [
+            [ { node_id => 3 }, "_cluster", "stats", "nodes", "{node_id}" ],
+            [ {}, "_cluster", "stats" ],
+        ],
+        qs => {
+            error_trace   => "boolean",
+            filter_path   => "list",
+            flat_settings => "boolean",
+            human         => "boolean",
+            timeout       => "time",
+        },
+    },
+
+    'dangling_indices.delete_dangling_index' => {
+        doc    => "modules-gateway-dangling-indices",
+        method => "DELETE",
+        parts  => { index_uuid => {} },
+        paths  => [ [ { index_uuid => 1 }, "_dangling", "{index_uuid}" ] ],
+        qs     => {
+            accept_data_loss => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            master_timeout   => "time",
+            timeout          => "time",
+        },
+    },
+
+    'dangling_indices.import_dangling_index' => {
+        doc    => "modules-gateway-dangling-indices",
+        method => "POST",
+        parts  => { index_uuid => {} },
+        paths  => [ [ { index_uuid => 1 }, "_dangling", "{index_uuid}" ] ],
+        qs     => {
+            accept_data_loss => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            master_timeout   => "time",
+            timeout          => "time",
+        },
+    },
+
+    'dangling_indices.list_dangling_indices' => {
+        doc   => "modules-gateway-dangling-indices",
+        parts => {},
+        paths => [ [ {}, "_dangling" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'data_frame_transform_deprecated.delete_transform' => {
+        doc    => "delete-transform",
+        method => "DELETE",
+        parts  => { transform_id => {} },
+        paths  => [
+            [   { transform_id => 2 }, "_data_frame",
+                "transforms",          "{transform_id}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            force       => "boolean",
+            human       => "boolean",
+        },
+    },
+
+    'data_frame_transform_deprecated.get_transform' => {
+        doc   => "get-transform",
+        parts => { transform_id => {} },
+        paths => [
+            [   { transform_id => 2 }, "_data_frame",
+                "transforms",          "{transform_id}",
+            ],
+            [ {}, "_data_frame", "transforms" ],
+        ],
+        qs => {
+            allow_no_match    => "boolean",
+            error_trace       => "boolean",
+            exclude_generated => "boolean",
+            filter_path       => "list",
+            from              => "int",
+            human             => "boolean",
+            size              => "int",
+        },
+    },
+
+    'data_frame_transform_deprecated.get_transform_stats' => {
+        doc   => "get-transform-stats",
+        parts => { transform_id => {} },
+        paths => [
+            [   { transform_id => 2 }, "_data_frame",
+                "transforms",          "{transform_id}",
+                "_stats",
+            ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            from           => "number",
+            human          => "boolean",
+            size           => "number",
+        },
+    },
+
+    'data_frame_transform_deprecated.preview_transform' => {
+        body   => { required => 1 },
+        doc    => "preview-transform",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_data_frame", "transforms", "_preview" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'data_frame_transform_deprecated.put_transform' => {
+        body   => { required => 1 },
+        doc    => "put-transform",
+        method => "PUT",
+        parts  => { transform_id => {} },
+        paths  => [
+            [   { transform_id => 2 }, "_data_frame",
+                "transforms",          "{transform_id}",
+            ],
+        ],
+        qs => {
+            defer_validation => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'data_frame_transform_deprecated.start_transform' => {
+        doc    => "start-transform",
+        method => "POST",
+        parts  => { transform_id => {} },
+        paths  => [
+            [   { transform_id => 2 }, "_data_frame",
+                "transforms",          "{transform_id}",
+                "_start",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'data_frame_transform_deprecated.stop_transform' => {
+        doc    => "stop-transform",
+        method => "POST",
+        parts  => { transform_id => {} },
+        paths  => [
+            [   { transform_id => 2 }, "_data_frame",
+                "transforms",          "{transform_id}",
+                "_stop",
+            ],
+        ],
+        qs => {
+            allow_no_match      => "boolean",
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            timeout             => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'data_frame_transform_deprecated.update_transform' => {
+        body   => { required => 1 },
+        doc    => "update-transform",
+        method => "POST",
+        parts  => { transform_id => { required => 1 } },
+        paths  => [
+            [   { transform_id => 2 }, "_data_frame",
+                "transforms",          "{transform_id}",
+                "_update",
+            ],
+        ],
+        qs => {
+            defer_validation => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'enrich.delete_policy' => {
+        doc    => "delete-enrich-policy-api",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_enrich", "policy", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'enrich.execute_policy' => {
+        doc    => "execute-enrich-policy-api",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  =>
+            [ [ { name => 2 }, "_enrich", "policy", "{name}", "_execute" ] ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'enrich.get_policy' => {
+        doc   => "get-enrich-policy-api",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 2 }, "_enrich", "policy", "{name}" ],
+            [ {}, "_enrich", "policy" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'enrich.put_policy' => {
+        body   => { required => 1 },
+        doc    => "put-enrich-policy-api",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_enrich", "policy", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'enrich.stats' => {
+        doc   => "enrich-stats-api",
+        parts => {},
+        paths => [ [ {}, "_enrich", "_stats" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'eql.delete' => {
+        doc    => "eql-search-api",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_eql", "search", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'eql.get' => {
+        doc   => "eql-search-api",
+        parts => { id => {} },
+        paths => [ [ { id => 2 }, "_eql", "search", "{id}" ] ],
+        qs    => {
+            error_trace                 => "boolean",
+            filter_path                 => "list",
+            human                       => "boolean",
+            keep_alive                  => "time",
+            wait_for_completion_timeout => "time",
+        },
+    },
+
+    'eql.search' => {
+        body  => { required => 1 },
+        doc   => "eql-search-api",
+        parts => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_eql", "search" ] ],
+        qs    => {
+            error_trace                 => "boolean",
+            filter_path                 => "list",
+            human                       => "boolean",
+            keep_alive                  => "time",
+            keep_on_completion          => "boolean",
+            wait_for_completion_timeout => "time",
+        },
+    },
+
+    'graph.explore' => {
+        body  => {},
+        doc   => "graph-explore-api",
+        parts => { index => { multi => 1 }, type => { multi => 1 } },
+        paths => [
+            [   { index => 0, type => 1 }, "{index}",
+                "{type}",                  "_graph",
+                "explore",
+            ],
+            [ { index => 0 }, "{index}", "_graph", "explore" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            routing     => "string",
+            timeout     => "time",
+        },
+    },
+
+    'ilm.delete_lifecycle' => {
+        doc    => "ilm-delete-lifecycle",
+        method => "DELETE",
+        parts  => { policy => {} },
+        paths  => [ [ { policy => 2 }, "_ilm", "policy", "{policy}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.explain_lifecycle' => {
+        doc   => "ilm-explain-lifecycle",
+        parts => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_ilm", "explain" ] ],
+        qs    => {
+            error_trace  => "boolean",
+            filter_path  => "list",
+            human        => "boolean",
+            only_errors  => "boolean",
+            only_managed => "boolean",
+        },
+    },
+
+    'ilm.get_lifecycle' => {
+        doc   => "ilm-get-lifecycle",
+        parts => { policy => {} },
+        paths => [
+            [ { policy => 2 }, "_ilm", "policy", "{policy}" ],
+            [ {}, "_ilm", "policy" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.get_status' => {
+        doc   => "ilm-get-status",
+        parts => {},
+        paths => [ [ {}, "_ilm", "status" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.move_to_step' => {
+        body   => {},
+        doc    => "ilm-move-to-step",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 2 }, "_ilm", "move", "{index}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.put_lifecycle' => {
+        body   => {},
+        doc    => "ilm-put-lifecycle",
+        method => "PUT",
+        parts  => { policy => {} },
+        paths  => [ [ { policy => 2 }, "_ilm", "policy", "{policy}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.remove_policy' => {
+        doc    => "ilm-remove-policy",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ilm", "remove" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.retry' => {
+        doc    => "ilm-retry-policy",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ilm", "retry" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.start' => {
+        doc    => "ilm-start",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ilm", "start" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.stop' => {
+        doc    => "ilm-stop",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ilm", "stop" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.add_block' => {
+        doc    => "index-modules-blocks",
+        method => "PUT",
+        parts  => { block => {}, index => { multi => 1 } },
+        paths  => [
+            [ { block => 2, index => 0 }, "{index}", "_block", "{block}" ]
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            timeout            => "time",
+        },
+    },
+
+    'indices.analyze' => {
+        body  => {},
+        doc   => "indices-analyze",
+        parts => { index => {} },
+        paths =>
+            [ [ { index => 0 }, "{index}", "_analyze" ], [ {}, "_analyze" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.clear_cache' => {
+        doc    => "indices-clearcache",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [ { index => 0 }, "{index}", "_cache", "clear" ],
+            [ {}, "_cache", "clear" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            fielddata          => "boolean",
+            fields             => "list",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            query              => "boolean",
+            request            => "boolean",
+        },
+    },
+
+    'indices.clone' => {
+        body   => {},
+        doc    => "indices-clone-index",
+        method => "PUT",
+        parts  => { index => {}, target => {} },
+        paths  => [
+            [ { index => 0, target => 2 }, "{index}", "_clone", "{target}" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.close' => {
+        doc    => "indices-open-close",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}", "_close" ] ],
+        qs     => {
+            allow_no_indices       => "boolean",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.create' => {
+        body   => {},
+        doc    => "indices-create-index",
+        method => "PUT",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}" ] ],
+        qs     => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            include_type_name      => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.create_data_stream' => {
+        doc    => "data-streams",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_data_stream", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.data_streams_stats' => {
+        doc   => "data-streams",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 1 }, "_data_stream", "{name}", "_stats" ],
+            [ {}, "_data_stream", "_stats" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.delete' => {
+        doc    => "indices-delete-index",
+        method => "DELETE",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}" ] ],
+        qs     => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            timeout            => "time",
+        },
+    },
+
+    'indices.delete_alias' => {
+        doc    => "indices-aliases",
+        method => "DELETE",
+        parts  => { index => { multi => 1 }, name => { multi => 1 } },
+        paths  =>
+            [ [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ] ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.delete_data_stream' => {
+        doc    => "data-streams",
+        method => "DELETE",
+        parts  => { name => { multi => 1 } },
+        paths  => [ [ { name => 1 }, "_data_stream", "{name}" ] ],
+        qs     => {
+            error_trace      => "boolean",
+            expand_wildcards => "enum",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'indices.delete_index_template' => {
+        doc    => "indices-templates",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_index_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.delete_template' => {
+        doc    => "indices-templates",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.exists' => {
+        doc    => "indices-exists",
+        method => "HEAD",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}" ] ],
+        qs     => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            flat_settings      => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_defaults   => "boolean",
+            local              => "boolean",
+        },
+    },
+
+    'indices.exists_alias' => {
+        doc    => "indices-aliases",
+        method => "HEAD",
+        parts  => { index => { multi => 1 }, name => { multi => 1 } },
+        paths  => [
+            [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ],
+            [ { name  => 1 }, "_alias", "{name}" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            local              => "boolean",
+        },
+    },
+
+    'indices.exists_index_template' => {
+        doc    => "indices-templates",
+        method => "HEAD",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_index_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            flat_settings  => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.exists_template' => {
+        doc    => "indices-templates",
+        method => "HEAD",
+        parts  => { name => { multi => 1 } },
+        paths  => [ [ { name => 1 }, "_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            flat_settings  => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.exists_type' => {
+        doc    => "indices-types-exists",
+        method => "HEAD",
+        parts  => { index => { multi => 1 }, type => { multi => 1 } },
+        paths  => [
+            [ { index => 0, type => 2 }, "{index}", "_mapping", "{type}" ]
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            local              => "boolean",
+        },
+    },
+
+    'indices.flush' => {
+        doc    => "indices-flush",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  =>
+            [ [ { index => 0 }, "{index}", "_flush" ], [ {}, "_flush" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            force              => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            wait_if_ongoing    => "boolean",
+        },
+    },
+
+    'indices.flush_synced' => {
+        doc    => "indices-synced-flush-api",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [ { index => 0 }, "{index}", "_flush", "synced" ],
+            [ {}, "_flush", "synced" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'indices.forcemerge' => {
+        doc    => "indices-forcemerge",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [ { index => 0 }, "{index}", "_forcemerge" ],
+            [ {}, "_forcemerge" ],
+        ],
+        qs => {
+            allow_no_indices     => "boolean",
+            error_trace          => "boolean",
+            expand_wildcards     => "enum",
+            filter_path          => "list",
+            flush                => "boolean",
+            human                => "boolean",
+            ignore_unavailable   => "boolean",
+            max_num_segments     => "number",
+            only_expunge_deletes => "boolean",
+        },
+    },
+
+    'indices.freeze' => {
+        doc    => "freeze-index-api",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_freeze" ] ],
+        qs     => {
+            allow_no_indices       => "boolean",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.get' => {
+        doc   => "indices-get-index",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}" ] ],
+        qs    => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            flat_settings      => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_defaults   => "boolean",
+            include_type_name  => "boolean",
+            local              => "boolean",
+            master_timeout     => "time",
+        },
+    },
+
+    'indices.get_alias' => {
+        doc   => "indices-aliases",
+        parts => { index => { multi => 1 }, name => { multi => 1 } },
+        paths => [
+            [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ],
+            [ { index => 0 }, "{index}", "_alias" ],
+            [ { name  => 1 }, "_alias",  "{name}" ],
+            [ {}, "_alias" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            local              => "boolean",
+        },
+    },
+
+    'indices.get_data_stream' => {
+        doc   => "data-streams",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 1 }, "_data_stream", "{name}" ],
+            [ {}, "_data_stream" ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            expand_wildcards => "enum",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'indices.get_field_mapping' => {
+        doc   => "indices-get-field-mapping",
+        parts => {
+            fields => { multi => 1 },
+            index  => { multi => 1 },
+            type   => { multi => 1 },
+        },
+        paths => [
+            [   { fields => 4, index => 0, type => 2 }, "{index}",
+                "_mapping",                             "{type}",
+                "field",                                "{fields}",
+            ],
+            [   { fields => 3, index => 0 }, "{index}",
+                "_mapping",                  "field",
+                "{fields}",
+            ],
+            [   { fields => 3, type => 1 }, "_mapping",
+                "{type}",                   "field",
+                "{fields}",
+            ],
+            [ { fields => 2 }, "_mapping", "field", "{fields}" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_defaults   => "boolean",
+            include_type_name  => "boolean",
+            local              => "boolean",
+        },
+    },
+
+    'indices.get_index_template' => {
+        doc   => "indices-templates",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 1 }, "_index_template", "{name}" ],
+            [ {}, "_index_template" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            flat_settings  => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.get_mapping' => {
+        doc   => "indices-get-mapping",
+        parts => { index => { multi => 1 }, type => { multi => 1 } },
+        paths => [
+            [ { index => 0, type => 2 }, "{index}", "_mapping", "{type}" ],
+            [ { index => 0 }, "{index}",  "_mapping" ],
+            [ { type  => 1 }, "_mapping", "{type}" ],
+            [ {}, "_mapping" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_type_name  => "boolean",
+            local              => "boolean",
+            master_timeout     => "time",
+        },
+    },
+
+    'indices.get_settings' => {
+        doc   => "indices-get-settings",
+        parts => { index => { multi => 1 }, name => { multi => 1 } },
+        paths => [
+            [ { index => 0, name => 2 }, "{index}", "_settings", "{name}" ],
+            [ { index => 0 }, "{index}",   "_settings" ],
+            [ { name  => 1 }, "_settings", "{name}" ],
+            [ {}, "_settings" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            flat_settings      => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_defaults   => "boolean",
+            local              => "boolean",
+            master_timeout     => "time",
+        },
+    },
+
+    'indices.get_template' => {
+        doc   => "indices-templates",
+        parts => { name => { multi => 1 } },
+        paths =>
+            [ [ { name => 1 }, "_template", "{name}" ], [ {}, "_template" ] ],
+        qs => {
+            error_trace       => "boolean",
+            filter_path       => "list",
+            flat_settings     => "boolean",
+            human             => "boolean",
+            include_type_name => "boolean",
+            local             => "boolean",
+            master_timeout    => "time",
+        },
+    },
+
+    'indices.get_upgrade' => {
+        doc   => "indices-upgrade",
+        parts => { index => { multi => 1 } },
+        paths =>
+            [ [ { index => 0 }, "{index}", "_upgrade" ], [ {}, "_upgrade" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'indices.migrate_to_data_stream' => {
+        doc    => "data-streams",
+        method => "POST",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_data_stream", "_migrate", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.open' => {
+        doc    => "indices-open-close",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}", "_open" ] ],
+        qs     => {
+            allow_no_indices       => "boolean",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.promote_data_stream' => {
+        doc    => "data-streams",
+        method => "POST",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_data_stream", "_promote", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.put_alias' => {
+        body   => {},
+        doc    => "indices-aliases",
+        method => "PUT",
+        parts  => { index => { multi => 1 }, name => {} },
+        paths  =>
+            [ [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ] ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.put_index_template' => {
+        body   => { required => 1 },
+        doc    => "indices-templates",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_index_template", "{name}" ] ],
+        qs     => {
+            cause          => "string",
+            create         => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.put_mapping' => {
+        body   => { required => 1 },
+        doc    => "indices-put-mapping",
+        method => "PUT",
+        parts  => { index => { multi => 1 }, type => {} },
+        paths  => [
+            [ { index => 0, type => 2 }, "{index}", "_mapping", "{type}" ],
+            [ { index => 0 }, "{index}",  "_mapping" ],
+            [ { type  => 1 }, "_mapping", "{type}" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_type_name  => "boolean",
+            master_timeout     => "time",
+            timeout            => "time",
+            write_index_only   => "boolean",
+        },
+    },
+
+    'indices.put_settings' => {
+        body   => { required => 1 },
+        doc    => "indices-update-settings",
+        method => "PUT",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [ { index => 0 }, "{index}", "_settings" ],
+            [ {}, "_settings" ]
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            flat_settings      => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            preserve_existing  => "boolean",
+            timeout            => "time",
+        },
+    },
+
+    'indices.put_template' => {
+        body   => { required => 1 },
+        doc    => "indices-templates",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_template", "{name}" ] ],
+        qs     => {
+            create            => "boolean",
+            error_trace       => "boolean",
+            filter_path       => "list",
+            human             => "boolean",
+            include_type_name => "boolean",
+            master_timeout    => "time",
+            order             => "number",
+        },
+    },
+
+    'indices.recovery' => {
+        doc   => "indices-recovery",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_recovery" ],
+            [ {}, "_recovery" ]
+        ],
+        qs => {
+            active_only => "boolean",
+            detailed    => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'indices.refresh' => {
+        doc    => "indices-refresh",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  =>
+            [ [ { index => 0 }, "{index}", "_refresh" ], [ {}, "_refresh" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'indices.reload_search_analyzers' => {
+        doc   => "indices-reload-analyzers",
+        parts => { index => { multi => 1 } },
+        paths =>
+            [ [ { index => 0 }, "{index}", "_reload_search_analyzers" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'indices.resolve_index' => {
+        doc   => "indices-resolve-index-api",
+        parts => { name => { multi => 1 } },
+        paths => [ [ { name => 2 }, "_resolve", "index", "{name}" ] ],
+        qs    => {
+            error_trace      => "boolean",
+            expand_wildcards => "enum",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'indices.rollover' => {
+        body   => {},
+        doc    => "indices-rollover-index",
+        method => "POST",
+        parts  => { alias => {}, new_index => {} },
+        paths  => [
+            [   { alias => 0, new_index => 2 }, "{alias}",
+                "_rollover",                    "{new_index}",
+            ],
+            [ { alias => 0 }, "{alias}", "_rollover" ],
+        ],
+        qs => {
+            dry_run                => "boolean",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            include_type_name      => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.segments' => {
+        doc   => "indices-segments",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_segments" ],
+            [ {}, "_segments" ]
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            verbose            => "boolean",
+        },
+    },
+
+    'indices.shard_stores' => {
+        doc   => "indices-shards-stores",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_shard_stores" ],
+            [ {}, "_shard_stores" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            status             => "list",
+        },
+    },
+
+    'indices.shrink' => {
+        body   => {},
+        doc    => "indices-shrink-index",
+        method => "PUT",
+        parts  => { index => {}, target => {} },
+        paths  => [
+            [ { index => 0, target => 2 }, "{index}", "_shrink", "{target}" ],
+        ],
+        qs => {
+            copy_settings          => "boolean",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.simulate_index_template' => {
+        body   => {},
+        doc    => "indices-templates",
+        method => "POST",
+        parts  => { name => {} },
+        paths  => [
+            [ { name => 2 }, "_index_template", "_simulate_index", "{name}" ],
+        ],
+        qs => {
+            cause          => "string",
+            create         => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.simulate_template' => {
+        body   => {},
+        doc    => "indices-templates",
+        method => "POST",
+        parts  => { name => {} },
+        paths  => [
+            [ { name => 2 }, "_index_template", "_simulate", "{name}" ],
+            [ {}, "_index_template", "_simulate" ],
+        ],
+        qs => {
+            cause          => "string",
+            create         => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.split' => {
+        body   => {},
+        doc    => "indices-split-index",
+        method => "PUT",
+        parts  => { index => {}, target => {} },
+        paths  => [
+            [ { index => 0, target => 2 }, "{index}", "_split", "{target}" ],
+        ],
+        qs => {
+            copy_settings          => "boolean",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.stats' => {
+        doc   => "indices-stats",
+        parts => { index => { multi => 1 }, metric => { multi => 1 } },
+        paths => [
+            [ { index  => 0, metric => 2 }, "{index}", "_stats", "{metric}" ],
+            [ { index  => 0 }, "{index}", "_stats" ],
+            [ { metric => 1 }, "_stats",  "{metric}" ],
+            [ {}, "_stats" ],
+        ],
+        qs => {
+            completion_fields          => "list",
+            error_trace                => "boolean",
+            expand_wildcards           => "enum",
+            fielddata_fields           => "list",
+            fields                     => "list",
+            filter_path                => "list",
+            forbid_closed_indices      => "boolean",
+            groups                     => "list",
+            human                      => "boolean",
+            include_segment_file_sizes => "boolean",
+            include_unloaded_segments  => "boolean",
+            level                      => "enum",
+            types                      => "list",
+        },
+    },
+
+    'indices.unfreeze' => {
+        doc    => "unfreeze-index-api",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_unfreeze" ] ],
+        qs     => {
+            allow_no_indices       => "boolean",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.update_aliases' => {
+        body   => { required => 1 },
+        doc    => "indices-aliases",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_aliases" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.upgrade' => {
+        doc    => "indices-upgrade",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  =>
+            [ [ { index => 0 }, "{index}", "_upgrade" ], [ {}, "_upgrade" ] ],
+        qs => {
+            allow_no_indices      => "boolean",
+            error_trace           => "boolean",
+            expand_wildcards      => "enum",
+            filter_path           => "list",
+            human                 => "boolean",
+            ignore_unavailable    => "boolean",
+            only_ancient_segments => "boolean",
+            wait_for_completion   => "boolean",
+        },
+    },
+
+    'indices.validate_query' => {
+        body  => {},
+        doc   => "search-validate",
+        parts => { index => { multi => 1 }, type => { multi => 1 } },
+        paths => [
+            [   { index => 0, type => 1 }, "{index}",
+                "{type}",                  "_validate",
+                "query",
+            ],
+            [ { index => 0 }, "{index}", "_validate", "query" ],
+            [ {}, "_validate", "query" ],
+        ],
+        qs => {
+            all_shards         => "boolean",
+            allow_no_indices   => "boolean",
+            analyze_wildcard   => "boolean",
+            analyzer           => "string",
+            default_operator   => "enum",
+            df                 => "string",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            explain            => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            lenient            => "boolean",
+            q                  => "string",
+            rewrite            => "boolean",
+        },
+    },
+
+    'ingest.delete_pipeline' => {
+        doc    => "delete-pipeline-api",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_ingest", "pipeline", "{id}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'ingest.get_pipeline' => {
+        doc   => "get-pipeline-api",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_ingest", "pipeline", "{id}" ],
+            [ {}, "_ingest", "pipeline" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'ingest.processor_grok' => {
+        doc   => "",
+        parts => {},
+        paths => [ [ {}, "_ingest", "processor", "grok" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ingest.put_pipeline' => {
+        body   => { required => 1 },
+        doc    => "put-pipeline-api",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_ingest", "pipeline", "{id}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'ingest.simulate' => {
+        body  => { required => 1 },
+        doc   => "simulate-pipeline-api",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_ingest", "pipeline", "{id}", "_simulate" ],
+            [ {}, "_ingest", "pipeline", "_simulate" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            verbose     => "boolean",
+        },
+    },
+
+    'license.delete' => {
+        doc    => "delete-license",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_license" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'license.get' => {
+        doc   => "get-license",
+        parts => {},
+        paths => [ [ {}, "_license" ] ],
+        qs    => {
+            accept_enterprise => "boolean",
+            error_trace       => "boolean",
+            filter_path       => "list",
+            human             => "boolean",
+            local             => "boolean",
+        },
+    },
+
+    'license.get_basic_status' => {
+        doc   => "get-basic-status",
+        parts => {},
+        paths => [ [ {}, "_license", "basic_status" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'license.get_trial_status' => {
+        doc   => "get-trial-status",
+        parts => {},
+        paths => [ [ {}, "_license", "trial_status" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'license.post' => {
+        body   => {},
+        doc    => "update-license",
+        method => "PUT",
+        parts  => {},
+        paths  => [ [ {}, "_license" ] ],
+        qs     => {
+            acknowledge => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'license.post_start_basic' => {
+        doc    => "start-basic",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_license", "start_basic" ] ],
+        qs     => {
+            acknowledge => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'license.post_start_trial' => {
+        doc    => "start-trial",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_license", "start_trial" ] ],
+        qs     => {
+            acknowledge => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            type        => "string",
+        },
+    },
+
+    'migration.deprecations' => {
+        doc   => "migration-api-deprecation",
+        parts => { index => {} },
+        paths => [
+            [ { index => 0 }, "{index}", "_migration", "deprecations" ],
+            [ {}, "_migration", "deprecations" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.close_job' => {
+        body   => {},
+        doc    => "ml-close-job",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_close",
+            ],
+        ],
+        qs => {
+            allow_no_jobs  => "boolean",
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            force          => "boolean",
+            human          => "boolean",
+            timeout        => "time",
+        },
+    },
+
+    'ml.delete_calendar' => {
+        doc    => "ml-delete-calendar",
+        method => "DELETE",
+        parts  => { calendar_id => {} },
+        paths  =>
+            [ [ { calendar_id => 2 }, "_ml", "calendars", "{calendar_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_calendar_event' => {
+        doc    => "ml-delete-calendar-event",
+        method => "DELETE",
+        parts  => { calendar_id => {}, event_id => {} },
+        paths  => [
+            [   { calendar_id => 2, event_id => 4 }, "_ml",
+                "calendars",                         "{calendar_id}",
+                "events",                            "{event_id}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_calendar_job' => {
+        doc    => "ml-delete-calendar-job",
+        method => "DELETE",
+        parts  => { calendar_id => {}, job_id => {} },
+        paths  => [
+            [   { calendar_id => 2, job_id => 4 },
+                "_ml", "calendars", "{calendar_id}", "jobs", "{job_id}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_data_frame_analytics' => {
+        doc    => "delete-dfanalytics",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  =>
+            [ [ { id => 3 }, "_ml", "data_frame", "analytics", "{id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            force       => "boolean",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'ml.delete_datafeed' => {
+        doc    => "ml-delete-datafeed",
+        method => "DELETE",
+        parts  => { datafeed_id => {} },
+        paths  =>
+            [ [ { datafeed_id => 2 }, "_ml", "datafeeds", "{datafeed_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            force       => "boolean",
+            human       => "boolean",
+        },
+    },
+
+    'ml.delete_expired_data' => {
+        body   => {},
+        doc    => "ml-delete-expired-data",
+        method => "DELETE",
+        parts  => { job_id => {} },
+        paths  => [
+            [ { job_id => 2 }, "_ml", "_delete_expired_data", "{job_id}" ],
+            [ {}, "_ml", "_delete_expired_data" ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            requests_per_second => "number",
+            timeout             => "time",
+        },
+    },
+
+    'ml.delete_filter' => {
+        doc    => "ml-delete-filter",
+        method => "DELETE",
+        parts  => { filter_id => {} },
+        paths  => [ [ { filter_id => 2 }, "_ml", "filters", "{filter_id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_forecast' => {
+        doc    => "ml-delete-forecast",
+        method => "DELETE",
+        parts  => { forecast_id => {}, job_id => {} },
+        paths  => [
+            [   { forecast_id => 4, job_id => 2 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "_forecast",                       "{forecast_id}",
+            ],
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_forecast",
+            ],
+        ],
+        qs => {
+            allow_no_forecasts => "boolean",
+            error_trace        => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+            timeout            => "time",
+        },
+    },
+
+    'ml.delete_job' => {
+        doc    => "ml-delete-job",
+        method => "DELETE",
+        parts  => { job_id => {} },
+        paths  =>
+            [ [ { job_id => 2 }, "_ml", "anomaly_detectors", "{job_id}" ] ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            force               => "boolean",
+            human               => "boolean",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'ml.delete_model_snapshot' => {
+        doc    => "ml-delete-snapshot",
+        method => "DELETE",
+        parts  => { job_id => {}, snapshot_id => {} },
+        paths  => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_trained_model' => {
+        doc    => "delete-trained-models",
+        method => "DELETE",
+        parts  => { model_id => {} },
+        paths  =>
+            [ [ { model_id => 2 }, "_ml", "trained_models", "{model_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.estimate_model_memory' => {
+        body   => { required => 1 },
+        doc    => "ml-apis",
+        method => "POST",
+        parts  => {},
+        paths  =>
+            [ [ {}, "_ml", "anomaly_detectors", "_estimate_model_memory" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.evaluate_data_frame' => {
+        body   => { required => 1 },
+        doc    => "evaluate-dfanalytics",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ml", "data_frame", "_evaluate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.explain_data_frame_analytics' => {
+        body  => {},
+        doc   => "explain-dfanalytics",
+        parts => { id => {} },
+        paths => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics",
+                "{id}",      "_explain"
+            ],
+            [ {}, "_ml", "data_frame", "analytics", "_explain" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.find_file_structure' => {
+        body   => { required => 1 },
+        doc    => "ml-find-file-structure",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ml", "find_file_structure" ] ],
+        qs     => {
+            charset               => "string",
+            column_names          => "list",
+            delimiter             => "string",
+            error_trace           => "boolean",
+            explain               => "boolean",
+            filter_path           => "list",
+            format                => "enum",
+            grok_pattern          => "string",
+            has_header_row        => "boolean",
+            human                 => "boolean",
+            line_merge_size_limit => "int",
+            lines_to_sample       => "int",
+            quote                 => "string",
+            should_trim_fields    => "boolean",
+            timeout               => "time",
+            timestamp_field       => "string",
+            timestamp_format      => "string",
+        },
+        serialize => "bulk",
+    },
+
+    'ml.flush_job' => {
+        body   => {},
+        doc    => "ml-flush-job",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_flush",
+            ],
+        ],
+        qs => {
+            advance_time => "string",
+            calc_interim => "boolean",
+            end          => "string",
+            error_trace  => "boolean",
+            filter_path  => "list",
+            human        => "boolean",
+            skip_time    => "string",
+            start        => "string",
+        },
+    },
+
+    'ml.forecast' => {
+        doc    => "ml-forecast",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_forecast",
+            ],
+        ],
+        qs => {
+            duration         => "time",
+            error_trace      => "boolean",
+            expires_in       => "time",
+            filter_path      => "list",
+            human            => "boolean",
+            max_model_memory => "string",
+        },
+    },
+
+    'ml.get_buckets' => {
+        body  => {},
+        doc   => "ml-get-bucket",
+        parts => { job_id => {}, timestamp => {} },
+        paths => [
+            [   { job_id => 2, timestamp => 5 }, "_ml",
+                "anomaly_detectors",             "{job_id}",
+                "results",                       "buckets",
+                "{timestamp}",
+            ],
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "buckets",
+            ],
+        ],
+        qs => {
+            anomaly_score   => "double",
+            desc            => "boolean",
+            end             => "string",
+            error_trace     => "boolean",
+            exclude_interim => "boolean",
+            expand          => "boolean",
+            filter_path     => "list",
+            from            => "int",
+            human           => "boolean",
+            size            => "int",
+            sort            => "string",
+            start           => "string",
+        },
+    },
+
+    'ml.get_calendar_events' => {
+        doc   => "ml-get-calendar-event",
+        parts => { calendar_id => {} },
+        paths => [
+            [   { calendar_id => 2 }, "_ml",
+                "calendars",          "{calendar_id}",
+                "events",
+            ],
+        ],
+        qs => {
+            end         => "time",
+            error_trace => "boolean",
+            filter_path => "list",
+            from        => "int",
+            human       => "boolean",
+            job_id      => "string",
+            size        => "int",
+            start       => "string",
+        },
+    },
+
+    'ml.get_calendars' => {
+        body  => {},
+        doc   => "ml-get-calendar",
+        parts => { calendar_id => {} },
+        paths => [
+            [ { calendar_id => 2 }, "_ml", "calendars", "{calendar_id}" ],
+            [ {}, "_ml", "calendars" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            from        => "int",
+            human       => "boolean",
+            size        => "int",
+        },
+    },
+
+    'ml.get_categories' => {
+        body  => {},
+        doc   => "ml-get-category",
+        parts => { category_id => {}, job_id => {} },
+        paths => [
+            [   { category_id => 5, job_id => 2 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "results",                         "categories",
+                "{category_id}",
+            ],
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "categories",
+            ],
+        ],
+        qs => {
+            error_trace           => "boolean",
+            filter_path           => "list",
+            from                  => "int",
+            human                 => "boolean",
+            partition_field_value => "string",
+            size                  => "int",
+        },
+    },
+
+    'ml.get_data_frame_analytics' => {
+        doc   => "get-dfanalytics",
+        parts => { id => {} },
+        paths => [
+            [ { id => 3 }, "_ml", "data_frame", "analytics", "{id}" ],
+            [ {}, "_ml", "data_frame", "analytics" ],
+        ],
+        qs => {
+            allow_no_match    => "boolean",
+            error_trace       => "boolean",
+            exclude_generated => "boolean",
+            filter_path       => "list",
+            from              => "int",
+            human             => "boolean",
+            size              => "int",
+        },
+    },
+
+    'ml.get_data_frame_analytics_stats' => {
+        doc   => "get-dfanalytics-stats",
+        parts => { id => {} },
+        paths => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics",
+                "{id}",      "_stats"
+            ],
+            [ {}, "_ml", "data_frame", "analytics", "_stats" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            from           => "int",
+            human          => "boolean",
+            size           => "int",
+            verbose        => "boolean",
+        },
+    },
+
+    'ml.get_datafeed_stats' => {
+        doc   => "ml-get-datafeed-stats",
+        parts => { datafeed_id => {} },
+        paths => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_stats",
+            ],
+            [ {}, "_ml", "datafeeds", "_stats" ],
+        ],
+        qs => {
+            allow_no_datafeeds => "boolean",
+            allow_no_match     => "boolean",
+            error_trace        => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+        },
+    },
+
+    'ml.get_datafeeds' => {
+        doc   => "ml-get-datafeed",
+        parts => { datafeed_id => {} },
+        paths => [
+            [ { datafeed_id => 2 }, "_ml", "datafeeds", "{datafeed_id}" ],
+            [ {}, "_ml", "datafeeds" ],
+        ],
+        qs => {
+            allow_no_datafeeds => "boolean",
+            allow_no_match     => "boolean",
+            error_trace        => "boolean",
+            exclude_generated  => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+        },
+    },
+
+    'ml.get_filters' => {
+        doc   => "ml-get-filter",
+        parts => { filter_id => {} },
+        paths => [
+            [ { filter_id => 2 }, "_ml", "filters", "{filter_id}" ],
+            [ {}, "_ml", "filters" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            from        => "int",
+            human       => "boolean",
+            size        => "int",
+        },
+    },
+
+    'ml.get_influencers' => {
+        body  => {},
+        doc   => "ml-get-influencer",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "influencers",
+            ],
+        ],
+        qs => {
+            desc             => "boolean",
+            end              => "string",
+            error_trace      => "boolean",
+            exclude_interim  => "boolean",
+            filter_path      => "list",
+            from             => "int",
+            human            => "boolean",
+            influencer_score => "double",
+            size             => "int",
+            sort             => "string",
+            start            => "string",
+        },
+    },
+
+    'ml.get_job_stats' => {
+        doc   => "ml-get-job-stats",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_stats",
+            ],
+            [ {}, "_ml", "anomaly_detectors", "_stats" ],
+        ],
+        qs => {
+            allow_no_jobs  => "boolean",
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+        },
+    },
+
+    'ml.get_jobs' => {
+        doc   => "ml-get-job",
+        parts => { job_id => {} },
+        paths => [
+            [ { job_id => 2 }, "_ml", "anomaly_detectors", "{job_id}" ],
+            [ {}, "_ml", "anomaly_detectors" ],
+        ],
+        qs => {
+            allow_no_jobs     => "boolean",
+            allow_no_match    => "boolean",
+            error_trace       => "boolean",
+            exclude_generated => "boolean",
+            filter_path       => "list",
+            human             => "boolean",
+        },
+    },
+
+    'ml.get_model_snapshots' => {
+        body  => {},
+        doc   => "ml-get-snapshot",
+        parts => { job_id => {}, snapshot_id => {} },
+        paths => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+            ],
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "model_snapshots",
+            ],
+        ],
+        qs => {
+            desc        => "boolean",
+            end         => "time",
+            error_trace => "boolean",
+            filter_path => "list",
+            from        => "int",
+            human       => "boolean",
+            size        => "int",
+            sort        => "string",
+            start       => "time",
+        },
+    },
+
+    'ml.get_overall_buckets' => {
+        body  => {},
+        doc   => "ml-get-overall-buckets",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "overall_buckets",
+            ],
+        ],
+        qs => {
+            allow_no_jobs   => "boolean",
+            allow_no_match  => "boolean",
+            bucket_span     => "string",
+            end             => "string",
+            error_trace     => "boolean",
+            exclude_interim => "boolean",
+            filter_path     => "list",
+            human           => "boolean",
+            overall_score   => "double",
+            start           => "string",
+            top_n           => "int",
+        },
+    },
+
+    'ml.get_records' => {
+        body  => {},
+        doc   => "ml-get-record",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "records",
+            ],
+        ],
+        qs => {
+            desc            => "boolean",
+            end             => "string",
+            error_trace     => "boolean",
+            exclude_interim => "boolean",
+            filter_path     => "list",
+            from            => "int",
+            human           => "boolean",
+            record_score    => "double",
+            size            => "int",
+            sort            => "string",
+            start           => "string",
+        },
+    },
+
+    'ml.get_trained_models' => {
+        doc   => "get-trained-models",
+        parts => { model_id => {} },
+        paths => [
+            [ { model_id => 2 }, "_ml", "trained_models", "{model_id}" ],
+            [ {}, "_ml", "trained_models" ],
+        ],
+        qs => {
+            allow_no_match           => "boolean",
+            decompress_definition    => "boolean",
+            error_trace              => "boolean",
+            exclude_generated        => "boolean",
+            filter_path              => "list",
+            from                     => "int",
+            human                    => "boolean",
+            include                  => "string",
+            include_model_definition => "boolean",
+            size                     => "int",
+            tags                     => "list",
+        },
+    },
+
+    'ml.get_trained_models_stats' => {
+        doc   => "get-trained-models-stats",
+        parts => { model_id => {} },
+        paths => [
+            [   { model_id => 2 }, "_ml",
+                "trained_models",  "{model_id}",
+                "_stats",
+            ],
+            [ {}, "_ml", "trained_models", "_stats" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            from           => "int",
+            human          => "boolean",
+            size           => "int",
+        },
+    },
+
+    'ml.info' => {
+        doc   => "get-ml-info",
+        parts => {},
+        paths => [ [ {}, "_ml", "info" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.open_job' => {
+        doc    => "ml-open-job",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_open"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.post_calendar_events' => {
+        body   => { required => 1 },
+        doc    => "ml-post-calendar-event",
+        method => "POST",
+        parts  => { calendar_id => {} },
+        paths  => [
+            [   { calendar_id => 2 }, "_ml",
+                "calendars",          "{calendar_id}",
+                "events",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.post_data' => {
+        body   => { required => 1 },
+        doc    => "ml-post-data",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_data"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            reset_end   => "string",
+            reset_start => "string",
+        },
+        serialize => "bulk",
+    },
+
+    'ml.preview_datafeed' => {
+        doc   => "ml-preview-datafeed",
+        parts => { datafeed_id => {} },
+        paths => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_preview",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_calendar' => {
+        body   => {},
+        doc    => "ml-put-calendar",
+        method => "PUT",
+        parts  => { calendar_id => {} },
+        paths  =>
+            [ [ { calendar_id => 2 }, "_ml", "calendars", "{calendar_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_calendar_job' => {
+        doc    => "ml-put-calendar-job",
+        method => "PUT",
+        parts  => { calendar_id => {}, job_id => {} },
+        paths  => [
+            [   { calendar_id => 2, job_id => 4 },
+                "_ml", "calendars", "{calendar_id}", "jobs", "{job_id}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_data_frame_analytics' => {
+        body   => { required => 1 },
+        doc    => "put-dfanalytics",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  =>
+            [ [ { id => 3 }, "_ml", "data_frame", "analytics", "{id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_datafeed' => {
+        body   => { required => 1 },
+        doc    => "ml-put-datafeed",
+        method => "PUT",
+        parts  => { datafeed_id => {} },
+        paths  =>
+            [ [ { datafeed_id => 2 }, "_ml", "datafeeds", "{datafeed_id}" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_throttled   => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'ml.put_filter' => {
+        body   => { required => 1 },
+        doc    => "ml-put-filter",
+        method => "PUT",
+        parts  => { filter_id => {} },
+        paths  => [ [ { filter_id => 2 }, "_ml", "filters", "{filter_id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_job' => {
+        body   => { required => 1 },
+        doc    => "ml-put-job",
+        method => "PUT",
+        parts  => { job_id => {} },
+        paths  =>
+            [ [ { job_id => 2 }, "_ml", "anomaly_detectors", "{job_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_trained_model' => {
+        body   => { required => 1 },
+        doc    => "put-trained-models",
+        method => "PUT",
+        parts  => { model_id => {} },
+        paths  =>
+            [ [ { model_id => 2 }, "_ml", "trained_models", "{model_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.revert_model_snapshot' => {
+        body   => {},
+        doc    => "ml-revert-snapshot",
+        method => "POST",
+        parts  => { job_id => {}, snapshot_id => {} },
+        paths  => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+                "_revert",
+            ],
+        ],
+        qs => {
+            delete_intervening_results => "boolean",
+            error_trace                => "boolean",
+            filter_path                => "list",
+            human                      => "boolean",
+        },
+    },
+
+    'ml.set_upgrade_mode' => {
+        doc    => "ml-set-upgrade-mode",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ml", "set_upgrade_mode" ] ],
+        qs     => {
+            enabled     => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'ml.start_data_frame_analytics' => {
+        body   => {},
+        doc    => "start-dfanalytics",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics",
+                "{id}",      "_start"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'ml.start_datafeed' => {
+        body   => {},
+        doc    => "ml-start-datafeed",
+        method => "POST",
+        parts  => { datafeed_id => {} },
+        paths  => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_start",
+            ],
+        ],
+        qs => {
+            end         => "string",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            start       => "string",
+            timeout     => "time",
+        },
+    },
+
+    'ml.stop_data_frame_analytics' => {
+        body   => {},
+        doc    => "stop-dfanalytics",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics", "{id}",
+                "_stop"
+            ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            force          => "boolean",
+            human          => "boolean",
+            timeout        => "time",
+        },
+    },
+
+    'ml.stop_datafeed' => {
+        body   => {},
+        doc    => "ml-stop-datafeed",
+        method => "POST",
+        parts  => { datafeed_id => {} },
+        paths  => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_stop",
+            ],
+        ],
+        qs => {
+            allow_no_datafeeds => "boolean",
+            allow_no_match     => "boolean",
+            error_trace        => "boolean",
+            filter_path        => "list",
+            force              => "boolean",
+            human              => "boolean",
+            timeout            => "time",
+        },
+    },
+
+    'ml.update_data_frame_analytics' => {
+        body   => { required => 1 },
+        doc    => "update-dfanalytics",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics",
+                "{id}",      "_update"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.update_datafeed' => {
+        body   => { required => 1 },
+        doc    => "ml-update-datafeed",
+        method => "POST",
+        parts  => { datafeed_id => {} },
+        paths  => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_update",
+            ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_throttled   => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'ml.update_filter' => {
+        body   => { required => 1 },
+        doc    => "ml-update-filter",
+        method => "POST",
+        parts  => { filter_id => {} },
+        paths  => [
+            [   { filter_id => 2 }, "_ml", "filters", "{filter_id}",
+                "_update"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.update_job' => {
+        body   => { required => 1 },
+        doc    => "ml-update-job",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_update",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.update_model_snapshot' => {
+        body   => { required => 1 },
+        doc    => "ml-update-snapshot",
+        method => "POST",
+        parts  => { job_id => {}, snapshot_id => {} },
+        paths  => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+                "_update",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.upgrade_job_snapshot' => {
+        doc    => "ml-upgrade-job-model-snapshot",
+        method => "POST",
+        parts  => { job_id => {}, snapshot_id => {} },
+        paths  => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+                "_upgrade",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            timeout             => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'ml.validate' => {
+        body   => { required => 1 },
+        doc    => "ml-jobs",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ml", "anomaly_detectors", "_validate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.validate_detector' => {
+        body   => { required => 1 },
+        doc    => "ml-jobs",
+        method => "POST",
+        parts  => {},
+        paths  =>
+            [ [ {}, "_ml", "anomaly_detectors", "_validate", "detector" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'monitoring.bulk' => {
+        body   => { required => 1 },
+        doc    => "monitor-elasticsearch-cluster",
+        method => "POST",
+        parts  => { type => {} },
+        paths  => [
+            [ { type => 1 }, "_monitoring", "{type}", "bulk" ],
+            [ {}, "_monitoring", "bulk" ],
+        ],
+        qs => {
+            error_trace        => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+            interval           => "string",
+            system_api_version => "string",
+            system_id          => "string",
+        },
+        serialize => "bulk",
+    },
+
+    'nodes.hot_threads' => {
+        doc   => "cluster-nodes-hot-threads",
+        parts => { node_id => { multi => 1 } },
+        paths => [
+            [ { node_id => 1 }, "_nodes", "{node_id}", "hot_threads" ],
+            [ {}, "_nodes", "hot_threads" ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            ignore_idle_threads => "boolean",
+            interval            => "time",
+            snapshots           => "number",
+            threads             => "number",
+            timeout             => "time",
+            type                => "enum",
+        },
+    },
+
+    'nodes.info' => {
+        doc   => "cluster-nodes-info",
+        parts => { metric => { multi => 1 }, node_id => { multi => 1 } },
+        paths => [
+            [   { metric => 2, node_id => 1 }, "_nodes",
+                "{node_id}",                   "{metric}",
+            ],
+            [ { metric  => 1 }, "_nodes", "{metric}" ],
+            [ { node_id => 1 }, "_nodes", "{node_id}" ],
+            [ {}, "_nodes" ],
+        ],
+        qs => {
+            error_trace   => "boolean",
+            filter_path   => "list",
+            flat_settings => "boolean",
+            human         => "boolean",
+            timeout       => "time",
+        },
+    },
+
+    'nodes.reload_secure_settings' => {
+        body   => {},
+        doc    => "",
+        method => "POST",
+        parts  => { node_id => { multi => 1 } },
+        paths  => [
+            [   { node_id => 1 }, "_nodes",
+                "{node_id}",      "reload_secure_settings",
+            ],
+            [ {}, "_nodes", "reload_secure_settings" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'nodes.stats' => {
+        doc   => "cluster-nodes-stats",
+        parts => {
+            index_metric => { multi => 1 },
+            metric       => { multi => 1 },
+            node_id      => { multi => 1 },
+        },
+        paths => [
+            [   { index_metric => 4, metric => 3, node_id => 1 },
+                "_nodes", "{node_id}", "stats", "{metric}", "{index_metric}",
+            ],
+            [   { index_metric => 3, metric => 2 }, "_nodes",
+                "stats",                            "{metric}",
+                "{index_metric}",
+            ],
+            [   { metric => 3, node_id => 1 }, "_nodes",
+                "{node_id}",                   "stats",
+                "{metric}",
+            ],
+            [ { metric  => 2 }, "_nodes", "stats",     "{metric}" ],
+            [ { node_id => 1 }, "_nodes", "{node_id}", "stats" ],
+            [ {}, "_nodes", "stats" ],
+        ],
+        qs => {
+            completion_fields          => "list",
+            error_trace                => "boolean",
+            fielddata_fields           => "list",
+            fields                     => "list",
+            filter_path                => "list",
+            groups                     => "boolean",
+            human                      => "boolean",
+            include_segment_file_sizes => "boolean",
+            level                      => "enum",
+            timeout                    => "time",
+            types                      => "list",
+        },
+    },
+
+    'nodes.usage' => {
+        doc   => "cluster-nodes-usage",
+        parts => { metric => { multi => 1 }, node_id => { multi => 1 } },
+        paths => [
+            [   { metric => 3, node_id => 1 }, "_nodes",
+                "{node_id}",                   "usage",
+                "{metric}",
+            ],
+            [ { metric  => 2 }, "_nodes", "usage",     "{metric}" ],
+            [ { node_id => 1 }, "_nodes", "{node_id}", "usage" ],
+            [ {}, "_nodes", "usage" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'rollup.delete_job' => {
+        doc    => "rollup-delete-job",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_rollup", "job", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.get_jobs' => {
+        doc   => "rollup-get-job",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_rollup", "job", "{id}" ],
+            [ {}, "_rollup", "job" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.get_rollup_caps' => {
+        doc   => "rollup-get-rollup-caps",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_rollup", "data", "{id}" ],
+            [ {}, "_rollup", "data" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.get_rollup_index_caps' => {
+        doc   => "rollup-get-rollup-index-caps",
+        parts => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_rollup", "data" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.put_job' => {
+        body   => { required => 1 },
+        doc    => "rollup-put-job",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_rollup", "job", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.rollup' => {
+        body   => { required => 1 },
+        doc    => "rollup-api",
+        method => "POST",
+        parts  =>
+            { index => { required => 1 }, rollup_index => { required => 1 } },
+        paths => [
+            [   { index => 0, rollup_index => 2 }, "{index}",
+                "_rollup",                         "{rollup_index}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.rollup_search' => {
+        body  => { required => 1 },
+        doc   => "rollup-search",
+        parts => { index => { multi => 1 }, type => {} },
+        paths => [
+            [   { index => 0, type => 1 }, "{index}",
+                "{type}",                  "_rollup_search",
+            ],
+            [ { index => 0 }, "{index}", "_rollup_search" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            rest_total_hits_as_int => "boolean",
+            typed_keys             => "boolean",
+        },
+    },
+
+    'rollup.start_job' => {
+        doc    => "rollup-start-job",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_rollup", "job", "{id}", "_start" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.stop_job' => {
+        doc    => "rollup-stop-job",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_rollup", "job", "{id}", "_stop" ] ],
+        qs     => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            timeout             => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'searchable_snapshots.clear_cache' => {
+        doc    => "searchable-snapshots-apis",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [   { index => 0 },          "{index}",
+                "_searchable_snapshots", "cache",
+                "clear",
+            ],
+            [ {}, "_searchable_snapshots", "cache", "clear" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'searchable_snapshots.mount' => {
+        body   => { required => 1 },
+        doc    => "searchable-snapshots-api-mount-snapshot",
+        method => "POST",
+        parts  => { repository => {}, snapshot => {} },
+        paths  => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+                "_mount",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            master_timeout      => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'searchable_snapshots.repository_stats' => {
+        doc   => "searchable-snapshots-apis",
+        parts => { repository => {} },
+        paths => [
+            [ { repository => 1 }, "_snapshot", "{repository}", "_stats" ]
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'searchable_snapshots.stats' => {
+        doc   => "searchable-snapshots-apis",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_searchable_snapshots", "stats" ],
+            [ {}, "_searchable_snapshots", "stats" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.authenticate' => {
+        doc   => "security-api-authenticate",
+        parts => {},
+        paths => [ [ {}, "_security", "_authenticate" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.change_password' => {
+        body   => { required => 1 },
+        doc    => "security-api-change-password",
+        method => "PUT",
+        parts  => { username => {} },
+        paths  => [
+            [   { username => 2 }, "_security",
+                "user",            "{username}",
+                "_password",
+            ],
+            [ {}, "_security", "user", "_password" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.clear_api_key_cache' => {
+        doc    => "security-api-clear-api-key-cache",
+        method => "POST",
+        parts  => { ids => { multi => 1 } },
+        paths  => [
+            [ { ids => 2 }, "_security", "api_key", "{ids}", "_clear_cache" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.clear_cached_privileges' => {
+        doc    => "security-api-clear-privilege-cache",
+        method => "POST",
+        parts  => { application => { multi => 1 } },
+        paths  => [
+            [   { application => 2 }, "_security",
+                "privilege",          "{application}",
+                "_clear_cache",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.clear_cached_realms' => {
+        doc    => "security-api-clear-cache",
+        method => "POST",
+        parts  => { realms => { multi => 1 } },
+        paths  => [
+            [   { realms => 2 }, "_security", "realm", "{realms}",
+                "_clear_cache",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            usernames   => "list",
+        },
+    },
+
+    'security.clear_cached_roles' => {
+        doc    => "security-api-clear-role-cache",
+        method => "POST",
+        parts  => { name => { multi => 1 } },
+        paths  => [
+            [ { name => 2 }, "_security", "role", "{name}", "_clear_cache" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.create_api_key' => {
+        body   => { required => 1 },
+        doc    => "security-api-create-api-key",
+        method => "PUT",
+        parts  => {},
+        paths  => [ [ {}, "_security", "api_key" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.delete_privileges' => {
+        doc    => "security-api-delete-privilege",
+        method => "DELETE",
+        parts  => { application => {}, name => {} },
+        paths  => [
+            [   { application => 2, name => 3 }, "_security",
+                "privilege",                     "{application}",
+                "{name}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.delete_role' => {
+        doc    => "security-api-delete-role",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_security", "role", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.delete_role_mapping' => {
+        doc    => "security-api-delete-role-mapping",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths => [ [ { name => 2 }, "_security", "role_mapping", "{name}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.delete_user' => {
+        doc    => "security-api-delete-user",
+        method => "DELETE",
+        parts  => { username => {} },
+        paths => [ [ { username => 2 }, "_security", "user", "{username}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.disable_user' => {
+        doc    => "security-api-disable-user",
+        method => "PUT",
+        parts  => { username => {} },
+        paths  => [
+            [   { username => 2 }, "_security",
+                "user",            "{username}",
+                "_disable"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.enable_user' => {
+        doc    => "security-api-enable-user",
+        method => "PUT",
+        parts  => { username => {} },
+        paths  => [
+            [   { username => 2 }, "_security",
+                "user",            "{username}",
+                "_enable"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.get_api_key' => {
+        doc   => "security-api-get-api-key",
+        parts => {},
+        paths => [ [ {}, "_security", "api_key" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            id          => "string",
+            name        => "string",
+            owner       => "boolean",
+            realm_name  => "string",
+            username    => "string",
+        },
+    },
+
+    'security.get_builtin_privileges' => {
+        doc   => "security-api-get-builtin-privileges",
+        parts => {},
+        paths => [ [ {}, "_security", "privilege", "_builtin" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_privileges' => {
+        doc   => "security-api-get-privileges",
+        parts => { application => {}, name => {} },
+        paths => [
+            [   { application => 2, name => 3 }, "_security",
+                "privilege",                     "{application}",
+                "{name}",
+            ],
+            [   { application => 2 }, "_security",
+                "privilege",          "{application}"
+            ],
+            [ {}, "_security", "privilege" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_role' => {
+        doc   => "security-api-get-role",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 2 }, "_security", "role", "{name}" ],
+            [ {}, "_security", "role" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_role_mapping' => {
+        doc   => "security-api-get-role-mapping",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 2 }, "_security", "role_mapping", "{name}" ],
+            [ {}, "_security", "role_mapping" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_token' => {
+        body   => { required => 1 },
+        doc    => "security-api-get-token",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "oauth2", "token" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_user' => {
+        doc   => "security-api-get-user",
+        parts => { username => { multi => 1 } },
+        paths => [
+            [ { username => 2 }, "_security", "user", "{username}" ],
+            [ {}, "_security", "user" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_user_privileges' => {
+        doc   => "security-api-get-privileges",
+        parts => {},
+        paths => [ [ {}, "_security", "user", "_privileges" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.grant_api_key' => {
+        body   => { required => 1 },
+        doc    => "security-api-grant-api-key",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "api_key", "grant" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.has_privileges' => {
+        body  => { required => 1 },
+        doc   => "security-api-has-privileges",
+        parts => { user => {} },
+        paths => [
+            [   { user => 2 }, "_security",
+                "user",        "{user}",
+                "_has_privileges"
+            ],
+            [ {}, "_security", "user", "_has_privileges" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.invalidate_api_key' => {
+        body   => { required => 1 },
+        doc    => "security-api-invalidate-api-key",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_security", "api_key" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.invalidate_token' => {
+        body   => { required => 1 },
+        doc    => "security-api-invalidate-token",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_security", "oauth2", "token" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.put_privileges' => {
+        body   => { required => 1 },
+        doc    => "security-api-put-privileges",
+        method => "PUT",
+        parts  => {},
+        paths  => [ [ {}, "_security", "privilege" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.put_role' => {
+        body   => { required => 1 },
+        doc    => "security-api-put-role",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_security", "role", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.put_role_mapping' => {
+        body   => { required => 1 },
+        doc    => "security-api-put-role-mapping",
+        method => "PUT",
+        parts  => { name => {} },
+        paths => [ [ { name => 2 }, "_security", "role_mapping", "{name}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.put_user' => {
+        body   => { required => 1 },
+        doc    => "security-api-put-user",
+        method => "PUT",
+        parts  => { username => {} },
+        paths => [ [ { username => 2 }, "_security", "user", "{username}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'slm.delete_lifecycle' => {
+        doc    => "slm-api-delete-policy",
+        method => "DELETE",
+        parts  => { policy_id => {} },
+        paths  => [ [ { policy_id => 2 }, "_slm", "policy", "{policy_id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.execute_lifecycle' => {
+        doc    => "slm-api-execute-lifecycle",
+        method => "PUT",
+        parts  => { policy_id => {} },
+        paths  => [
+            [   { policy_id => 2 }, "_slm",
+                "policy",           "{policy_id}",
+                "_execute"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.execute_retention' => {
+        doc    => "slm-api-execute-retention",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_slm", "_execute_retention" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.get_lifecycle' => {
+        doc   => "slm-api-get-policy",
+        parts => { policy_id => { multi => 1 } },
+        paths => [
+            [ { policy_id => 2 }, "_slm", "policy", "{policy_id}" ],
+            [ {}, "_slm", "policy" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.get_stats' => {
+        doc   => "slm-api-get-stats",
+        parts => {},
+        paths => [ [ {}, "_slm", "stats" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.get_status' => {
+        doc   => "slm-api-get-status",
+        parts => {},
+        paths => [ [ {}, "_slm", "status" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.put_lifecycle' => {
+        body   => {},
+        doc    => "slm-api-put-policy",
+        method => "PUT",
+        parts  => { policy_id => {} },
+        paths  => [ [ { policy_id => 2 }, "_slm", "policy", "{policy_id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.start' => {
+        doc    => "slm-api-start",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_slm", "start" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.stop' => {
+        doc    => "slm-api-stop",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_slm", "stop" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'snapshot.cleanup_repository' => {
+        doc    => "clean-up-snapshot-repo-api",
+        method => "POST",
+        parts  => { repository => {} },
+        paths  => [
+            [ { repository => 1 }, "_snapshot", "{repository}", "_cleanup" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'snapshot.clone' => {
+        body   => { required => 1 },
+        doc    => "modules-snapshots",
+        method => "PUT",
+        parts  => { repository => {}, snapshot => {}, target_snapshot => {} },
+        paths  => [
+            [   { repository => 1, snapshot => 2, target_snapshot => 4 },
+                "_snapshot",
+                "{repository}",
+                "{snapshot}",
+                "_clone",
+                "{target_snapshot}",
+            ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'snapshot.create' => {
+        body   => {},
+        doc    => "modules-snapshots",
+        method => "PUT",
+        parts  => { repository => {}, snapshot => {} },
+        paths  => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            master_timeout      => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'snapshot.create_repository' => {
+        body   => { required => 1 },
+        doc    => "modules-snapshots",
+        method => "PUT",
+        parts  => { repository => {} },
+        paths  => [ [ { repository => 1 }, "_snapshot", "{repository}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+            verify         => "boolean",
+        },
+    },
+
+    'snapshot.delete' => {
+        doc    => "modules-snapshots",
+        method => "DELETE",
+        parts  => { repository => {}, snapshot => {} },
+        paths  => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+            ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'snapshot.delete_repository' => {
+        doc    => "modules-snapshots",
+        method => "DELETE",
+        parts  => { repository => { multi => 1 } },
+        paths  => [ [ { repository => 1 }, "_snapshot", "{repository}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'snapshot.get' => {
+        doc   => "modules-snapshots",
+        parts => { repository => {}, snapshot => { multi => 1 } },
+        paths => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+            ],
+        ],
+        qs => {
+            error_trace        => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            verbose            => "boolean",
+        },
+    },
+
+    'snapshot.get_repository' => {
+        doc   => "modules-snapshots",
+        parts => { repository => { multi => 1 } },
+        paths => [
+            [ { repository => 1 }, "_snapshot", "{repository}" ],
+            [ {}, "_snapshot" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'snapshot.restore' => {
+        body   => {},
+        doc    => "modules-snapshots",
+        method => "POST",
+        parts  => { repository => {}, snapshot => {} },
+        paths  => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+                "_restore",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            master_timeout      => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'snapshot.status' => {
+        doc   => "modules-snapshots",
+        parts => { repository => {}, snapshot => { multi => 1 } },
+        paths => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+                "_status",
+            ],
+            [ { repository => 1 }, "_snapshot", "{repository}", "_status" ],
+            [ {}, "_snapshot", "_status" ],
+        ],
+        qs => {
+            error_trace        => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+        },
+    },
+
+    'snapshot.verify_repository' => {
+        doc    => "modules-snapshots",
+        method => "POST",
+        parts  => { repository => {} },
+        paths  => [
+            [ { repository => 1 }, "_snapshot", "{repository}", "_verify" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'sql.clear_cursor' => {
+        body   => { required => 1 },
+        doc    => "sql-pagination",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_sql", "close" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'sql.query' => {
+        body   => { required => 1 },
+        doc    => "sql-rest-overview",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_sql" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            human       => "boolean",
+        },
+    },
+
+    'sql.translate' => {
+        body   => { required => 1 },
+        doc    => "sql-translate",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_sql", "translate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ssl.certificates' => {
+        doc   => "security-api-ssl",
+        parts => {},
+        paths => [ [ {}, "_ssl", "certificates" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'tasks.cancel' => {
+        doc    => "tasks",
+        method => "POST",
+        parts  => { task_id => {} },
+        paths  => [
+            [ { task_id => 1 }, "_tasks", "{task_id}", "_cancel" ],
+            [ {}, "_tasks", "_cancel" ],
+        ],
+        qs => {
+            actions             => "list",
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            nodes               => "list",
+            parent_task_id      => "string",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'tasks.get' => {
+        doc   => "tasks",
+        parts => { task_id => {} },
+        paths => [ [ { task_id => 1 }, "_tasks", "{task_id}" ] ],
+        qs    => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            timeout             => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'tasks.list' => {
+        doc   => "tasks",
+        parts => {},
+        paths => [ [ {}, "_tasks" ] ],
+        qs    => {
+            actions             => "list",
+            detailed            => "boolean",
+            error_trace         => "boolean",
+            filter_path         => "list",
+            group_by            => "enum",
+            human               => "boolean",
+            nodes               => "list",
+            parent_task_id      => "string",
+            timeout             => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'transform.delete_transform' => {
+        doc    => "delete-transform",
+        method => "DELETE",
+        parts  => { transform_id => {} },
+        paths  =>
+            [ [ { transform_id => 1 }, "_transform", "{transform_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            force       => "boolean",
+            human       => "boolean",
+        },
+    },
+
+    'transform.get_transform' => {
+        doc   => "get-transform",
+        parts => { transform_id => {} },
+        paths => [
+            [ { transform_id => 1 }, "_transform", "{transform_id}" ],
+            [ {}, "_transform" ],
+        ],
+        qs => {
+            allow_no_match    => "boolean",
+            error_trace       => "boolean",
+            exclude_generated => "boolean",
+            filter_path       => "list",
+            from              => "int",
+            human             => "boolean",
+            size              => "int",
+        },
+    },
+
+    'transform.get_transform_stats' => {
+        doc   => "get-transform-stats",
+        parts => { transform_id => {} },
+        paths => [
+            [   { transform_id => 1 }, "_transform",
+                "{transform_id}",      "_stats"
+            ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            from           => "number",
+            human          => "boolean",
+            size           => "number",
+        },
+    },
+
+    'transform.preview_transform' => {
+        body   => { required => 1 },
+        doc    => "preview-transform",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_transform", "_preview" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'transform.put_transform' => {
+        body   => { required => 1 },
+        doc    => "put-transform",
+        method => "PUT",
+        parts  => { transform_id => {} },
+        paths  =>
+            [ [ { transform_id => 1 }, "_transform", "{transform_id}" ] ],
+        qs => {
+            defer_validation => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'transform.start_transform' => {
+        doc    => "start-transform",
+        method => "POST",
+        parts  => { transform_id => {} },
+        paths  => [
+            [   { transform_id => 1 }, "_transform",
+                "{transform_id}",      "_start"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'transform.stop_transform' => {
+        doc    => "stop-transform",
+        method => "POST",
+        parts  => { transform_id => {} },
+        paths  => [
+            [   { transform_id => 1 }, "_transform", "{transform_id}",
+                "_stop"
+            ],
+        ],
+        qs => {
+            allow_no_match      => "boolean",
+            error_trace         => "boolean",
+            filter_path         => "list",
+            force               => "boolean",
+            human               => "boolean",
+            timeout             => "time",
+            wait_for_checkpoint => "boolean",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'transform.update_transform' => {
+        body   => { required => 1 },
+        doc    => "update-transform",
+        method => "POST",
+        parts  => { transform_id => { required => 1 } },
+        paths  => [
+            [   { transform_id => 1 }, "_transform",
+                "{transform_id}",      "_update",
+            ],
+        ],
+        qs => {
+            defer_validation => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'watcher.ack_watch' => {
+        doc    => "watcher-api-ack-watch",
+        method => "PUT",
+        parts  => { action_id => { multi => 1 }, watch_id => {} },
+        paths  => [
+            [   { action_id => 4, watch_id => 2 }, "_watcher",
+                "watch",                           "{watch_id}",
+                "_ack",                            "{action_id}",
+            ],
+            [ { watch_id => 2 }, "_watcher", "watch", "{watch_id}", "_ack" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.activate_watch' => {
+        doc    => "watcher-api-activate-watch",
+        method => "PUT",
+        parts  => { watch_id => {} },
+        paths  => [
+            [   { watch_id => 2 }, "_watcher",
+                "watch",           "{watch_id}",
+                "_activate",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.deactivate_watch' => {
+        doc    => "watcher-api-deactivate-watch",
+        method => "PUT",
+        parts  => { watch_id => {} },
+        paths  => [
+            [   { watch_id => 2 }, "_watcher",
+                "watch",           "{watch_id}",
+                "_deactivate",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.delete_watch' => {
+        doc    => "watcher-api-delete-watch",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_watcher", "watch", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.execute_watch' => {
+        body   => {},
+        doc    => "watcher-api-execute-watch",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [
+            [ { id => 2 }, "_watcher", "watch", "{id}", "_execute" ],
+            [ {}, "_watcher", "watch", "_execute" ],
+        ],
+        qs => {
+            debug       => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'watcher.get_watch' => {
+        doc   => "watcher-api-get-watch",
+        parts => { id => {} },
+        paths => [ [ { id => 2 }, "_watcher", "watch", "{id}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.put_watch' => {
+        body   => {},
+        doc    => "watcher-api-put-watch",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_watcher", "watch", "{id}" ] ],
+        qs     => {
+            active          => "boolean",
+            error_trace     => "boolean",
+            filter_path     => "list",
+            human           => "boolean",
+            if_primary_term => "number",
+            if_seq_no       => "number",
+            version         => "number",
+        },
+    },
+
+    'watcher.query_watches' => {
+        body  => {},
+        doc   => "watcher-api-query-watches",
+        parts => {},
+        paths => [ [ {}, "_watcher", "_query", "watches" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.start' => {
+        doc    => "watcher-api-start",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_watcher", "_start" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.stats' => {
+        doc   => "watcher-api-stats",
+        parts => { metric => { multi => 1 } },
+        paths => [
+            [ { metric => 2 }, "_watcher", "stats", "{metric}" ],
+            [ {}, "_watcher", "stats" ],
+        ],
+        qs => {
+            emit_stacktraces => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'watcher.stop' => {
+        doc    => "watcher-api-stop",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_watcher", "_stop" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'xpack.info' => {
+        doc   => "info-api",
+        parts => {},
+        paths => [ [ {}, "_xpack" ] ],
+        qs    => {
+            accept_enterprise => "boolean",
+            categories        => "list",
+            error_trace       => "boolean",
+            filter_path       => "list",
+            human             => "boolean",
+        },
+    },
+
+    'xpack.usage' => {
+        doc   => "usage-api",
+        parts => {},
+        paths => [ [ {}, "_xpack", "usage" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+#=== AUTOGEN - END ===
+
+);
+
+__PACKAGE__->_qs_init( \%API );
+1;
+
+__END__
+
+# ABSTRACT: This class contains the spec for the Elasticsearch APIs
+
+=head1 DESCRIPTION
+
+All of the Elasticsearch APIs are defined in this role. The example given below
+is the definition for the L<Search::Elasticsearch::Client::7_0::Direct/index()> method:
+
+    'index' => {
+        body   => { required => 1 },
+        doc    => "docs-index_",
+        method => "POST",
+        parts  => { id => {}, index => {}, type => {} },
+        paths  => [
+            [   { id => 2, index => 0, type => 1 }, "{index}",
+                "{type}",                           "{id}"
+            ],
+            [ { id    => 2, index => 0 }, "{index}", "_doc", "{id}" ],
+            [ { index => 0, type  => 1 }, "{index}", "{type}" ],
+            [ { index => 0 }, "{index}", "_doc" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            if_primary_term        => "number",
+            if_seq_no              => "number",
+            op_type                => "enum",
+            pipeline               => "string",
+            refresh                => "enum",
+            require_alias          => "boolean",
+            routing                => "string",
+            timeout                => "time",
+            version                => "number",
+            version_type           => "enum",
+            wait_for_active_shards => "string",
+        },
+    }
+
+These definitions can be used by different L<Search::Elasticsearch::Role::Client>
+implementations to provide distinct user interfaces.
+
+=head1 METHODS
+
+=head2 C<api()>
+
+    $defn = $api->api($name);
+
+The only method in this class is the C<api()> method which takes the name
+of the I<action> and returns its definition.  Actions in the
+C<indices> or C<cluster> namespace use the namespace as a prefix, eg:
+
+    $defn = $e->api('indices.create');
+    $defn = $e->api('cluster.node_stats');
+
+=head1 SEE ALSO
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Role::API>
+
+=item *
+
+L<Search::Elasticsearch::Client::7_0::Direct>
+
+=back
diff --git a/lib/Search/Elasticsearch/Client/2_0/Role/Bulk.pm b/lib/Search/Elasticsearch/Client/7_0/Role/Bulk.pm
similarity index 83%
rename from lib/Search/Elasticsearch/Client/2_0/Role/Bulk.pm
rename to lib/Search/Elasticsearch/Client/7_0/Role/Bulk.pm
index 27330c2..c834f94 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Role/Bulk.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Role/Bulk.pm
@@ -1,5 +1,22 @@
-package Search::Elasticsearch::Client::2_0::Role::Bulk;
-$Search::Elasticsearch::Client::2_0::Role::Bulk::VERSION = '6.81';
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Role::Bulk;
+
 use Moo::Role;
 requires 'add_action', 'flush';
 
@@ -33,16 +50,6 @@ our %Actions = (
     'delete' => 1
 );
 
-around BUILDARGS => sub {
-    my $orig = shift;
-    my ( $class, $params ) = parse_params(@_);
-    my $es = $params->{es} or throw( 'Param', 'Missing required param <es>' );
-    $params->{_metadata_params} = $es->api('bulk.metadata')->{params};
-    $params->{_update_params}   = $es->api('bulk.update')->{params};
-    $params->{_required_params} = $es->api('bulk.required')->{params};
-    return $class->$orig($params);
-};
-
 #===================================
 sub _build__serializer { shift->es->transport->serializer }
 #===================================
@@ -62,10 +69,14 @@ sub _build_on_error {
 sub BUILDARGS {
 #===================================
     my ( $class, $params ) = parse_params(@_);
+    my $es = $params->{es} or throw( 'Param', 'Missing required param <es>' );
+    $params->{_metadata_params} = $es->api('bulk.metadata')->{params};
+    $params->{_update_params}   = $es->api('bulk.update')->{params};
+    $params->{_required_params} = $es->api('bulk.required')->{params};
+    my $bulk_spec = $es->api('bulk');
     my %args;
-    for (qw(index type consistency fields refresh replication routing timeout))
-    {
-        $args{$_} = $params->{$_}
+    for ( keys %{ $bulk_spec->{qs} }, keys %{ $bulk_spec->{parts} } ) {
+        $args{$_} = delete $params->{$_}
             if exists $params->{$_};
     }
     $params->{_bulk_args} = \%args;
@@ -100,7 +111,7 @@ sub update {
 sub create_docs {
 #===================================
     my $self = shift;
-    $self->add_action( map { ( 'create' => { _source => $_ } ) } @_ );
+    $self->add_action( map { ( 'create' => { source => $_ } ) } @_ );
 }
 
 #===================================
@@ -127,12 +138,10 @@ sub _encode_action {
     my $params     = {%$orig};
     my $serializer = $self->_serializer;
 
-    for ( @{ $self->_metadata_params } ) {
-        my $val
-            = exists $params->{$_}    ? delete $params->{$_}
-            : exists $params->{"_$_"} ? delete $params->{"_$_"}
-            :                           next;
-        $metadata{"_$_"} = $val;
+    my $meta_params = $self->_metadata_params;
+    for ( keys %$meta_params ) {
+        next unless exists $params->{$_};
+        $metadata{ $meta_params->{$_} } = delete $params->{$_};
     }
 
     for ( @{ $self->_required_params } ) {
@@ -148,14 +157,10 @@ sub _encode_action {
         }
     }
     elsif ( $action ne 'delete' ) {
-        $source
-            = delete $params->{_source}
-            || delete $params->{source}
-            || throw(
-            'Param',
+        $source = delete $params->{source}
+            || throw( 'Param',
             "Missing <source> for action <$action>: "
-                . $serializer->encode($orig)
-            );
+                . $serializer->encode($orig) );
     }
 
     throw(    "Unknown params <"
@@ -272,32 +277,4 @@ sub _doc_transformer {
 
 1;
 
-# ABSTRACT: Provides common functionality to L<Elasticseach::Client::2_0::Bulk> and L<Search::Elasticsearch::Client::2_0::Async::Bulk>
-
-__END__
-
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Role::Bulk - Provides common functionality to L<Elasticseach::Client::2_0::Bulk> and L<Search::Elasticsearch::Client::2_0::Async::Bulk>
-
-=head1 VERSION
-
-version 6.81
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
+# ABSTRACT: Provides common functionality to L<Elasticseach::Client::7_0::Bulk> and L<Search::Elasticsearch::Client::7_0::Async::Bulk>
diff --git a/lib/Search/Elasticsearch/Client/2_0/Role/Scroll.pm b/lib/Search/Elasticsearch/Client/7_0/Role/Scroll.pm
similarity index 58%
rename from lib/Search/Elasticsearch/Client/2_0/Role/Scroll.pm
rename to lib/Search/Elasticsearch/Client/7_0/Role/Scroll.pm
index 4837ae9..9d67a0f 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Role/Scroll.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Role/Scroll.pm
@@ -1,14 +1,29 @@
-package Search::Elasticsearch::Client::2_0::Role::Scroll;
-$Search::Elasticsearch::Client::2_0::Role::Scroll::VERSION = '6.81';
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Role::Scroll;
+
 use Moo::Role;
 requires 'finish';
 use Search::Elasticsearch::Util qw(parse_params throw);
 use Devel::GlobalDestruction;
 use namespace::clean;
-
 has 'es' => ( is => 'ro', required => 1 );
 has 'scroll'        => ( is => 'ro' );
-has 'scroll_in_qs'  => ( is => 'ro' );
 has 'total'         => ( is => 'rwp' );
 has 'max_score'     => ( is => 'rwp' );
 has 'facets'        => ( is => 'rwp' );
@@ -31,12 +46,7 @@ sub scroll_request {
         if $self->_pid != $$;
 
     my %args = ( scroll => $self->scroll );
-    if ( $self->scroll_in_qs ) {
-        $args{scroll_id} = $self->_scroll_id;
-    }
-    else {
-        $args{body} = $self->_scroll_id;
-    }
+    $args{body} = { scroll_id => $self->_scroll_id };
     $self->es->scroll(%args);
 }
 
@@ -50,32 +60,4 @@ sub DEMOLISH {
 
 1;
 
-# ABSTRACT: Provides common functionality to L<Search::Elasticsearch::Client::2_0::Scroll> and L<Search::Elasticsearch::Client::2_0::Async::Scroll>
-
-__END__
-
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Role::Scroll - Provides common functionality to L<Search::Elasticsearch::Client::2_0::Scroll> and L<Search::Elasticsearch::Client::2_0::Async::Scroll>
-
-=head1 VERSION
-
-version 6.81
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
+# ABSTRACT: Provides common functionality to L<Search::Elasticsearch::Client::7_0::Scroll> and L<Search::Elasticsearch::Client::7_0::Async::Scroll>
diff --git a/lib/Search/Elasticsearch/Client/2_0/Scroll.pm b/lib/Search/Elasticsearch/Client/7_0/Scroll.pm
similarity index 68%
rename from lib/Search/Elasticsearch/Client/2_0/Scroll.pm
rename to lib/Search/Elasticsearch/Client/7_0/Scroll.pm
index fa32c66..e94f9f6 100644
--- a/lib/Search/Elasticsearch/Client/2_0/Scroll.pm
+++ b/lib/Search/Elasticsearch/Client/7_0/Scroll.pm
@@ -1,5 +1,22 @@
-package Search::Elasticsearch::Client::2_0::Scroll;
-$Search::Elasticsearch::Client::2_0::Scroll::VERSION = '6.81';
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::Scroll;
+
 use Moo;
 use Search::Elasticsearch::Util qw(parse_params throw);
 use namespace::clean;
@@ -7,7 +24,7 @@ use namespace::clean;
 has '_buffer' => ( is => 'ro' );
 
 with 'Search::Elasticsearch::Role::Is_Sync',
-    'Search::Elasticsearch::Client::2_0::Role::Scroll';
+    'Search::Elasticsearch::Client::7_0::Role::Scroll';
 
 #===================================
 sub BUILDARGS {
@@ -16,19 +33,16 @@ sub BUILDARGS {
     my $es = delete $params->{es};
     my $scroll = $params->{scroll} ||= '1m';
 
-    throw( 'Param',
-        'The (scroll_in_body) parameter has been replaced by (scroll_in_qs)' )
-        if exists $params->{scroll_in_body};
-
-    my $scroll_in_qs = delete $params->{scroll_in_qs};
     my $results      = $es->search($params);
 
     my $total = $results->{hits}{total};
+    if (ref $total) {
+        $total = $total->{value}
+    }
 
     return {
         es           => $es,
         scroll       => $scroll,
-        scroll_in_qs => $scroll_in_qs,
         aggregations => $results->{aggregations},
         facets       => $results->{facets},
         suggest      => $results->{suggest},
@@ -106,27 +120,16 @@ sub finish {
     my $scroll_id = $self->_scroll_id or return;
     $self->_clear_scroll_id;
 
-    my %args
-        = $self->scroll_in_qs
-        ? ( scroll_id => $scroll_id )
-        : ( body => $scroll_id );
+    my %args = ( body => { scroll_id => $scroll_id } );
     eval { $self->es->clear_scroll(%args) };
     return 1;
 }
 
 1;
 
-=pod
-
-=encoding UTF-8
-
-=head1 NAME
-
-Search::Elasticsearch::Client::2_0::Scroll - A helper module for scrolled searches
-
-=head1 VERSION
+__END__
 
-version 6.81
+# ABSTRACT: A helper module for scrolled searches
 
 =head1 SYNOPSIS
 
@@ -136,8 +139,11 @@ version 6.81
 
     my $scroll = $es->scroll_helper(
         index       => 'my_index',
-        search_type => 'scan',
-        size        => 500
+        body => {
+            query   => {...},
+            size    => 1000,
+            sort    => '_doc'
+        }
     );
 
     say "Total hits: ". $scroll->total;
@@ -153,22 +159,20 @@ until there are no more matching results, much like a cursor in an SQL
 database.
 
 Unlike paginating through results (with the C<from> parameter in
-L<search()|Search::Elasticsearch::Client::2_0::Direct/search()>),
+L<search()|Search::Elasticsearch::Client::7_0::Direct/search()>),
 scrolled searches take a snapshot of the current state of the index. Even
 if you keep adding new documents to the index or updating existing documents,
 a scrolled search will only see the index as it was when the search began.
 
 This module is a helper utility that wraps the functionality of the
-L<search()|Search::Elasticsearch::Client::2_0::Direct/search()> and
-L<scroll()|Search::Elasticsearch::Client::2_0::Direct/scroll()> methods to make
+L<search()|Search::Elasticsearch::Client::7_0::Direct/search()> and
+L<scroll()|Search::Elasticsearch::Client::7_0::Direct/scroll()> methods to make
 them easier to use.
 
-B<IMPORTANT>: Deep scrolling can be expensive.  See L</DEEP SCROLLING>
-for more.
-
-This class does L<Search::Elasticsearch::Client::2_0::Role::Scroll> and
+This class does L<Search::Elasticsearch::Client::7_0::Role::Scroll> and
 L<Search::Elasticsearch::Role::Is_Sync>.
 
+
 =head1 USE CASES
 
 There are two primary use cases:
@@ -202,6 +206,7 @@ list, and return results grouped by C<thread_id>:
 
     }
 
+
 =head2 Extracting all documents
 
 Often you will want to extract all (or a subset of) documents in an index.
@@ -214,14 +219,14 @@ C<client_id>:
 
     my $scroll = $es->scroll_helper(
         index       => 'my_index',
-        search_type => 'scan',          # important!
-        size        => 500,
+        size        => 1000,
         body        => {
             query => {
                 match => {
                     client_id => 123
                 }
-            }
+            },
+            sort => '_doc'
         }
     );
 
@@ -231,42 +236,8 @@ C<client_id>:
 
 Very often the I<something> that you will want to do with these results
 involves bulk-indexing them into a new index. The easiest way to
-marry a scrolled search with bulk indexing is to use the
-L<Search::Elasticsearch::Client::2_0::Bulk/reindex()> method.
-
-=head1 DEEP SCROLLING
-
-Deep scrolling (and deep pagination) are very expensive in a distributed
-environment, and the reason they are expensive is that results need to
-be sorted in a global order.
-
-For example, if we have an index with 5 shards, and we request the first
-10 results, each shard has to return its top 10, and then the I<requesting
-node> (the node that is handling the search request) has to resort these
-50 results to return a global top 10. Now, if we request page 1,000
-(ie results 10,001 .. 10,010), then each shard has to return 10,010 results,
-and the requesting node has to sort through 50,050 results just to return
-10 of them!
-
-You can see how this can get very heavy very quickly. This is the reason that
-web search engines never return more than 1,000 results.
-
-=head2 Disable sorting for efficient scrolling
-
-The problem with deep scrolling is the sorting phase.  If we disable sorting,
-then we can happily scroll through millions of documents efficiently.  The
-way to do this is to set C<search_type> to C<scan>:
-
-    my $scroll = $es->scroll_helper(
-        search_type => 'scan',
-        size        => 500,
-    );
-
-Scanning disables sorting and will just return C<size> results from each
-shard until there are no more results to return. B<Note>: this means
-that, when querying an index with 5 shards, the scrolled search
-will pull C<size * 5> results at a time. If you have large documents or
-are memory constrained, you will need to take this into account.
+do this is to use the built-in L<Search::Elasticsearch::Client::7_0::Direct/reindex()>
+functionality provided by Elasticsearch.
 
 =head1 METHODS
 
@@ -277,17 +248,15 @@ are memory constrained, you will need to take this into account.
     my $es = Search::Elasticsearch->new(...);
     my $scroll = $es->scroll_helper(
         scroll         => '1m',            # optional
-        scroll_in_qs   => 0|1,             # optional
         %search_params
     );
 
-The L<Search::Elasticsearch::Client::2_0::Direct/scroll_helper()> method loads
-L<Search::Elasticsearch::Client::2_0::Scroll> class and calls L</new()>,
+The L<Search::Elasticsearch::Client::7_0::Direct/scroll_helper()> method loads
+L<Search::Elasticsearch::Client::7_0::Scroll> class and calls L</new()>,
 passing in any arguments.
 
-You can specify a C<scroll> duration (which defaults to C<"1m">) and
-C<scroll_in_qs> (which defaults to C<false>). Any other parameters are
-passed directly to L<Search::Elasticsearch::Client::2_0::Direct/search()>.
+You can specify a C<scroll> duration (which defaults to C<"1m">).
+Any other parameters are passed directly to L<Search::Elasticsearch::Client::7_0::Direct/search()>.
 
 The C<scroll> duration tells Elasticearch how long it should keep the scroll
 alive.  B<Note>: this duration doesn't need to be long enough to process
@@ -296,10 +265,7 @@ The expiry gets renewed for another C<scroll> period every time new
 a new batch of results is retrieved from the cluster.
 
 By default, the C<scroll_id> is passed as the C<body> to the
-L<scroll|Search::Elasticsearch::Client::2_0::Direct/scroll()> request.
-To send it in the query string instead, set C<scroll_in_qs> to a true value,
-but be aware: when querying very many indices, the scroll ID can become
-too long for intervening proxies.
+L<scroll|Search::Elasticsearch::Client::7_0::Direct/scroll()> request.
 
 The C<scroll> request uses C<GET> by default.  To use C<POST> instead,
 set L<send_get_body_as|Search::Elasticsearch::Transport/send_get_body_as> to
@@ -342,7 +308,7 @@ the buffer.
 
 The C<finish()> method clears out the buffer, sets L</is_finished()> to C<true>
 and tries to clear the C<scroll_id> on Elasticsearch.  This API is only
-supported since v0.90.5, but the call to C<clear_scroll> is wrapped in an
+supported since v0.90.6, but the call to C<clear_scroll> is wrapped in an
 C<eval> so the C<finish()> method can be safely called with any version
 of Elasticsearch.
 
@@ -393,29 +359,10 @@ How long the original search plus all subsequent batches took, in milliseconds.
 
 =over
 
-=item * L<Search::Elasticsearch::Client::2_0::Bulk/reindex()>
+=item * L<Search::Elasticsearch::Client::7_0::Direct/search()>
 
-=item * L<Search::Elasticsearch::Client::2_0::Direct/search()>
+=item * L<Search::Elasticsearch::Client::7_0::Direct/scroll()>
 
-=item * L<Search::Elasticsearch::Client::2_0::Direct/scroll()>
+=item * L<Search::Elasticsearch::Client::7_0::Direct/reindex()>
 
 =back
-
-=head1 AUTHOR
-
-Enrico Zimuel <enrico.zimuel@elastic.co>
-
-=head1 COPYRIGHT AND LICENSE
-
-This software is Copyright (c) 2020 by Elasticsearch BV.
-
-This is free software, licensed under:
-
-  The Apache License, Version 2.0, January 2004
-
-=cut
-
-__END__
-
-# ABSTRACT: A helper module for scrolled searches
-
diff --git a/lib/Search/Elasticsearch/Client/7_0/TestServer.pm b/lib/Search/Elasticsearch/Client/7_0/TestServer.pm
new file mode 100644
index 0000000..fc54259
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/7_0/TestServer.pm
@@ -0,0 +1,47 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::7_0::TestServer;
+
+use strict;
+use warnings;
+
+#===================================
+sub command_line {
+#===================================
+    my ( $class, $ts, $pid_file, $dir, $transport, $http ) = @_;
+
+    return (
+        $ts->es_home . '/bin/elasticsearch',
+        '-p',
+        $pid_file->filename,
+        map {"-E$_"} (
+            'path.data=' . $dir,
+            'network.host=127.0.0.1',
+            'cluster.name=es_test',
+            'discovery.zen.ping_timeout=1s',
+            'discovery.zen.ping.unicast.hosts=127.0.0.1:' . $ts->es_port,
+            'transport.tcp.port=' . $transport,
+            'http.port=' . $http,
+            @{ $ts->conf }
+        )
+    );
+}
+
+1
+
+# ABSTRACT: Client-specific backend for Search::Elasticsearch::TestServer
diff --git a/lib/Search/Elasticsearch/Client/8_0.pm b/lib/Search/Elasticsearch/Client/8_0.pm
new file mode 100644
index 0000000..ae53ece
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0.pm
@@ -0,0 +1,76 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0;
+
+our $VERSION='8.00';
+use Search::Elasticsearch 8.00 ();
+
+1;
+
+__END__
+
+# ABSTRACT: Thin client with full support for Elasticsearch 8.x APIs
+
+=head1 DESCRIPTION
+
+The L<Search::Elasticsearch::Client::8_0> package provides a client
+compatible with Elasticsearch 8.x.  It should be used in conjunction
+with L<Search::Elasticsearch> as follows:
+
+    $e = Search::Elasticsearch->new(
+        client => "8_0::Direct"
+    );
+
+See L<Search::Elasticsearch::Client::8_0::Direct> for documentation
+about how to use the client itself.
+
+=head1 PREVIOUS VERSIONS OF ELASTICSEARCH
+
+This version of the client supports the Elasticsearch 8.0 branch,
+which is not backwards compatible with earlier branches.
+
+If you need to talk to a version of Elasticsearch before 8.0.0, please
+install one of the following packages:
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Client::7_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::6_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::5_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::2_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::1_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::0_90>
+
+=back
diff --git a/lib/Search/Elasticsearch/Client/8_0/Async.pm b/lib/Search/Elasticsearch/Client/8_0/Async.pm
new file mode 100644
index 0000000..5bdf99d
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Async.pm
@@ -0,0 +1,76 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Async;
+
+our $VERSION='8.00';
+use Search::Elasticsearch::Client::8_0 8.00 ();
+
+1;
+
+__END__
+
+# ABSTRACT: Thin async client with full support for Elasticsearch 8.x APIs
+
+=head1 DESCRIPTION
+
+The L<Search::Elasticsearch::Client::8_0::Async> package provides a client
+compatible with Elasticsearch 8.x.  It should be used in conjunction
+with L<Search::Elasticsearch::Async> as follows:
+
+    $e = Search::Elasticsearch::Async->new(
+        client => "8_0::Direct"
+    );
+
+See L<Search::Elasticsearch::Client::8_0::Direct> for documentation
+about how to use the client itself.
+
+=head1 PREVIOUS VERSIONS OF ELASTICSEARCH
+
+This version of the client supports the Elasticsearch 7.0 branch,
+which is not backwards compatible with earlier branches.
+
+If you need to talk to a version of Elasticsearch before 7.0.0, please
+install one of the following packages:
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Client::7_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::6_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::5_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::2_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::1_0::Async>
+
+=item *
+
+L<Search::Elasticsearch::Client::0_90::Async>
+
+=back
diff --git a/lib/Search/Elasticsearch/Client/8_0/Async/Bulk.pm b/lib/Search/Elasticsearch/Client/8_0/Async/Bulk.pm
new file mode 100644
index 0000000..1f42cee
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Async/Bulk.pm
@@ -0,0 +1,498 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Async::Bulk;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::Bulk',
+    'Search::Elasticsearch::Role::Is_Async';
+
+use Search::Elasticsearch::Util qw(parse_params throw);
+use Scalar::Util qw(weaken blessed);
+use Promises qw(deferred);
+use Try::Tiny;
+use namespace::clean;
+
+has 'on_fatal' => ( is => 'lazy' );
+
+#===================================
+sub _build_on_fatal {
+#===================================
+    my $self = shift;
+    return sub {
+        warn("Fatal bulk error: @_");
+    };
+}
+
+#===================================
+sub add_action {
+#===================================
+    my $self      = shift;
+    my $buffer    = $self->_buffer;
+    my $max_size  = $self->max_size;
+    my $max_count = $self->max_count;
+    my $max_time  = $self->max_time;
+
+    my $deferred = deferred;
+    my @actions  = @_;
+
+    my $weak_add;
+    my $add = sub {
+        while (@actions) {
+            my @json = try {
+                $self->_encode_action( splice( @actions, 0, 2 ) );
+            }
+            catch {
+                $self->on_fatal->($_);
+                $deferred->reject($_);
+                ();
+            };
+            return unless @json;
+
+            push @$buffer, @json;
+
+            my $size = $self->_buffer_size;
+            $size += length($_) + 1 for @json;
+            $self->_buffer_size($size);
+
+            my $count = $self->_buffer_count( $self->_buffer_count + 1 );
+
+            next
+                unless ( $max_size and $size >= $max_size )
+                || ( $max_count and $count >= $max_count )
+                || ( $max_time  and time >= $self->_last_flush + $max_time );
+
+            return $self->flush->done( $weak_add,
+                sub { $deferred->reject(@_) } );
+        }
+        return $deferred->resolve;
+
+    };
+
+    weaken( $weak_add = $add );
+    $add->();
+    return $deferred->promise;
+
+}
+
+#===================================
+sub flush {
+#===================================
+    my $self = shift;
+
+    my $size  = $self->_buffer_size;
+    my $count = $self->_buffer_count;
+
+    $self->_last_flush(time);
+
+    unless ($size) {
+        return deferred->resolve( { items => [] } )->promise;
+    }
+
+    my @items = ( @{ $self->_buffer } );
+    $self->clear_buffer;
+
+    if ( $self->verbose ) {
+        local $| = 1;
+        print ".";
+    }
+
+    my $promise
+        = $self->es->bulk( %{ $self->_bulk_args }, body => \@items )->catch(
+        sub {
+            my $error = shift;
+            if ( $error->is( 'Cxn', 'NoNodes' ) ) {
+                push @{ $self->_buffer }, @items;
+                $self->_buffer_size( $self->_buffer_size + $size );
+                $self->_buffer_count( $self->_buffer_count + $count );
+            }
+            die $error;
+        }
+        );
+    $promise->then( sub { $self->_report( \@items, @_ ) },
+        sub { $self->on_fatal(@_) } );
+    return $promise;
+}
+
+1;
+
+# ABSTRACT: A helper module for the Bulk API
+
+=head1 SYNOPSIS
+
+    use Search::Elasticsearch::Async;
+
+    my $es   = Search::Elasticsearch::Async->new;
+    my $bulk = $es->bulk_helper(
+        index   => 'my_index',
+        type    => 'my_type'
+    );
+
+    # Index docs:
+    $promise = $bulk->index({ id => 1, source => { foo => 'bar' }});
+    $promise = $bulk->add_action( index => { id => 1, source => { foo=> 'bar' }});
+
+    # Create docs:
+    $promise = $bulk->create({ id => 1, source => { foo => 'bar' }});
+    $promise = $bulk->add_action( create => { id => 1, source => { foo=> 'bar' }});
+    $promise = $bulk->create_docs({ foo => 'bar' })
+
+    # Delete docs:
+    $promise = $bulk->delete({ id => 1});
+    $promise = $bulk->add_action( delete => { id => 1 });
+    $promise = $bulk->delete_ids(1,2,3)
+
+    # Update docs:
+    $promise = $bulk->update({ id => 1, script => '...' });
+    $promise = $bulk->add_action( update => { id => 1, script => '...' });
+
+    # Manual flush
+    $promise = $bulk->flush;
+
+=head1 DESCRIPTION
+
+This module provides an async wrapper for the L<Search::Elasticsearch::Client::8_0::Direct/bulk()>
+method which makes it easier to run multiple create, index, update or delete
+actions in a single request.
+
+The L<Search::Elasticsearch::Client::8_0::Async::Bulk> module acts as a queue, buffering up actions
+until it reaches a maximum count of actions, or a maximum size of JSON request
+body, at which point it issues a C<bulk()> request.
+
+Once you have finished adding actions, call L</flush()> to force the final
+C<bulk()> request on the items left in the queue.
+
+This class does L<Search::Elasticsearch::Client::8_0::Role::Bulk> and
+L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 CREATING A NEW INSTANCE
+
+=head2 C<new()>
+
+    $bulk = $es->bulk_helper(
+
+        index       => 'default_index',     # optional
+        type        => 'default_type',      # optional
+        %other_bulk_params                  # optional
+
+        max_count   => 1_000,               # optional
+        max_size    => 1_000_000,           # optional
+        max_time    => 6,                   # optional
+
+        verbose     => 0 | 1,               # optional
+
+        on_success  => sub {...},           # optional
+        on_error    => sub {...},           # optional
+        on_conflict => sub {...},           # optional
+        on_fatal    => sub {...},           # optional
+
+    );
+
+The C<bulk_helper> method loads L<Search::Elasticsearch::Client::8_0::Async::Bulk>,
+calls L</new()> with the specified parameters and returns a new C<$bulk> object.
+
+The C<index> and C<type> parameters provide default values for
+C<index> and C<type>, which can be overridden in each action.
+You can also pass any other values which are accepted
+by the L<bulk()|Search::Elasticsearch::Client::8_0::Direct/bulk()> method.
+
+See L</flush()> for more information about the other parameters.
+
+=head1 FLUSHING THE BUFFER
+
+=head2 C<flush()>
+
+    $promise = $bulk->flush;
+
+The C<flush()> method sends all buffered actions to Elasticsearch using
+a L<bulk()|Search::Elasticsearch::Client::8_0::Direct/bulk()> request and returns
+a L<Promise>, which is rejected if the bulk request fails or if any of
+the C<on_success>, C<on_error> or C<on_conflict> callbacks throws an
+exception, otherwise it is resolved with the items that have been flushed.
+
+=head2 Auto-flushing
+
+An automatic L</flush()> is triggered whenever the C<max_count>, C<max_size>,
+or C<max_time> threshold is breached.  This causes all actions in the buffer to be
+sent to Elasticsearch.
+
+=over
+
+=item * C<max_count>
+
+The maximum number of actions to allow before triggering a L</flush()>.
+This can be disabled by setting C<max_count> to C<0>. Defaults to
+C<1,000>.
+
+=item * C<max_size>
+
+The maximum size of JSON request body to allow before triggering a
+L</flush()>.  This can be disabled by setting C<max_size> to C<0>.  Defaults
+to C<1_000,000> bytes.
+
+=item * C<max_time>
+
+The maximum number of seconds to wait before triggering a flush.  Defaults
+to C<0> seconds, which means that it is disabled.  B<Note:> This timeout
+is only triggered when new items are added to the queue, not in the background.
+
+=back
+
+=head2 Errors when flushing
+
+There are two levels of error which can be thrown when L</flush()>
+is called, either manually or automatically.
+
+=over
+
+=item * Temporary Elasticsearch errors
+
+A C<Cxn> error like a C<NoNodes> error which indicates that your cluster is down.
+These errors do not clear the buffer, as they can be retried later on.
+These errors are reported via the C<on_fatal> callback and by rejecting
+the promise returned by L</flush()>, L</index()> etc.
+
+=item * Action errors
+
+Individual actions may fail. For instance, a C<create> action will fail
+if a document with the same C<index>, C<type> and C<id> already exists.
+These action errors are reported via L<callbacks|/Using callbacks>.
+
+=back
+
+=head2 Using callbacks
+
+By default, any I<Action errors> (see above) cause warnings to be
+written to C<STDERR>.  However, you can use the C<on_error>, C<on_conflict>
+and C<on_success> callbacks for more fine-grained control.
+
+All callbacks receive the following arguments:
+
+=over
+
+=item C<$action>
+
+The name of the action, ie C<index>, C<create>, C<update> or C<delete>.
+
+=item C<$response>
+
+The response that Elasticsearch returned for this action.
+
+=item C<$i>
+
+The index of the action, ie the first action in the flush request
+will have C<$i> set to C<0>, the second will have C<$i> set to C<1> etc.
+
+=back
+
+=head3 C<on_success>
+
+    $bulk = $e->bulk_helper->new(
+        on_success  => sub {
+            my ($action,$response,$i) = @_;
+            # do something
+        },
+    );
+
+The C<on_success> callback is called for every action that has a successful
+response.
+
+=head3 C<on_conflict>
+
+    $bulk = $e->bulk_helper->new(
+        on_conflict  => sub {
+            my ($action,$response,$i,$version) = @_;
+            # do something
+        },
+    );
+
+The C<on_conflict> callback is called for actions that have triggered
+a C<Conflict> error, eg trying to C<create> a document which already
+exists.  The C<$version> argument will contain the version number
+of the document currently stored in Elasticsearch (if found).
+
+=head3 C<on_error>
+
+    $bulk = $e->bulk_helper->new(
+        on_error  => sub {
+            my ($action,$response,$i) = @_;
+            # do something
+        },
+    );
+
+The C<on_error> callback is called for any error (unless the C<on_conflict>)
+callback has already been called).
+
+=head2 Disabling callbacks and autoflush
+
+If you want to be in control of flushing, and you just want to receive
+the raw response that Elasticsearch sends instead of using callbacks,
+then you can do so as follows:
+
+    $bulk = $e->bulk_helper->new(
+        max_count   => 0,
+        max_size    => 0,
+        on_error    => undef
+    );
+
+    $bulk->add_actions(....);
+    $bulk->flush
+         ->then(
+            sub { my $response = shift; ...},
+            sub { my $error = shift; ....}
+           )
+
+=head1 CREATE, INDEX, UPDATE, DELETE
+
+The L</add_action()>, L</create()>, L</create_docs()>, L</index()>,
+L</delete()>, L</delete_ids()> and L</update()> methods all return a Promise,
+which is resolved once the actions have been added to the queue and
+AFTER the queue has been flushed (if necessary).  It is important
+to wait for the promise to be resolved before continuing to queue more
+items, otherwise the pending requests may fill up your available memory.
+
+For instance:
+
+    use Promises qw(deferred);
+    use Scalar::Util qw(weaken);
+
+    $bulk = $es->bulk_helper;
+
+    sub bulk_index {
+        my $d = deferred;
+        my $weak_cb;
+        my $cb = sub {
+            my @docs = get_next_docs_from_somewhere();
+            unless (@docs) {
+                return $d->resolve;
+            }
+            $bulk->index(@docs)
+                 ->then(
+                      $weak_cb,
+                      sub { $d->reject(@_) }
+                   );
+        };
+        weaken ($weak_cb = $cb);
+        $cb->();
+        $d->promise->then( sub {$b->flush} );
+    }
+
+=head2 C<add_action()>
+
+    $promise = $bulk->add_action(
+        create => { ...params... },
+        index  => { ...params... },
+        update => { ...params... },
+        delete => { ...params... }
+    );
+
+The C<add_action()> method allows you to add multiple C<create>, C<index>,
+C<update> and C<delete> actions to the queue. The first value is the action
+type, and the second value is the parameters that describe that action.
+See the individual helper methods below for details.
+
+B<Note:> Parameters like C<index> or C<type> can be specified as C<index> or as
+C<_index>, so the following two lines are equivalent:
+
+    index => { index  => 'index', type  => 'type', id  => 1, source => {...}},
+    index => { _index => 'index', _type => 'type', _id => 1, source => {...}},
+
+B<Note:> The C<index> and C<type> parameters can be specified in the
+params for any action, but if not specified, will default to the C<index>
+and C<type> values specified in L</new()>.  These are required parameters:
+they must be specified either in L</new()> or in every action.
+
+=head2 C<create()>
+
+    $promise = $bulk->create(
+        { index => 'custom_index',         source => { doc body }},
+        { type  => 'custom_type', id => 1, source => { doc body }},
+        ...
+    );
+
+The C<create()> helper method allows you to add multiple C<create> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::8_0::Direct/create()>
+except that the document body should be passed as the C<source> or C<_source>
+parameter, instead of as C<body>.
+
+=head2 C<create_docs()>
+
+    $promise = $bulk->create_docs(
+        { doc body },
+        { doc body },
+        ...
+    );
+
+The C<create_docs()> helper is a shorter form of L</create()> which can be used
+when you are using the default C<index> and C<type> as set in L</new()>
+and you are not specifying a custom C<id> per document.  In this case,
+you can just pass the individual document bodies.
+
+=head2 C<index()>
+
+    $promise = $bulk->index(
+        { index => 'custom_index',         source => { doc body }},
+        { type  => 'custom_type', id => 1, source => { doc body }},
+        ...
+    );
+
+The C<index()> helper method allows you to add multiple C<index> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::8_0::Direct/index()>
+except that the document body should be passed as the C<source> or C<_source>
+parameter, instead of as C<body>.
+
+=head2 C<delete()>
+
+    $promise = $bulk->delete(
+        { index => 'custom_index', id => 1},
+        { type  => 'custom_type',  id => 2},
+        ...
+    );
+
+The C<delete()> helper method allows you to add multiple C<delete> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::8_0::Direct/delete()>.
+
+=head2 C<delete_ids()>
+
+    $bulk->delete_ids(1,2,3...)
+
+The C<delete_ids()> helper method can be used when all of the documents you
+want to delete have the default C<index> and C<type> as set in L</new()>.
+In this case, all you have to do is to pass in a list of IDs.
+
+=head2 C<update()>
+
+    $promise = $bulk->update(
+        { id            => 1,
+          doc           => { partial doc },
+          doc_as_upsert => 1
+        },
+        { id            => 2,
+          script        => { script },
+          upsert        => { upsert doc }
+        },
+        ...
+    );
+
+
+The C<update()> helper method allows you to add multiple C<update> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::8_0::Direct/update()>.
+An update can either use a I<partial doc> which gets merged with an existing
+doc (example 1 above), or can use a C<script> to update an existing doc
+(example 2 above). More information on C<script> can be found here:
+L<Search::Elasticsearch::Client::8_0::Direct/update()>.
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Async/Scroll.pm b/lib/Search/Elasticsearch/Client/8_0/Async/Scroll.pm
new file mode 100644
index 0000000..c5bcee3
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Async/Scroll.pm
@@ -0,0 +1,528 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Async::Scroll;
+
+use Moo;
+use Search::Elasticsearch::Util qw(parse_params throw);
+use Search::Elasticsearch::Async::Util qw(thenable);
+use Scalar::Util qw(weaken blessed);
+use Promises qw(deferred);
+use namespace::clean;
+
+has 'one_at_a_time' => ( is => 'ro' );
+has 'on_start'      => ( is => 'ro', clearer => '_clear_on_start' );
+has 'on_results'    => ( is => 'ro', clearer => '_clear_on_results' );
+has 'on_error'      => ( is => 'lazy', clearer => '_clear_on_error' );
+has '_guard'        => ( is => 'rwp', clearer => '_clear__guard' );
+
+with 'Search::Elasticsearch::Role::Is_Async',
+    'Search::Elasticsearch::Client::8_0::Role::Scroll';
+
+#===================================
+sub BUILDARGS {
+#===================================
+    my ( $class, $search_params ) = parse_params(@_);
+
+        my %params;
+    for (qw(es on_start on_result on_results on_error)) {
+        my $val = delete $search_params->{$_};
+        next unless defined $val;
+        $params{$_} = $val;
+    }
+
+    $params{scroll} = $search_params->{scroll} ||= '1m';
+    $params{search_params} = $search_params;
+
+    if ( $params{on_result} ) {
+        $params{on_results}    = delete $params{on_result};
+        $params{one_at_a_time} = 1;
+    }
+    elsif ( !$params{on_results} ) {
+        throw( 'Param', 'Missing required param: on_results or on_result' );
+    }
+    return \%params;
+}
+
+#===================================
+sub _build_on_error {
+#===================================
+    sub { warn "Scroll error: @_"; die @_ }
+}
+
+#===================================
+sub start {
+#===================================
+    my $self = shift;
+    $self->_set__guard($self);
+
+    $self->es->search( $self->search_params )->then(
+        sub {
+            $self->_first_results(@_);
+        }
+        )->then(
+        sub {
+            $self->_fetch_loop;
+        }
+        )->catch(
+        sub {
+            $self->on_error->(@_);
+            @_;
+        }
+        )->finally(
+        sub {
+            $self->finish;
+            $self->_clear__guard;
+        }
+        );
+}
+
+#===================================
+sub _first_results {
+#===================================
+    my ( $self, $results ) = @_;
+
+    my $total = $results->{hits}{total};
+    if (ref $total) {
+        $total = $total->{value};
+    }
+    $self->_set_total($total);
+    $self->_set_max_score( $results->{hits}{max_score} );
+    $self->_set_aggregations( $results->{aggregations} );
+    $self->_set_facets( $results->{facets} );
+    $self->_set_suggest( $results->{suggest} );
+    $self->_set_took( $results->{took} );
+    $self->_set_total_took( $results->{took} );
+
+    if ($total) {
+        $self->_set__scroll_id( $results->{_scroll_id} );
+    }
+    else {
+        $self->finish;
+    }
+
+    $self->on_start && $self->on_start->($self);
+
+    my $hits = $results->{hits}{hits};
+    return unless @$hits;
+    return $self->_push_results($hits);
+}
+
+#===================================
+sub _next_results {
+#===================================
+    my ( $self, $results ) = @_;
+    $self->_set__scroll_id( $results->{_scroll_id} );
+    $self->_set_total_took( $self->total_took + $results->{took} );
+
+    my $hits = $results->{hits}{hits};
+    return $self->finish
+        unless @$hits;
+    $self->_push_results($hits);
+}
+
+#===================================
+sub _fetch_loop {
+#===================================
+    my $self = shift;
+    my $d    = deferred;
+
+    my $weak_loop;
+    my $loop = sub {
+        if ( $self->is_finished ) {
+            return $d->resolve;
+        }
+        $self->scroll_request->then( sub { $self->_next_results(@_) } )
+            ->done( $weak_loop, sub { $d->reject(@_) } );
+    };
+    weaken( $weak_loop = $loop );
+    $loop->();
+    return $d->promise;
+}
+
+#===================================
+sub _push_results {
+#===================================
+    my $self       = shift;
+    my $it         = $self->_results_iterator(@_);
+    my $on_results = $self->on_results;
+
+    my $deferred = deferred;
+
+    my $weak_process;
+    my $process = sub {
+        while ( !$self->is_finished ) {
+            my @results  = $it->() or last;
+            my @response = $on_results->(@results);
+            my $promise  = thenable(@response) or next;
+            return $promise->done( $weak_process,
+                sub { $deferred->reject(@_) } );
+        }
+        $deferred->resolve;
+    };
+    weaken( $weak_process = $process );
+    $process->();
+    return $deferred->promise;
+}
+
+#===================================
+sub _results_iterator {
+#===================================
+    my $self    = shift;
+    my @results = @{ shift() };
+
+    $self->one_at_a_time
+        ? sub { splice @results, 0, 1 }
+        : sub { splice @results };
+}
+
+#===================================
+sub finish {
+#===================================
+    my $self = shift;
+    $self->_set_is_finished(1);
+
+    my $scroll_id = $self->_scroll_id;
+    $self->_clear_scroll_id;
+
+    if ( !$scroll_id || $self->_pid != $$ ) {
+        my $d = deferred;
+        $d->resolve();
+        return $d->promise;
+    }
+
+    my %args = ( body => { scroll_id => $scroll_id } );
+
+    $self->es->clear_scroll(%args)->then(
+        sub {
+            $self->_clear_on_start;
+            $self->_clear_on_results;
+            $self->_clear_on_error;
+        },
+        sub { }
+    );
+}
+
+1;
+
+# ABSTRACT: A helper module for scrolled searches
+
+=head1 SYNOPSIS
+
+    use Search::Elasticsearch::Async;
+
+    my $es = Search::Elasticsearch::Async->new;
+
+    my $scroll = $es->scroll_helper
+        index       => 'my_index',
+        body => {
+            size    => 1000,
+            sort    => '_doc',
+            query   => {...}
+        },
+        on_start    => \&on_start,
+        on_result   => \&on_result,
+      | on_results  => \&on_results,
+        on_error    => \&on_error
+    );
+
+    $scroll->start->then( sub {say "Done"}, sub { warn @_ } );
+
+    sub on_start {
+        my $scroll = shift;
+        say "Total hits: ". $scroll->total;
+    }
+
+    sub on_result {
+        my $doc = shift;
+        do_something($doc);
+    }
+
+    sub on_results {
+        for my $doc (@_) {
+            do_something($doc)
+        }
+    }
+
+    sub on_error {
+        my $error = shift;
+        warn "$error";
+    }
+
+=head1 DESCRIPTION
+
+A I<scrolled search> is a search that allows you to keep pulling results
+until there are no more matching results, much like a cursor in an SQL
+database.
+
+Unlike paginating through results (with the C<from> parameter in
+L<search()|Search::Elasticsearch::Client::8_0::Direct/search()>),
+scrolled searches take a snapshot of the current state of the index. Even
+if you keep adding new documents to the index or updating existing documents,
+a scrolled search will only see the index as it was when the search began.
+
+This module is a helper utility that wraps the functionality of the
+L<search()|Search::Elasticsearch::Client::8_0::Direct/search()> and
+L<scroll()|Search::Elasticsearch::Client::8_0::Direct/scroll()> methods to make
+them easier to use.
+
+This class does L<Search::Elasticsearch::Client::8_0::Role::Scroll> and
+L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 USE CASES
+
+There are two primary use cases:
+
+=head2 Pulling enough results
+
+Perhaps you want to group your results by some field, and you don't know
+exactly how many results you will need in order to return 10 grouped
+results.  With a scrolled search you can keep pulling more results
+until you have enough.  For instance, you can search emails in a mailing
+list, and return results grouped by C<thread_id>:
+
+    use Promises qw(deferred);
+
+    sub find_email_threads {
+        my (%groups,@results,$scroll);
+
+        my $d = deferred;
+
+        $scroll = $es->scroll_helper(
+            index     => 'my_emails',
+            type      => 'email',
+            body      => { query => {... some query ... }},
+            on_result => sub {
+                my $doc = shift;
+                my $thread = $doc->{_source}{thread_id};
+                unless ($groups{$thread}) {
+                    $groups{$thread} = [];
+                    push @results, $groups{$thread};
+                }
+                push @{$groups{$thread}},$doc;
+
+                # stop collecting if we have 10 results
+                if (@results == 10) {
+                    $scroll->finish;
+                }
+            }
+        );
+
+        $scroll->start->then(
+            # resolve with results if completed successfully
+            sub { $d->resolve(@results) },
+
+            # reject with error if failed
+            sub { $d->reject(@_) }
+        );
+
+        return $d->promise;
+    }
+
+=head2 Extracting all documents
+
+Often you will want to extract all (or a subset of) documents in an index.
+If you want to change your type mappings, you will need to reindex all of your
+data. Or perhaps you want to move a subset of the data in one index into
+a new dedicated index. In these cases, you don't care about sort
+order, you just want to retrieve all documents which match a query, and do
+something with them. For instance, to retrieve all the docs for a particular
+C<client_id>:
+
+    $es->scroll_helper(
+        index       => 'my_index',
+        size        => 1000,
+        body        => {
+            query => {
+                match => {
+                    client_id => 123
+                }
+            },
+            sort => '_doc'
+        },
+        on_result => sub { do_something(@_) }
+    )->start;
+
+Very often the I<something> that you will want to do with these results
+involves bulk-indexing them into a new index. The easiest way to
+do this is to use the built-in L<Search::Elasticsearch::Client::8_0::Direct/reindex()>
+functionality provided by Elasticsearch.
+
+=head1 METHODS
+
+=head2 C<new()>
+
+    use Search::Elasticsearch::Async;
+
+    my $es = Search::Elasticsearch::Async->new(...);
+    my $scroll = $es->scroll_helper(
+        scroll             => '1m',            # optional
+
+        on_result          => sub {...}        # required
+      | on_results         => sub {...}        # required
+
+        on_start           => sub {...}        # optional
+        on_error           => sub {...}        # optional
+        %search_params,
+    );
+    $scroll->start;
+
+The L<Search::Elasticsearch::Client::8_0::Direct/scroll_helper()> method loads
+L<Search::Elasticsearch::Client::8_0::Async::Scroll> class and calls L</new()>,
+passing in any arguments.
+
+You can specify a C<scroll> duration (which defaults to C<"1m">).
+Any other parameters are passed directly to L<Search::Elasticsearch::Client::8_0::Direct/search()>.
+
+The C<scroll> duration tells Elasticearch how long it should keep the scroll
+alive.  B<Note>: this duration doesn't need to be long enough to process
+all results, just long enough to process a single B<batch> of results.
+The expiry gets renewed for another C<scroll> period every time new
+a new batch of results is retrieved from the cluster.
+
+By default, the C<scroll_id> is passed as the C<body> to the
+L<scroll|Search::Elasticsearch::Client::8_0::Direct/scroll()> request.
+
+The C<scroll> request uses C<GET> by default.  To use C<POST> instead,
+set L<send_get_body_as|Search::Elasticsearch::Transport/send_get_body_as> to
+C<POST>.
+
+=head3 Callbacks
+
+You must specify either an C<on_result> callback or an C<on_results> callback.
+
+=head4 C<on_result> and C<on_results>
+
+The C<on_result> callback is called once for every result that is received.
+
+    sub on_result {
+        my $doc = shift;
+        do_something($doc);
+    }
+
+Alternatively, you can specify an C<on_results> callback which is called
+once for every set of results returned by Elasticsearch:
+
+    sub on_results {
+        for my $doc (@_) {
+            do_something($doc)
+        }
+    }
+
+If either C<on_result> or C<on_results> returns a new L<Promise>, processing
+of further results will be paused until the promise has been rejected or
+resolved.
+
+=head4 C<on_start>
+
+The C<on_start> callback is called after the first request has completed,
+at which stage the properties like C<total()>, C<aggregations()>, etc
+will have been populated.
+
+=head4 C<on_error>
+
+The C<on_error> callback is called if any error occurs.  The default
+implementation warns about the error, and rethrows it.
+
+    sub on_error { warn "Scroll error: @_"; die @_ }
+
+If you wish to handle (and surpress) certain errors, then don't call C<die()>,
+eg:
+
+    sub on_error {
+        my $error = shift;
+        if ($error =~/SomeCatchableError/) {
+            # do something to handle error
+        }
+        else {
+            # rethrow error
+            die $error;
+        }
+    }
+
+=head2 C<start()>
+
+    $scroll->start
+           ->then( \&success, \&failure );
+
+The C<start()> method starts the scroll and returns a L<Promise> which
+will be resolved when the scroll completes (or L</finish()> is called),
+or rejected if any errors remain unhandled.
+
+=head2 C<finish()>
+
+    $scroll->finish;
+
+The C<finish()> method clears out the buffer, sets L</is_finished()> to C<true>
+and tries to clear the C<scroll_id> on Elasticsearch.  This API is only
+supported since v0.90.6, but the call to C<clear_scroll> is wrapped in an
+C<eval> so the C<finish()> method can be safely called with any version
+of Elasticsearch.
+
+When the C<$scroll> instance goes out of scope, L</finish()> is called
+automatically if required.
+
+=head2 C<is_finished()>
+
+    $bool = $scroll->is_finished;
+
+A flag which returns C<true> if all results have been processed or
+L</finish()> has been called.
+
+=head1 INFO ACCESSORS
+
+The information from the original search is returned via the accessors
+below.  These values can be accessed in the C<on_start> callback:
+
+=head2 C<total>
+
+The total number of documents that matched your query.
+
+=head2 C<max_score>
+
+The maximum score of any documents in your query.
+
+=head2 C<aggregations>
+
+Any aggregations that were specified, or C<undef>
+
+=head2 C<facets>
+
+Any facets that were specified, or C<undef>
+
+=head2 C<suggest>
+
+Any suggestions that were specified, or C<undef>
+
+=head2 C<took>
+
+How long the original search took, in milliseconds
+
+=head2 C<took_total>
+
+How long the original search plus all subsequent batches took, in milliseconds.
+This value can only be checked once the scroll has completed.
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Client::8_0::Direct/search()>
+
+=item * L<Search::Elasticsearch::Client::8_0::Direct/scroll()>
+
+=back
diff --git a/lib/Search/Elasticsearch/Client/8_0/Bulk.pm b/lib/Search/Elasticsearch/Client/8_0/Bulk.pm
new file mode 100644
index 0000000..65faabd
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Bulk.pm
@@ -0,0 +1,412 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Bulk;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::Bulk',
+    'Search::Elasticsearch::Role::Is_Sync';
+use Search::Elasticsearch::Util qw(parse_params throw);
+use Try::Tiny;
+use namespace::clean;
+
+#===================================
+sub add_action {
+#===================================
+    my $self      = shift;
+    my $buffer    = $self->_buffer;
+    my $max_size  = $self->max_size;
+    my $max_count = $self->max_count;
+    my $max_time  = $self->max_time;
+
+    while (@_) {
+        my @json = $self->_encode_action( splice( @_, 0, 2 ) );
+
+        push @$buffer, @json;
+
+        my $size = $self->_buffer_size;
+        $size += length($_) + 1 for @json;
+        $self->_buffer_size($size);
+
+        my $count = $self->_buffer_count( $self->_buffer_count + 1 );
+
+        $self->flush
+            if ( $max_size and $size >= $max_size )
+            || ( $max_count and $count >= $max_count )
+            || ( $max_time  and time >= $self->_last_flush + $max_time );
+    }
+    return 1;
+}
+
+#===================================
+sub flush {
+#===================================
+    my $self = shift;
+    $self->_last_flush(time);
+
+    return { items => [] }
+        unless $self->_buffer_size;
+
+    if ( $self->verbose ) {
+        local $| = 1;
+        print ".";
+    }
+    my $buffer  = $self->_buffer;
+    my $results = try {
+        my $res = $self->es->bulk( %{ $self->_bulk_args }, body => $buffer );
+        $self->clear_buffer;
+        return $res;
+    }
+    catch {
+        my $error = $_;
+        $self->clear_buffer
+            unless $error->is( 'Cxn', 'NoNodes' );
+
+        die $error;
+    };
+    $self->_report( $buffer, $results );
+    return defined wantarray ? $results : undef;
+}
+
+1;
+
+__END__
+
+# ABSTRACT: A helper module for the Bulk API
+
+=head1 SYNOPSIS
+
+    use Search::Elasticsearch;
+
+    my $es   = Search::Elasticsearch->new;
+    my $bulk = $es->bulk_helper(
+        index   => 'my_index',
+        type    => 'my_type'
+    );
+
+    # Index docs:
+    $bulk->index({ id => 1, source => { foo => 'bar' }});
+    $bulk->add_action( index => { id => 1, source => { foo=> 'bar' }});
+
+    # Create docs:
+    $bulk->create({ id => 1, source => { foo => 'bar' }});
+    $bulk->add_action( create => { id => 1, source => { foo=> 'bar' }});
+    $bulk->create_docs({ foo => 'bar' })
+
+    # Delete docs:
+    $bulk->delete({ id => 1});
+    $bulk->add_action( delete => { id => 1 });
+    $bulk->delete_ids(1,2,3)
+
+    # Update docs:
+    $bulk->update({ id => 1, script => '...' });
+    $bulk->add_action( update => { id => 1, script => '...' });
+
+    # Manual flush
+    $bulk->flush;
+
+=head1 DESCRIPTION
+
+This module provides a wrapper for the L<Search::Elasticsearch::Client::8_0::Direct/bulk()>
+method which makes it easier to run multiple create, index, update or delete
+actions in a single request.
+
+The L<Search::Elasticsearch::Client::8_0::Bulk> module acts as a queue, buffering up actions
+until it reaches a maximum count of actions, or a maximum size of JSON request
+body, at which point it issues a C<bulk()> request.
+
+Once you have finished adding actions, call L</flush()> to force the final
+C<bulk()> request on the items left in the queue.
+
+This class does L<Search::Elasticsearch::Client::8_0::Role::Bulk> and
+L<Search::Elasticsearch::Role::Is_Sync>.
+
+=head1 CREATING A NEW INSTANCE
+
+=head2 C<new()>
+
+    my $bulk = $es->bulk_helper(
+
+        index       => 'default_index',     # optional
+        type        => 'default_type',      # optional
+        %other_bulk_params                  # optional
+
+        max_count   => 1_000,               # optional
+        max_size    => 1_000_000,           # optional
+        max_time    => 6,                   # optional
+
+        verbose     => 0 | 1,               # optional
+
+        on_success  => sub {...},           # optional
+        on_error    => sub {...},           # optional
+        on_conflict => sub {...},           # optional
+
+
+    );
+
+The C<new()> method returns a new C<$bulk> object.  You must pass your
+Search::Elasticsearch client as the C<es> argument.
+
+The C<index> and C<type> parameters provide default values for
+C<index> and C<type>, which can be overridden in each action.
+You can also pass any other values which are accepted
+by the L<bulk()|Search::Elasticsearch::Client::8_0::Direct/bulk()> method.
+
+See L</flush()> for more information about the other parameters.
+
+=head1 FLUSHING THE BUFFER
+
+=head2 C<flush()>
+
+    $result = $bulk->flush;
+
+The C<flush()> method sends all buffered actions to Elasticsearch using
+a L<bulk()|Search::Elasticsearch::Client::8_0::Direct/bulk()> request.
+
+=head2 Auto-flushing
+
+An automatic L</flush()> is triggered whenever the C<max_count>, C<max_size>,
+or C<max_time> threshold is breached.  This causes all actions in the buffer to be
+sent to Elasticsearch.
+
+=over
+
+=item * C<max_count>
+
+The maximum number of actions to allow before triggering a L</flush()>.
+This can be disabled by setting C<max_count> to C<0>. Defaults to
+C<1,000>.
+
+=item * C<max_size>
+
+The maximum size of JSON request body to allow before triggering a
+L</flush()>.  This can be disabled by setting C<max_size> to C<0>.  Defaults
+to C<1_000,000> bytes.
+
+=item * C<max_time>
+
+The maximum number of seconds to wait before triggering a flush.  Defaults
+to C<0> seconds, which means that it is disabled.  B<Note:> This timeout
+is only triggered when new items are added to the queue, not in the background.
+
+=back
+
+=head2 Errors when flushing
+
+There are two types of error which can be thrown when L</flush()>
+is called, either manually or automatically.
+
+=over
+
+=item * Temporary Elasticsearch errors
+
+A C<Cxn> error like a C<NoNodes> error which indicates that your cluster is down.
+These errors do not clear the buffer, as they can be retried later on.
+
+=item * Action errors
+
+Individual actions may fail. For instance, a C<create> action will fail
+if a document with the same C<index>, C<type> and C<id> already exists.
+These action errors are reported via L<callbacks|/Using callbacks>.
+
+=back
+
+=head2 Using callbacks
+
+By default, any I<Action errors> (see above) cause warnings to be
+written to C<STDERR>.  However, you can use the C<on_error>, C<on_conflict>
+and C<on_success> callbacks for more fine-grained control.
+
+All callbacks receive the following arguments:
+
+=over
+
+=item C<$action>
+
+The name of the action, ie C<index>, C<create>, C<update> or C<delete>.
+
+=item C<$response>
+
+The response that Elasticsearch returned for this action.
+
+=item C<$i>
+
+The index of the action, ie the first action in the flush request
+will have C<$i> set to C<0>, the second will have C<$i> set to C<1> etc.
+
+=back
+
+=head3 C<on_success>
+
+    my $bulk = $es->bulk_helper(
+        on_success  => sub {
+            my ($action,$response,$i) = @_;
+            # do something
+        },
+    );
+
+The C<on_success> callback is called for every action that has a successful
+response.
+
+=head3 C<on_conflict>
+
+    my $bulk = $es->bulk_helper(
+        on_conflict  => sub {
+            my ($action,$response,$i,$version) = @_;
+            # do something
+        },
+    );
+
+The C<on_conflict> callback is called for actions that have triggered
+a C<Conflict> error, eg trying to C<create> a document which already
+exists.  The C<$version> argument will contain the version number
+of the document currently stored in Elasticsearch (if found).
+
+=head3 C<on_error>
+
+    my $bulk = $es->bulk_helper(
+        on_error  => sub {
+            my ($action,$response,$i) = @_;
+            # do something
+        },
+    );
+
+The C<on_error> callback is called for any error (unless the C<on_conflict>)
+callback has already been called).
+
+=head2 Disabling callbacks and autoflush
+
+If you want to be in control of flushing, and you just want to receive
+the raw response that Elasticsearch sends instead of using callbacks,
+then you can do so as follows:
+
+    my $bulk = $es->bulk_helper(
+        max_count   => 0,
+        max_size    => 0,
+        on_error    => undef
+    );
+
+    $bulk->add_actions(....);
+    $response = $bulk->flush;
+
+=head1 CREATE, INDEX, UPDATE, DELETE
+
+=head2 C<add_action()>
+
+    $bulk->add_action(
+        create => { ...params... },
+        index  => { ...params... },
+        update => { ...params... },
+        delete => { ...params... }
+    );
+
+The C<add_action()> method allows you to add multiple C<create>, C<index>,
+C<update> and C<delete> actions to the queue. The first value is the action
+type, and the second value is the parameters that describe that action.
+See the individual helper methods below for details.
+
+B<Note:> Parameters like C<index> or C<type> can be specified as C<index> or as
+C<_index>, so the following two lines are equivalent:
+
+    index => { index  => 'index', type  => 'type', id  => 1, source => {...}},
+    index => { _index => 'index', _type => 'type', _id => 1, source => {...}},
+
+B<Note:> The C<index> and C<type> parameters can be specified in the
+params for any action, but if not specified, will default to the C<index>
+and C<type> values specified in L</new()>.  These are required parameters:
+they must be specified either in L</new()> or in every action.
+
+=head2 C<create()>
+
+    $bulk->create(
+        { index => 'custom_index',         source => { doc body }},
+        { type  => 'custom_type', id => 1, source => { doc body }},
+        ...
+    );
+
+The C<create()> helper method allows you to add multiple C<create> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::8_0::Direct/create()>
+except that the document body should be passed as the C<source> or C<_source>
+parameter, instead of as C<body>.
+
+=head2 C<create_docs()>
+
+    $bulk->create_docs(
+        { doc body },
+        { doc body },
+        ...
+    );
+
+The C<create_docs()> helper is a shorter form of L</create()> which can be used
+when you are using the default C<index> and C<type> as set in L</new()>
+and you are not specifying a custom C<id> per document.  In this case,
+you can just pass the individual document bodies.
+
+=head2 C<index()>
+
+    $bulk->index(
+        { index => 'custom_index',         source => { doc body }},
+        { type  => 'custom_type', id => 1, source => { doc body }},
+        ...
+    );
+
+The C<index()> helper method allows you to add multiple C<index> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::8_0::Direct/index()>
+except that the document body should be passed as the C<source> or C<_source>
+parameter, instead of as C<body>.
+
+=head2 C<delete()>
+
+    $bulk->delete(
+        { index => 'custom_index', id => 1},
+        { type  => 'custom_type',  id => 2},
+        ...
+    );
+
+The C<delete()> helper method allows you to add multiple C<delete> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::8_0::Direct/delete()>.
+
+=head2 C<delete_ids()>
+
+    $bulk->delete_ids(1,2,3...)
+
+The C<delete_ids()> helper method can be used when all of the documents you
+want to delete have the default C<index> and C<type> as set in L</new()>.
+In this case, all you have to do is to pass in a list of IDs.
+
+=head2 C<update()>
+
+    $bulk->update(
+        { id            => 1,
+          doc           => { partial doc },
+          doc_as_upsert => 1
+        },
+        { id            => 2,
+          script        => { script }
+          upsert        => { upsert doc }
+        },
+        ...
+    );
+
+
+The C<update()> helper method allows you to add multiple C<update> actions.
+It accepts the same parameters as L<Search::Elasticsearch::Client::8_0::Direct/update()>.
+An update can either use a I<partial doc> which gets merged with an existing
+doc (example 1 above), or can use a C<script> to update an existing doc
+(example 2 above). More information on C<script> can be found here:
+L<Search::Elasticsearch::Client::8_0::Direct/update()>.
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct.pm b/lib/Search/Elasticsearch/Client/8_0/Direct.pm
new file mode 100644
index 0000000..1012ee0
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct.pm
@@ -0,0 +1,1683 @@
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Client::8_0::Direct;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+
+use Search::Elasticsearch::Util qw(parse_params is_compat);
+use namespace::clean;
+
+sub _namespace {__PACKAGE__}
+
+has 'async_search'         => ( is => 'lazy', init_arg => undef );
+has 'autoscaling'          => ( is => 'lazy', init_arg => undef );
+has 'cat'                  => ( is => 'lazy', init_arg => undef );
+has 'ccr'                  => ( is => 'lazy', init_arg => undef );
+has 'cluster'              => ( is => 'lazy', init_arg => undef );
+has 'dangling_indices'     => ( is => 'lazy', init_arg => undef );
+has 'enrich'               => ( is => 'lazy', init_arg => undef );
+has 'eql'                  => ( is => 'lazy', init_arg => undef );
+has 'features'             => ( is => 'lazy', init_arg => undef );
+has 'fleet'                => ( is => 'lazy', init_arg => undef );
+has 'graph'                => ( is => 'lazy', init_arg => undef );
+has 'ilm'                  => ( is => 'lazy', init_arg => undef );
+has 'indices'              => ( is => 'lazy', init_arg => undef );
+has 'ingest'               => ( is => 'lazy', init_arg => undef );
+has 'license'              => ( is => 'lazy', init_arg => undef );
+has 'logstash'             => ( is => 'lazy', init_arg => undef );
+has 'migration'            => ( is => 'lazy', init_arg => undef );
+has 'ml'                   => ( is => 'lazy', init_arg => undef );
+has 'monitoring'           => ( is => 'lazy', init_arg => undef );
+has 'nodes'                => ( is => 'lazy', init_arg => undef );
+has 'rollup'               => ( is => 'lazy', init_arg => undef );
+has 'searchable_snapshots' => ( is => 'lazy', init_arg => undef );
+has 'security'             => ( is => 'lazy', init_arg => undef );
+has 'shutdown'             => ( is => 'lazy', init_arg => undef );
+has 'snapshot'             => ( is => 'lazy', init_arg => undef );
+has 'slm'                  => ( is => 'lazy', init_arg => undef );
+has 'sql'                  => ( is => 'lazy', init_arg => undef );
+has 'ssl'                  => ( is => 'lazy', init_arg => undef );
+has 'tasks'                => ( is => 'lazy', init_arg => undef );
+has 'transform'            => ( is => 'lazy', init_arg => undef );
+has 'watcher'              => ( is => 'lazy', init_arg => undef );
+has 'xpack'                => ( is => 'lazy', init_arg => undef );
+has 'bulk_helper_class'    => ( is => 'rw' );
+has 'scroll_helper_class'  => ( is => 'rw' );
+has '_bulk_class'          => ( is => 'lazy' );
+has '_scroll_class'        => ( is => 'lazy' );
+
+#===================================
+sub _build__bulk_class {
+#===================================
+    my $self       = shift;
+    my $bulk_class = $self->bulk_helper_class
+        || 'Client::' . $self->api_version . '::Bulk';
+    $self->_build_helper( 'bulk', $bulk_class );
+}
+
+#===================================
+sub _build__scroll_class {
+#===================================
+    my $self         = shift;
+    my $scroll_class = $self->scroll_helper_class
+        || 'Client::' . $self->api_version . '::Scroll';
+    $self->_build_helper( 'scroll', $scroll_class );
+}
+
+#===================================
+sub bulk_helper {
+#===================================
+    my ( $self, $params ) = parse_params(@_);
+    $params->{es} ||= $self;
+    $self->_bulk_class->new($params);
+}
+
+#===================================
+sub scroll_helper {
+#===================================
+    my ( $self, $params ) = parse_params(@_);
+    $params->{es} ||= $self;
+    $self->_scroll_class->new($params);
+}
+
+#===================================
+sub _build_autoscaling          { shift->_build_namespace('Autoscaling') }
+sub _build_async_search         { shift->_build_namespace('AsyncSearch') }
+sub _build_cat                  { shift->_build_namespace('Cat') }
+sub _build_ccr                  { shift->_build_namespace('CCR') }
+sub _build_cluster              { shift->_build_namespace('Cluster') }
+sub _build_dangling_indices     { shift->_build_namespace('DanglingIndices') }
+sub _build_enrich               { shift->_build_namespace('Enrich') }
+sub _build_eql                  { shift->_build_namespace('Eql') }
+sub _build_features             { shift->_build_namespace('Features') }
+sub _build_fleet                { shift->_build_namespace('Fleet') }
+sub _build_graph                { shift->_build_namespace('Graph') }
+sub _build_ilm                  { shift->_build_namespace('ILM') }
+sub _build_indices              { shift->_build_namespace('Indices') }
+sub _build_ingest               { shift->_build_namespace('Ingest') }
+sub _build_license              { shift->_build_namespace('License') }
+sub _build_logstash             { shift->_build_namespace('Logstash') }
+sub _build_migration            { shift->_build_namespace('Migration') }
+sub _build_ml                   { shift->_build_namespace('ML') }
+sub _build_monitoring           { shift->_build_namespace('Monitoring') }
+sub _build_nodes                { shift->_build_namespace('Nodes') }
+sub _build_rollup               { shift->_build_namespace('Rollup') }
+sub _build_searchable_snapshots { shift->_build_namespace('SearchableSnapshots') }
+sub _build_security             { shift->_build_namespace('Security') }
+sub _build_shutdown             { shift->_build_namespace('Shutdown') }
+sub _build_snapshot             { shift->_build_namespace('Snapshot') }
+sub _build_slm                  { shift->_build_namespace('Slm') }
+sub _build_sql                  { shift->_build_namespace('SQL') }
+sub _build_ssl                  { shift->_build_namespace('SSL') }
+sub _build_tasks                { shift->_build_namespace('Tasks') }
+sub _build_transform            { shift->_build_namespace('Transform') }
+sub _build_watcher              { shift->_build_namespace('Watcher') }
+sub _build_xpack                { shift->_build_namespace('XPack') }
+#===================================
+
+__PACKAGE__->_install_api('');
+
+1;
+
+__END__
+
+# ABSTRACT: Thin client with full support for Elasticsearch 8.x APIs
+
+=head1 SYNOPSIS
+
+Create a client:
+
+    use Search::Elasticsearch;
+    my $e = Search::Elasticsearch->new(
+        client => '8_0::Direct'
+    );
+
+Index a doc:
+
+    $e->index(
+        index   => 'my_index',
+        type    => 'blog_post',
+        id      => 123,
+        body    => {
+            title   => "Elasticsearch clients",
+            content => "Interesting content...",
+            date    => "2013-09-23"
+        }
+    );
+
+Get a doc:
+
+    $e->get(
+        index   => 'my_index',
+        type    => 'my_type',
+        id      => 123
+    );
+
+Search for docs:
+
+    $results = $e->search(
+        index   => 'my_index',
+        body    => {
+            query => {
+                match => {
+                    title => "elasticsearch"
+                }
+            }
+        }
+    );
+
+Index-level requests:
+
+    $e->indices->create( index => 'my_index' );
+    $e->indices->delete( index => 'my_index' )
+
+Ingest pipeline requests:
+
+    $e->ingest->get_pipeline( id => 'apache-logs' );
+
+Cluster-level requests:
+
+    $health = $e->cluster->health;
+
+Node-level requests:
+
+    $info  = $e->nodes->info;
+    $stats = $e->nodes->stats;
+
+Snapshot and restore:
+
+    $e->snapshot->create_repository(
+        repository => 'my_backups',
+        type       => 'fs',
+        settings   => {
+            location => '/mnt/backups'
+        }
+    );
+
+    $e->snapshot->create(
+        repository => 'my_backups',
+        snapshot   => 'backup_2014'
+    );
+
+Task management:
+
+    $e->tasks->list;
+
+`cat` debugging:
+
+    say $e->cat->allocation;
+    say $e->cat->health;
+
+Cross-cluster replication requests:
+
+    say $e->ccr->follow;
+
+Index lifecycle management requests:
+
+    say $e->ilm->put_lifecycle;
+
+=head1 DESCRIPTION
+
+The L<Search::Elasticsearch::Client::8_0::Direct> class provides the
+Elasticsearch 8.x compatible client returned by:
+
+    $e = Search::Elasticsearch->new(
+        client => "8_0::Direct"  # default
+    );
+
+It is intended to be as close as possible to the native REST API that
+Elasticsearch uses, so that it is easy to translate the
+L<Elasticsearch reference documentation|http://www.elasticsearch/guide>
+for an API to the equivalent in this client.
+
+This class provides the methods for L<document CRUD|/DOCUMENT CRUD METHODS>,
+L<bulk document CRUD|/BULK DOCUMENT CRUD METHODS> and L<search|/SEARCH METHODS>.
+It also provides access to clients for managing L<indices|/indices()>
+and the L<cluster|/cluster()>.
+
+=head1 PREVIOUS VERSIONS OF ELASTICSEARCH
+
+This version of the client supports the Elasticsearch 8.0 branch,
+which is not backwards compatible with earlier branches.
+
+If you need to talk to a version of Elasticsearch before 8.0.0, please
+install one of the following modules:
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Client::7_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::6_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::5_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::2_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::1_0>
+
+=item *
+
+L<Search::Elasticsearch::Client::0_90>
+
+=back
+
+=head1 CONVENTIONS
+
+=head2 Parameter passing
+
+Parameters can be passed to any request method as a list or as a hash
+reference. The following two statements are equivalent:
+
+    $e->search( size => 10 );
+    $e->search({size => 10});
+
+=head2 Path parameters
+
+Any values that should be included in the URL path, eg C</{index}/{type}>
+should be passed as top level parameters:
+
+    $e->search( index => 'my_index', type => 'my_type' );
+
+Alternatively, you can specify a C<path> parameter directly:
+
+    $e->search( path => '/my_index/my_type' );
+
+=head2 Query-string parameters
+
+Any values that should be included in the query string should be passed
+as top level parameters:
+
+    $e->search( size => 10 );
+
+If you pass in a C<\%params> hash, then it will be included in the
+query string parameters without any error checking. The following:
+
+    $e->search( size => 10, params => { from => 6, size => 6 })
+
+would result in this query string:
+
+    ?from=6&size=10
+
+=head2 Body parameter
+
+The request body should be passed in the C<body> key:
+
+    $e->search(
+        body => {
+            query => {...}
+        }
+    );
+
+The body can also be a UTF8-decoded string, which will be converted into
+UTF-8 bytes and passed as is:
+
+    $e->indices->analyze( body => "The quick brown fox");
+
+=head2 Boolean parameters
+
+Elasticsearch 7.0.0 and above no longer accepts truthy and falsey values for booleans.  Instead,
+it will accept only a JSON C<true> or C<false>, or the string equivalents C<"true"> or C<"false">.
+
+In the Perl client, you can use the following values:
+
+=over
+
+=item * True: C<true>, C<\1>, or a L<JSON::PP::Boolean> object.
+
+=item * False: C<false>, C<\0>, or a L<JSON::PP::Boolean> object.
+
+=back
+
+=head2 Filter path parameter
+
+Any API which returns a JSON body accepts a C<filter_path> parameter
+which will filter the JSON down to only the specified paths.  For instance,
+if you are running a search request and only want the C<total> hits and
+the C<_source> field for each hit (without the C<_id>, C<_index> etc),
+you can do:
+
+    $e->search(
+        query => {...},
+        filter_paths => [ 'hits.total', 'hits.hits._source' ]
+    );
+
+=head2 Ignore parameter
+
+Normally, any HTTP status code outside the 200-299 range will result in
+an error being thrown.  To suppress these errors, you can specify which
+status codes to ignore in the C<ignore> parameter.
+
+    $e->indices->delete(
+        index  => 'my_index',
+        ignore => 404
+    );
+
+This is most useful for
+L<Missing|Search::Elasticsearch::Error/Search::Elasticsearch::Error::Missing> errors, which
+are triggered by a C<404> status code when some requested resource does
+not exist.
+
+Multiple error codes can be specified with an array:
+
+    $e->indices->delete(
+        index  => 'my_index',
+        ignore => [404,409]
+    );
+
+=head1 CONFIGURATION
+
+=head2 C<bulk_helper_class>
+
+The class to use for the L</bulk_helper()> method. Defaults to
+L<Search::Elasticsearch::Client::8_0::Bulk>.
+
+=head2 C<scroll_helper_class>
+
+The class to use for the L</scroll_helper()> method. Defaults to
+L<Search::Elasticsearch::Client::8_0::Scroll>.
+
+=head1 GENERAL METHODS
+
+=head2 C<info()>
+
+    $info = $e->info
+
+Returns information about the version of Elasticsearch that the responding node
+is running.
+
+=head2 C<ping()>
+
+    $e->ping
+
+Pings a node in the cluster and returns C<1> if it receives a C<200>
+response, otherwise it throws an error.
+
+=head2 C<indices()>
+
+    $indices_client = $e->indices;
+
+Returns a L<Search::Elasticsearch::Client::8_0::Direct::Indices> object which can be used
+for managing indices, eg creating, deleting indices, managing mapping,
+index settings etc.
+
+=head2 C<ingest()>
+
+    $ingest_client = $e->ingest;
+
+Returns a L<Search::Elasticsearch::Client::8_0::Direct::Ingest> object which can be used
+for managing ingest pipelines.
+
+=head2 C<cluster()>
+
+    $cluster_client = $e->cluster;
+
+Returns a L<Search::Elasticsearch::Client::8_0::Direct::Cluster> object which can be used
+for managing the cluster, eg cluster-wide settings and cluster health.
+
+=head2 C<nodes()>
+
+    $node_client = $e->nodes;
+
+Returns a L<Search::Elasticsearch::Client::8_0::Direct::Nodes> object which can be used
+to retrieve node info and stats.
+
+=head2 C<snapshot()>
+
+    $snapshot_client = $e->snapshot;
+
+Returns a L<Search::Elasticsearch::Client::8_0::Direct::Snapshot> object which
+is used for managing backup repositories and creating and restoring
+snapshots.
+
+=head2 C<tasks()>
+
+    $tasks_client = $e->tasks;
+
+Returns a L<Search::Elasticsearch::Client::8_0::Direct::Tasks> object which
+is used for accessing the task management API.
+
+=head2 C<cat()>
+
+    $cat_client = $e->cat;
+
+Returns a L<Search::Elasticsearch::Client::8_0::Direct::Cat> object which can be used
+to retrieve simple to read text info for debugging and monitoring an
+Elasticsearch cluster.
+
+=head2 C<ccr()>
+
+    $ccr_client = $e->ccr;
+
+Returns a L<Search::Elasticsearch::Client::8_0::Direct::CCR> object which can be used
+to handle cross-cluster replication requests.
+
+
+=head2 C<ilm()>
+
+    $ilm_client = $e->ilm;
+
+Returns a L<Search::Elasticsearch::Client::8_0::Direct::ILM> object which can be used
+to handle index lifecycle management requests.
+
+=head1 DOCUMENT CRUD METHODS
+
+These methods allow you to perform create, index, update and delete requests
+for single documents:
+
+=head2 C<index()>
+
+    $response = $e->index(
+        index   => 'index_name',        # required
+        type    => 'type_name',         # required
+        id      => 'doc_id',            # optional, otherwise auto-generated
+
+        body    => { document }         # required
+    );
+
+The C<index()> method is used to index a new document or to reindex
+an existing document.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<if_primary_term>,
+    C<if_seq_no>,
+    C<op_type>,
+    C<parent>,
+    C<pipeline>,
+    C<refresh>,
+    C<routing>,
+    C<timeout>,
+    C<version>,
+    C<version_type>,
+    C<wait_for_active_shards>
+
+See the L<index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html>
+for more information.
+
+=head2 C<create()>
+
+    $response = $e->create(
+        index   => 'index_name',        # required
+        type    => 'type_name',         # required
+        id      => 'doc_id',            # required
+
+        body    => { document }         # required
+    );
+
+The C<create()> method works exactly like the L</index()> method, except
+that it will throw a C<Conflict> error if a document with the same
+C<index>, C<type> and C<id> already exists.
+
+Query string parameters:
+    C<consistency>,
+    C<error_trace>,
+    C<human>,
+    C<op_type>,
+    C<parent>,
+    C<refresh>,
+    C<routing>,
+    C<timeout>,
+    C<version>,
+    C<version_type>
+
+See the L<create docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-create.html>
+for more information.
+
+=head2 C<get()>
+
+    $response = $e->get(
+        index   => 'index_name',        # required
+        type    => 'type_name',         # required
+        id      => 'doc_id',            # required
+    );
+
+The C<get()> method will retrieve the document with the specified
+C<index>, C<type> and C<id>, or will throw a C<Missing> error.
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<human>,
+    C<parent>,
+    C<preference>,
+    C<realtime>,
+    C<refresh>,
+    C<routing>,
+    C<stored_fields>,
+    C<version>,
+    C<version_type>
+
+See the L<get docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html>
+for more information.
+
+=head2 C<get_source()>
+
+    $response = $e->get_source(
+        index   => 'index_name',        # required
+        type    => 'type_name',         # required
+        id      => 'doc_id',            # required
+    );
+
+The C<get_source()> method works just like the L</get()> method except that
+it returns just the C<_source> field (the value of the C<body> parameter
+in the L</index()> method) instead of returning the C<_source> field
+plus the document metadata, ie the C<_index>, C<_type> etc.
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<human>,
+    C<parent>,
+    C<preference>,
+    C<realtime>,
+    C<refresh>,
+    C<routing>,
+    C<version>,
+    C<version_type>
+
+See the L<get_source docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html>
+for more information.
+
+=head2 C<exists()>
+
+    $response = $e->exists(
+        index   => 'index_name',        # required
+        type    => 'type_name',         # required
+        id      => 'doc_id',            # required
+    );
+
+The C<exists()> method returns C<1> if a document with the specified
+C<index>, C<type> and C<id> exists, or an empty string if it doesn't.
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<human>,
+    C<parent>,
+    C<preference>,
+    C<realtime>,
+    C<refresh>,
+    C<routing>,
+    C<version>,
+    C<version_type>
+
+See the L<exists docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html>
+for more information.
+
+=head2 C<delete()>
+
+    $response = $e->delete(
+        index   => 'index_name',        # required
+        type    => 'type_name',         # required
+        id      => 'doc_id',            # required
+    );
+
+The C<delete()> method will delete the document with the specified
+C<index>, C<type> and C<id>, or will throw a C<Missing> error.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<if_primary_term>,
+    C<if_seq_no>,
+    C<parent>,
+    C<refresh>,
+    C<routing>,
+    C<timeout>,
+    C<version>,
+    C<version_type>,
+    C<wait_for_active_shards>
+
+See the L<delete docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html>
+for more information.
+
+=head2 C<update()>
+
+    $response = $e->update(
+        index   => 'index_name',        # required
+        type    => 'type_name',         # required
+        id      => 'doc_id',            # required
+
+        body    => { update }           # required
+    );
+
+The C<update()> method updates a document with the corresponding
+C<index>, C<type> and C<id> if it exists. Updates can be performed either by:
+
+=over
+
+=item * providing a partial document to be merged in to the existing document:
+
+    $response = $e->update(
+        ...,
+        body => {
+            doc => { new_field => 'new_value'},
+        }
+    );
+
+=item * with an inline script:
+
+    $response = $e->update(
+        ...,
+        body => {
+            script => {
+                source => "ctx._source.counter += incr",
+                params => { incr => 6 }
+            }
+        }
+    );
+
+=item * with an indexed script:
+
+    $response = $e->update(
+        ...,
+        body => {
+            script => {
+                id     => $id,
+                lang   => 'painless',
+                params => { incr => 6 }
+            }
+        }
+    );
+
+See L<indexed scripts|https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_indexed_scripts>
+for more information.
+
+=item * with a script stored as a file:
+
+    $response = $e->update(
+        ...,
+        body => {
+            script => {
+                file   => 'counter',
+                lang   => 'painless',
+                params => { incr => 6 }
+            }
+        }
+    );
+
+See L<scripting docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html>
+for more information.
+
+=back
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<fields>,
+    C<human>,
+    C<if_primary_term>,
+    C<if_seq_no>,
+    C<lang>,
+    C<parent>,
+    C<refresh>,
+    C<retry_on_conflict>,
+    C<routing>,
+    C<timeout>,
+    C<version>,
+    C<version_type>,
+    C<wait_for_active_shards>
+
+See the L<update docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html>
+for more information.
+
+=head2 C<termvectors()>
+
+    $results = $e->termvectors(
+        index   => $index,          # required
+        type    => $type,           # required
+
+        id      => $id,             # optional
+        body    => {...}            # optional
+    )
+
+The C<termvectors()> method retrieves term and field statistics, positions,
+offsets and payloads for the specified document, assuming that termvectors
+have been enabled.
+
+Query string parameters:
+    C<error_trace>,
+    C<field_statistics>,
+    C<fields>,
+    C<human>,
+    C<offsets>,
+    C<parent>,
+    C<payloads>,
+    C<positions>,
+    C<preference>,
+    C<realtime>,
+    C<routing>,
+    C<term_statistics>,
+    C<version>,
+    C<version_type>
+
+See the L<termvector docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-termvectors.html>
+for more information.
+
+=head1 BULK DOCUMENT CRUD METHODS
+
+The bulk document CRUD methods are used for running multiple CRUD actions
+within a single request.  By reducing the number of network requests
+that need to be made, bulk requests greatly improve performance.
+
+=head2 C<bulk()>
+
+    $response = $e->bulk(
+        index   => 'index_name',        # required if type specified
+        type    => 'type_name',         # optional
+
+        body    => [ actions ]          # required
+    );
+
+See L<Search::Elasticsearch::Client::8_0::Bulk> and L</bulk_helper()> for a helper module that makes
+bulk indexing simpler to use.
+
+The C<bulk()> method can perform multiple L</index()>, L</create()>,
+L</delete()> or L</update()> actions with a single request. The C<body>
+parameter expects an array containing the list of actions to perform.
+
+An I<action> consists of an initial metadata hash ref containing the action
+type, plus the associated metadata, eg :
+
+    { delete => { _index => 'index', _type => 'type', _id => 123 }}
+
+The C<index> and C<create> actions then expect a hashref containing
+the document itself:
+
+    { create => { _index => 'index', _type => 'type', _id => 123 }},
+    { title => "A newly created document" }
+
+And the C<update> action expects a hashref containing the update commands,
+eg:
+
+    { update => { _index => 'index', _type => 'type', _id => 123 }},
+    { script => "ctx._source.counter+=1" }
+
+
+Each action can include the same parameters that you would pass to
+the equivalent L</index()>, L</create()>, L</delete()> or L</update()>
+request, except that C<_index>, C<_type> and C<_id> must be specified with
+the preceding underscore. All other parameters can be specified with or
+without the underscore.
+
+For instance:
+
+    $response = $e->bulk(
+        index   => 'index_name',        # default index name
+        type    => 'type_name',         # default type name
+        body    => [
+
+            # create action
+            { create => {
+                _index => 'not_the_default_index',
+                _type  => 'not_the_default_type',
+                _id    => 123
+            }},
+            { title => 'Foo' },
+
+            # index action
+            { index => { _id => 124 }},
+            { title => 'Foo' },
+
+            # delete action
+            { delete => { _id => 126 }},
+
+            # update action
+            { update => { _id => 126 }},
+            { script => "ctx._source.counter+1" }
+        ]
+    );
+
+Each action is performed separately. One failed action will not
+cause the others to fail as well.
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<fields>,
+    C<human>,
+    C<pipeline>,
+    C<refresh>,
+    C<routing>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<bulk docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html>
+for more information.
+
+=head2 C<bulk_helper()>
+
+    $bulk_helper = $e->bulk_helper( @args );
+
+Returns a new instance of the class specified in the L</bulk_helper_class>,
+which defaults to L<Search::Elasticsearch::Client::8_0::Bulk>.
+
+=head2 C<mget()>
+
+    $results = $e->mget(
+        index   => 'default_index',     # optional, required when type specified
+        type    => 'default_type',      # optional
+
+        body    => { docs or ids }      # required
+    );
+
+The C<mget()> method will retrieve multiple documents with a single request.
+The C<body> consists of an array of documents to retrieve:
+
+    $results = $e->mget(
+        index   => 'default_index',
+        type    => 'default_type',
+        body    => {
+            docs => [
+                { _id => 1},
+                { _id => 2, _type => 'not_the_default_type' }
+            ]
+        }
+    );
+
+You can also pass any of the other parameters that the L</get()> request
+accepts.
+
+If you have specified an C<index> and C<type>, you can just include the
+C<ids> of the documents to retrieve:
+
+    $results = $e->mget(
+        index   => 'default_index',
+        type    => 'default_type',
+        body    => {
+            ids => [ 1, 2, 3]
+        }
+    );
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<error_trace>,
+    C<human>,
+    C<preference>,
+    C<realtime>,
+    C<refresh>,
+    C<routing>,
+    C<stored_fields>
+
+See the L<mget docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-get.html>
+for more information.
+
+=head2 C<mtermvectors()>
+
+    $results = $e->mtermvectors(
+        index   => $index,          # required if type specified
+        type    => $type,           # optional
+
+        body    => { }              # optional
+    )
+
+Runs multiple L</termvector()> requests in a single request, eg:
+
+    $results = $e->mtermvectors(
+        index   => 'test',
+        body    => {
+            docs => [
+                { _type => 'test', _id => 1, fields => ['text'] },
+                { _type => 'test', _id => 2, payloads => 1 },
+            ]
+        }
+    );
+
+Query string parameters:
+    C<error_trace>,
+    C<field_statistics>,
+    C<fields>,
+    C<human>,
+    C<ids>,
+    C<offsets>,
+    C<parent>,
+    C<payloads>,
+    C<positions>,
+    C<preference>,
+    C<realtime>,
+    C<routing>,
+    C<term_statistics>,
+    C<version>,
+    C<version_type>
+
+See the L<mtermvectors docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-termvectors.html>
+for more information.
+
+=head1 SEARCH METHODS
+
+The search methods are used for querying documents in one, more or all indices
+and of one, more or all types:
+
+=head2 C<search()>
+
+    $results = $e->search(
+        index   => 'index' | \@indices,     # optional
+        type    => 'type'  | \@types,       # optional
+
+        body    => { search params }        # optional
+    );
+
+The C<search()> method searches for matching documents in one or more
+indices.  It is just as easy to search a single index as it is to search
+all the indices in your cluster.  It can also return
+L<aggregations|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html>
+L<highlighted snippets|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-highlighting.html>
+and L<did-you-mean|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-phrase.html>
+or L<search-as-you-type|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-completion.html>
+suggestions.
+
+The I<lite> L<version of search|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html>
+allows you to specify a query string in the C<q> parameter, using the
+Lucene query string syntax:
+
+    $results = $e->search( q => 'title:(elasticsearch clients)');
+
+However, the preferred way to search is by using the
+L<Query DSL|http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html>
+to create a query, and passing that C<query> in the
+L<request body|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html>:
+
+    $results = $e->search(
+        body => {
+            query => {
+                match => { title => 'Elasticsearch clients'}
+            }
+        }
+    );
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<allow_no_indices>,
+    C<allow_partial_search_results>,
+    C<analyze_wildcard>,
+    C<analyzer>,
+    C<batched_reduce_size>,
+    C<default_operator>,
+    C<df>,
+    C<docvalue_fields>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<explain>,
+    C<from>,
+    C<human>,
+    C<ignore_throttled>,
+    C<ignore_unavailable>,
+    C<lenient>,
+    C<max_concurrent_shard_requests>,
+    C<pre_filter_shard_size>,
+    C<preference>,
+    C<q>,
+    C<request_cache>,
+    C<rest_total_hits_as_int>,
+    C<routing>,
+    C<scroll>,
+    C<search_type>,
+    C<seq_no_primary_term>,
+    C<size>,
+    C<sort>,
+    C<stats>,
+    C<stored_fields>,
+    C<suggest_field>,
+    C<suggest_mode>,
+    C<suggest_size>,
+    C<suggest_text>,
+    C<terminate_after>,
+    C<timeout>,
+    C<track_scores>,
+    C<track_total_hits>,
+    C<typed_keys>,
+    C<version>
+
+See the L<search reference|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html>
+for more information.
+
+Also see L<Search::Elasticsearch::Transport/send_get_body_as>.
+
+=head2 C<count()>
+
+    $results = $e->count(
+        index   => 'index' | \@indices,     # optional
+        type    => 'type'  | \@types,       # optional
+
+        body    => { query }                # optional
+    )
+
+The C<count()> method returns the total count of all documents matching the
+query:
+
+    $results = $e->count(
+        body => {
+            query => {
+                match => { title => 'Elasticsearch clients' }
+            }
+        }
+    );
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<analyze_wildcard>,
+    C<analyzer>,
+    C<default_operator>,
+    C<df>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_throttled>,
+    C<ignore_unavailable>,
+    C<lenient>,
+    C<lowercase_expanded_terms>
+    C<min_score>,
+    C<preference>,
+    C<q>,
+    C<routing>,
+    C<terminate_after>
+
+See the L<count docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html>
+for more information.
+
+=head2 C<search_template()>
+
+    $results = $e->search_template(
+        index   => 'index' | \@indices,     # optional
+        type    => 'type'  | \@types,       # optional
+
+        body    => { search params }        # required
+    );
+
+Perform a search by specifying a template (either predefined or defined
+within the C<body>) and parameters to use with the template, eg:
+
+    $results = $e->search_template(
+        body => {
+            source => {
+                query => {
+                    match => {
+                        "{{my_field}}" => "{{my_value}}"
+                    }
+                },
+                size => "{{my_size}}"
+            },
+            params => {
+                my_field => 'foo',
+                my_value => 'bar',
+                my_size  => 6
+            }
+        }
+    );
+
+See the L<search template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<explain>,
+    C<human>,
+    C<ignore_throttled>,
+    C<ignore_unavailable>,
+    C<preference>,
+    C<profile>,
+    C<rest_total_hits_as_int>,
+    C<scroll>,
+    C<search_type>,
+    C<typed_keys>
+
+=head2 C<render_search_template()>
+
+    $response = $e->render_search_template(
+        id   => 'id',           # optional
+        body => { template }    # optional
+    );
+
+Renders the template, filling in the passed-in parameters and returns the resulting JSON, eg:
+
+    $results = $e->render_search_template(
+        body => {
+            source => {
+                query => {
+                    match => {
+                        "{{my_field}}" => "{{my_value}}"
+                    }
+                },
+                size => "{{my_size}}"
+            },
+            params => {
+                my_field => 'foo',
+                my_value => 'bar',
+                my_size  => 6
+            }
+        }
+    );
+
+See the L<search template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html>
+for more information.
+
+=head2 C<scroll()>
+
+    $results = $e->scroll(
+        scroll      => '1m',
+        body => {
+            scroll_id   => $id
+        }
+    );
+
+When a L</search()> has been performed with the
+C<scroll> parameter, the C<scroll()>
+method allows you to keep pulling more results until the results
+are exhausted.
+
+See L</scroll_helper()> and L<Search::Elasticsearch::Client::8_0::Scroll> for a helper utility
+which makes managing scroll requests much easier.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<rest_total_hits_as_int>,
+    C<scroll>,
+    C<scroll_id>
+
+See the L<scroll docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html>
+and the L<search_type docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search.html/search-request-search-type.html>
+for more information.
+
+=head2 C<clear_scroll()>
+
+    $response = $e->clear_scroll(
+        body => {
+            scroll_id => $id | \@ids    # required
+        }
+    );
+
+The C<clear_scroll()> method can clear unfinished scroll requests, freeing
+up resources on the server.
+
+=head2 C<scroll_helper()>
+
+    $scroll_helper = $e->scroll_helper( @args );
+
+Returns a new instance of the class specified in the L</scroll_helper_class>,
+which defaults to L<Search::Elasticsearch::Client::8_0::Scroll>.
+
+
+=head2 C<msearch()>
+
+    $results = $e->msearch(
+        index   => 'default_index' | \@indices,     # optional
+        type    => 'default_type'  | \@types,       # optional
+
+        body    => [ searches ]                     # required
+    );
+
+The C<msearch()> method allows you to perform multiple searches in a single
+request.  Similar to the L</bulk()> request, each search request in the
+C<body> consists of two hashes: the metadata hash then the search request
+hash (the same data that you'd specify in the C<body> of a L</search()>
+request).  For instance:
+
+    $results = $e->msearch(
+        index   => 'default_index',
+        type    => ['default_type_1', 'default_type_2'],
+        body => [
+            # uses defaults
+            {},
+            { query => { match_all => {} }},
+
+            # uses a custom index
+            { index => 'not_the_default_index' },
+            { query => { match_all => {} }}
+        ]
+    );
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<max_concurrent_searches>,
+    C<max__concurrent_shard_requests>,
+    C<pre_filter_shard_size>,
+    C<rest_total_hits_as_int>,
+    C<search_type>,
+    C<typed_keys>
+
+See the L<msearch docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html>
+for more information.
+
+=head2 C<msearch_template()>
+
+    $results = $e->msearch_template(
+        index   => 'default_index' | \@indices,     # optional
+        type    => 'default_type'  | \@types,       # optional
+
+        body    => [ search_templates ]             # required
+    );
+
+The C<msearch_template()> method allows you to perform multiple searches in a single
+request using search templates.  Similar to the L</bulk()> request, each search
+request in the C<body> consists of two hashes: the metadata hash then the search request
+hash (the same data that you'd specify in the C<body> of a L</search()>
+request).  For instance:
+
+    $results = $e->msearch(
+        index   => 'default_index',
+        type    => ['default_type_1', 'default_type_2'],
+        body => [
+            # uses defaults
+            {},
+            { source => { query => { match => { user => "{{user}}" }}} params => { user => 'joe' }},
+
+            # uses a custom index
+            { index => 'not_the_default_index' },
+            { source => { query => { match => { user => "{{user}}" }}} params => { user => 'joe' }},
+        ]
+    );
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<max_concurrent_searches>,
+    C<rest_total_hits_as_int>,
+    C<search_type>,
+    C<typed_keys>
+
+See the L<msearch-template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html>
+for more information.
+
+=head2 C<explain()>
+
+    $response = $e->explain(
+        index   => 'my_index',  # required
+        type    => 'my_type',   # required
+        id      => 123,         # required
+
+        body    => { search }   # required
+    );
+
+The C<explain()> method explains why the specified document did or
+did not match a query, and how the relevance score was calculated.
+For instance:
+
+    $response = $e->explain(
+        index   => 'my_index',
+        type    => 'my_type',
+        id      => 123,
+        body    => {
+            query => {
+                match => { title => 'Elasticsearch clients' }
+            }
+        }
+    );
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<analyze_wildcard>,
+    C<analyzer>,
+    C<default_operator>,
+    C<df>,
+    C<error_trace>,
+    C<human>,
+    C<lenient>,
+    C<parent>,
+    C<preference>,
+    C<q>,
+    C<routing>,
+    C<stored_fields>
+
+See the L<explain docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-explain.html>
+for more information.
+
+=head2 C<field_caps()>
+
+    $response = $e->field_caps(
+        index   => 'index'   | \@indices,   # optional
+        body    => { filters }              # optional
+    );
+
+The C<field-caps> API returns field types and abilities, merged across indices.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<fields>,
+    C<human>,
+    C<ignore_unavailable>
+
+See the L<field-caps docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-field-caps.html>
+for more information.
+
+=head2 C<search_shards()>
+
+    $response = $e->search_shards(
+        index   => 'index' | \@indices,     # optional
+    )
+
+The C<search_shards()> method returns information about which shards on
+which nodes will execute a search request.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<local>,
+    C<preference>,
+    C<routing>
+
+See the L<search-shards docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/search-shards.html>
+for more information.
+
+=head2 C<rank_eval()>
+
+    $result = $e->rank_eval(
+        index   => 'index' | \@indices,     # optional
+        body    => {...}                    # required
+    );
+
+The ranking evaluation API provides a way to execute test cases to determine whether search results
+are improving or worsening.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<filter_path>,
+    C<human>,
+    C<ignore_unavailable>
+
+See the L<rank-eval docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/search-rank-eval.html>
+for more information.
+
+=head1 CRUD-BY-QUERY METHODS
+
+=head2 C<delete_by_query()>
+
+    $response = $e->delete_by_query(
+        index   => 'index' | \@indices,     # optional
+        type    => 'type'  | \@types,       # optional,
+        body    => { delete-by-query }      # required
+    );
+
+The C<delete_by_query()> method deletes all documents which match the specified query.
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<allow_no_indices>,
+    C<analyze_wildcard>,
+    C<analyzer>,
+    C<conflicts>,
+    C<default_operator>,
+    C<df>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<from>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<lenient>,
+    C<preference>,
+    C<q>,
+    C<refresh>,
+    C<request_cache>,
+    C<requests_per_second>,
+    C<routing>,
+    C<scroll>,
+    C<scroll_size>,
+    C<search_timeout>,
+    C<search_type>,
+    C<size>,
+    C<slices>,
+    C<sort>,
+    C<stats>,
+    C<terminate_after>,
+    C<version>,
+    C<timeout>,
+    C<wait_for_active_shards>,
+    C<wait_for_completion>
+
+See the L<delete-by-query docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html>
+for more information.
+
+=head2 C<delete_by_query_rethrottle()>
+
+    $response = $e->delete_by_query_rethrottle(
+        task_id             => 'id'         # required
+        requests_per_second => num
+    );
+
+The C<delete_by_query_rethrottle()> API is used to dynamically update the throtting
+of an existing delete-by-query request, identified by C<task_id>.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<requests_per_second>
+
+See the L<delete-by-query-rethrottle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html>
+for more information.
+
+=head2 C<reindex()>
+
+    $response = $e->reindex(
+        body => { reindex }     # required
+    );
+
+The C<reindex()> API is used to index documents from one index or multiple indices
+to a new index.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<refresh>,
+    C<requests_per_second>,
+    C<slices>,
+    C<timeout>,
+    C<wait_for_active_shards>,
+    C<wait_for_completion>
+
+See the L<reindex docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html>
+for more information.
+
+=head2 C<reindex_rethrottle()>
+
+    $response = $e->delete_by_query_rethrottle(
+        task_id => 'id',            # required
+        requests_per_second => num
+    );
+
+The C<reindex_rethrottle()> API is used to dynamically update the throtting
+of an existing reindex request, identified by C<task_id>.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<requests_per_second>
+
+See the L<reindex-rethrottle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html>
+for more information.
+
+
+=head2 C<update_by_query()>
+
+    $response = $e->update_by_query(
+        index   => 'index' | \@indices,     # optional
+        type    => 'type'  | \@types,       # optional,
+        body    => { update-by-query }      # optional
+    );
+
+The C<update_by_query()> API is used to bulk update documents from one index or
+multiple indices using a script.
+
+Query string parameters:
+    C<_source>,
+    C<_source_excludes>,
+    C<_source_includes>,
+    C<allow_no_indices>,
+    C<analyze_wildcard>,
+    C<analyzer>,
+    C<conflicts>,
+    C<default_operator>,
+    C<df>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<from>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<lenient>,
+    C<pipeline>,
+    C<preference>,
+    C<q>,
+    C<refresh>,
+    C<request_cache>,
+    C<requests_per_second>,
+    C<routing>,
+    C<scroll>,
+    C<scroll_size>,
+    C<search_timeout>,
+    C<search_type>,
+    C<size>,
+    C<slices>,
+    C<sort>,
+    C<stats>,
+    C<terminate_after>,
+    C<timeout>,
+    C<version>,
+    C<version_type>,
+    C<wait_for_active_shards>,
+    C<wait_for_completion>
+
+See the L<update_by_query docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html>
+for more information.
+
+=head2 C<update_by_query_rethrottle()>
+
+    $response = $e->update_by_query_rethrottle(
+        task_id             => 'id'         # required
+        requests_per_second => num
+    );
+
+The C<update_by_query_rethrottle()> API is used to dynamically update the throtting
+of an existing update-by-query request, identified by C<task_id>.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<requests_per_second>
+
+See the L<update-by-query-rethrottle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html>
+for more information.
+
+
+=head1 INDEXED SCRIPT METHODS
+
+Elasticsearch allows you to store scripts in the cluster state
+and reference them by id. The methods to manage indexed scripts are as follows:
+
+=head2 C<put_script()>
+
+    $result  = $e->put_script(
+        id      => 'id',       # required
+        context => $context,   # optional
+        body    => { script }  # required
+    );
+
+The C<put_script()> method is used to store a script in the cluster state. For instance:
+
+    $result  = $e->put_scripts(
+        id   => 'hello_world',
+        body => {
+          script => {
+            lang   => 'painless',
+            source => q(return "hello world")
+          }
+        }
+    );
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+
+See the L<indexed scripts docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_indexed_scripts> for more.
+
+=head2 C<get_script()>
+
+    $script = $e->get_script(
+        id   => 'id',       # required
+    );
+
+Retrieve the indexed script from the cluster state.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>
+
+See the L<indexed scripts docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_indexed_scripts> for more.
+
+=head2 C<delete_script()>
+
+    $script = $e->delete_script(
+        id   => 'id',       # required
+    );
+
+Delete the indexed script from the cluster state.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<indexed scripts docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html#_indexed_scripts> for more.
+
+=head2 C<scripts_painless_execute()>
+
+    $result = $e->scripts_painless_execute(
+        body => {...}   # required
+    );
+
+The Painless execute API allows an arbitrary script to be executed and a result to be returned.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<painless execution docs|https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-execute-api.html> for more.
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/AsyncSearch.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/AsyncSearch.pm
new file mode 100644
index 0000000..8f774a2
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/AsyncSearch.pm
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::AsyncSearch;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('async_search');
+
+1;
+
+__END__
+
+# ABSTRACT: Async Search feature of Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+The full documentation for Async Search is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/async-search.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    $response = $es->async_search->get(
+        id => $id  # required
+    )
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Autoscaling.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Autoscaling.pm
new file mode 100644
index 0000000..86ce3c9
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Autoscaling.pm
@@ -0,0 +1,43 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Autoscaling;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('autoscaling');
+
+1;
+
+__END__
+
+# ABSTRACT: Autoscaling feature of Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+The full documentation for Autoscaling is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/autoscaling-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    $response = $es->autoscaling->get();
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/CCR.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/CCR.pm
new file mode 100644
index 0000000..7fac733
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/CCR.pm
@@ -0,0 +1,228 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::CCR;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('ccr');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing cross-cluster replication APIs for Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+This module provides methods to use the cross-cluster replication feature.
+
+The full documentation for CCR is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    $response = $es->ccr->follow(
+        index   => $index,  # required
+        body    => {...}    # required
+    )
+
+The C<follow()> method creates a new follower index that is configured to follow the referenced leader index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<wait_for_active_shards>
+
+See the L<CCR follow docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-put-follow.html>
+for more information.
+
+
+=head2 C<pause_follow()>
+
+    $response = $es->ccr->pause_follow(
+        index   => $index,  # required
+    )
+
+The C<pause_follow()> method pauses following of an index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR pause follow docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-post-pause-follow.html>
+for more information.
+
+
+=head2 C<resume_follow()>
+
+    $response = $es->ccr->resume_follow(
+        index   => $index,  # required
+    )
+
+The C<resume_follow()> method resumes following of an index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR resume follow docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-post-resume-follow.html>
+for more information.
+
+
+=head2 C<unfollow()>
+
+    $response = $es->ccr->unfollow(
+        index   => $index,  # required
+    )
+
+The C<unfollow()> method converts a follower index into a normal index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR unfollow docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-post-unfollow.html>
+for more information.
+
+
+=head2 C<forget_follower()>
+
+    $response = $es->ccr->forget_follower(
+        index   => $index,  # required
+    )
+
+The C<forget_follower()> method removes the follower retention leases from the leader.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR forget_follower docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-post-forget-follower.html>
+for more information.
+
+=head1 STATS METHODS
+
+=head2 C<stats()>
+
+    $response = $es->ccr->stats()
+
+The C<stats()> method returns all stats related to cross-cluster replication.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR stats docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-stats.html>
+for more information.
+
+=head2 C<follow_stats()>
+
+    $response = $es->ccr->follow_stats(
+        index   => $index | \@indices,  # optional
+    )
+
+The C<follow_stats()> method returns shard-level stats about follower indices.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR follow stats docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-follow-stats.html>
+for more information.
+
+
+=head2 C<follow_info()>
+
+    $response = $es->ccr->follow_info(
+        index   => $index | \@indices,  # optional
+    )
+
+The C<follow_info()> method returns the parameters and the status for each follower index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR follow info docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-follow-info.html>
+for more information.
+
+=head1 AUTO-FOLLOW METHODS
+
+=head2 C<put_auto_follow_pattern()>
+
+    $response = $es->ccr->put_auto_follow_pattern(
+        name    => $name    # required
+    )
+
+The C<put_auto_follow_pattern()> method creates a new named collection of auto-follow patterns against the remote cluster specified in the request body.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR put auto follow pattern docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-put-auto-follow-pattern.html>
+for more information.
+
+
+=head2 C<get_auto_follow_pattern()>
+
+    $response = $es->ccr->get_auto_follow_pattern(
+        name    => $name    # optional
+    )
+
+The C<get_auto_follow_pattern()> method retrieves a named collection of auto-follow patterns, or all patterns.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR get auto follow pattern docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-get-auto-follow-pattern.html>
+for more information.
+
+=head2 C<delete_auto_follow_pattern()>
+
+    $response = $es->ccr->delete_auto_follow_pattern(
+        name    => $name    # required
+    )
+
+The C<delete_auto_follow_pattern()> method deletes a named collection of auto-follow patterns.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<CCR delete auto follow pattern docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-delete-auto-follow-pattern.html>
+for more information.
+
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Cat.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Cat.pm
new file mode 100644
index 0000000..930addd
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Cat.pm
@@ -0,0 +1,513 @@
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Client::8_0::Direct::Cat;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('cat');
+
+#===================================
+sub help {
+#===================================
+    my ( $self, $params ) = parse_params(@_);
+    $params->{help} = 1;
+    my $defn = $self->api->{'cat.help'};
+    $self->perform_request( $defn, $params );
+}
+
+#===================================
+around 'perform_request' => sub {
+#===================================
+    my $orig = shift;
+    my $self = shift;
+    my ( $defn, $params ) = parse_params(@_);
+    if ( $params->{help} && $params->{help} ne 'false' ) {
+        $defn = { %$defn, parts => {} };
+    }
+
+    return $orig->( $self, $defn, $params );
+
+};
+
+1;
+
+__END__
+
+# ABSTRACT: A client for running cat debugging requests
+
+=head1 DESCRIPTION
+
+The C<cat> API in Elasticsearch provides information about your
+cluster and indices in a simple, easy to read text format, intended
+for human consumption.
+
+These APIs have a number of parameters in common:
+
+=over
+
+=item * C<help>
+
+Returns help about the API, eg:
+
+    say $e->cat->allocation(help => 1);
+
+=item * C<v>
+
+Includes the column headers in the output:
+
+    say $e->cat->allocation(v => 1);
+
+=item * C<h>
+
+Accepts a list of column names to be output, eg:
+
+    say $e->cat->indices(h => ['health','index']);
+
+=item * C<bytes>
+
+Formats byte-based values as bytes (C<b>), kilobytes (C<k>), megabytes
+(C<m>) or gigabytes (C<g>)
+
+
+=back
+
+It does L<Search::Elasticsearch::Role::Client::Direct>.
+
+=head1 METHODS
+
+=head2 C<help()>
+
+    say $e->cat->help;
+
+Returns the list of supported C<cat> APIs
+
+=head2 C<aliases()>
+
+    say $e->cat->aliases(
+        name => 'name' | \@names    # optional
+    );
+
+Returns information about index aliases, optionally limited to the specified
+index/alias names.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat aliases docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-aliases.html>
+for more information.
+
+=head2 C<allocation()>
+
+    say $e->cat->allocation(
+        node_id => 'node' | \@nodes    # optional
+    );
+
+Provides a snapshot of how shards have located around the cluster and the
+state of disk usage.
+
+Query string parameters:
+    C<bytes>,
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat allocation docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-allocation.html>
+for more information.
+
+=head2 C<count()>
+
+    say $e->cat->count(
+        index => 'index' | \@indices    # optional
+    );
+
+Provides quick access to the document count of the entire cluster, or
+individual indices.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat count docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-count.html>
+for more information.
+
+=head2 C<fielddata()>
+
+    say $e->cat->fielddata(
+        fields => 'field' | \@fields    # optional
+    );
+
+Shows the amount of memory used by each of the specified `fields` (or all
+fields) loaded into fielddata.
+
+Query string parameters:
+    C<bytes>,
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat fielddata docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-fielddata.html>
+for more information.
+
+=head2 C<health()>
+
+    say $e->cat->health();
+
+Provides a snapshot of how shards have located around the cluster and the
+state of disk usage.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<ts>,
+    C<s>,
+    C<v>
+
+See the L<cat health docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-health.html>
+for more information.
+
+=head2 C<indices()>
+
+    say $e->cat->indices(
+        index => 'index' | \@indices    # optional
+    );
+
+Provides a summary of index size and health for the whole cluster
+or individual indices
+
+Query string parameters:
+    C<bytes>,
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<health>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<pri>,
+    C<s>,
+    C<v>
+
+See the L<cat indices docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-indices.html>
+for more information.
+
+=head2 C<master()>
+
+    say $e->cat->master();
+
+Displays the master’s node ID, bound IP address, and node name.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat master docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-master.html>
+for more information.
+
+=head2 C<nodeattrs()>
+
+    say $e->cat->nodeattrs();
+
+Returns the node attributes set per node.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat nodeattrs docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-nodeattrs.html>
+for more information.
+
+=head2 C<nodes()>
+
+    say $e->cat->nodes();
+
+Provides a snapshot of all of the nodes in your cluster.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat nodes docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-nodes.html>
+for more information.
+
+=head2 C<pending_tasks()>
+
+    say $e->cat->pending_tasks();
+
+Returns any cluster-level tasks which are queued on the master.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<h>,
+    C<help>,
+    C<s>,
+    C<v>
+
+See the L<cat pending-tasks docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-pending-tasks.html>
+for more information.
+
+=head2 C<plugins()>
+
+    say $e->cat->plugins();
+
+Returns information about plugins installed on each node.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<h>,
+    C<help>,
+    C<s>,
+    C<v>
+
+See the L<cat plugins docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-plugins.html>
+for more information.
+
+=head2 C<recovery()>
+
+    say $e->cat->recovery(
+        index => 'index' | \@indices    # optional
+    );
+
+Provides a view of shard replication. It will show information
+anytime data from at least one shard is copying to a different node.
+It can also show up on cluster restarts. If your recovery process seems
+stuck, try it to see if there’s any movement using C<recovery()>.
+
+Query string parameters:
+    C<bytes>,
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat recovery docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-recovery.html>
+for more information.
+
+=head2 C<repositories()>
+
+    say $e->cat->repositories()
+
+Provides a list of registered snapshot repositories.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat repositories docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-repositories.html>
+for more information.
+
+=head2 C<segments()>
+
+    say $e->cat->segments(
+        index => 'index' | \@indices    # optional
+    );
+
+Provides low level information about the segments in the shards of an index.
+
+Query string parameters:
+    C<bytes>,
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<s>,
+    C<v>
+
+See the L<cat shards docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-segments.html>
+for more information.
+
+=head2 C<shards()>
+
+    say $e->cat->shards(
+        index => 'index' | \@indices    # optional
+    );
+
+Provides a detailed view of what nodes contain which shards, the state and
+size of each shard.
+
+Query string parameters:
+    C<bytes>,
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat shards docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-shards.html>
+for more information.
+
+=head2 C<snapshots()>
+
+    say $e->cat->snapshots(
+        repository => 'repository' | \@repositories # optional
+    )
+
+Provides a list of all snapshots that belong to the specified repositories.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat snapshots docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-snapshots.html>
+for more information.
+
+=head2 C<tasks()>
+
+    say $e->cat->tasks()
+
+Provides a list of node-level tasks.
+
+Query string parameters:
+    C<actions>,
+    C<detailed>,
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<node_id>,
+    C<parent_node>,
+    C<parent_task>,
+    C<s>,
+    C<v>
+
+See the L<cat tasks docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
+for more information.
+
+=head2 C<templates()>
+
+    say $e->cat->templates(
+        name => $name # optional
+    )
+
+Provides a list of index templates.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<s>,
+    C<v>
+
+See the L<cat templates docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/templates.html>
+for more information.
+
+
+=head2 C<thread_pool()>
+
+    say $e->cat->thread_pool(
+        index => 'index' | \@indices    # optional
+    );
+
+Shows cluster wide thread pool statistics per node. By default the C<active>,
+C<queue> and C<rejected> statistics are returned for the C<bulk>, C<index> and
+C<search> thread pools.
+
+Query string parameters:
+    C<error_trace>,
+    C<format>,
+    C<h>,
+    C<help>,
+    C<human>,
+    C<local>,
+    C<master_timeout>,
+    C<size>,
+    C<s>,
+    C<v>
+
+See the L<cat thread_pool docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cat-thread-pool.html>
+for more information.
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Cluster.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Cluster.pm
new file mode 100644
index 0000000..7c965ca
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Cluster.pm
@@ -0,0 +1,250 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Cluster;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+__PACKAGE__->_install_api('cluster');
+
+1;
+
+__END__
+
+# ABSTRACT: A client for running cluster-level requests
+
+=head1 DESCRIPTION
+
+This module provides methods to make cluster-level requests, such as
+getting and setting cluster-level settings, manually rerouting shards,
+and retrieving for monitoring purposes.
+
+It does L<Search::Elasticsearch::Role::Client::Direct>.
+
+=head1 METHODS
+
+=head2 C<health()>
+
+    $response = $e->cluster->health(
+        index   => 'index' | \@indices  # optional
+    );
+
+The C<health()> method is used to retrieve information about the cluster
+health, returning C<red>, C<yellow> or C<green> to indicate the state
+of the cluster, indices or shards.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<level>,
+    C<local>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>,
+    C<wait_for_events>,
+    C<wait_for_no_initializing_shards>,
+    C<wait_for_no_relocating_shards>,
+    C<wait_for_nodes>,
+    C<wait_for_status>
+
+See the L<cluster health docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html>
+for more information.
+
+=head2 C<stats()>
+
+    $response = $e->cluster->stats(
+        node_id => 'node' | \@nodes     # optional
+    );
+
+Returns high-level cluster stats, optionally limited to the listed nodes.
+
+Query string parameters:
+    C<error_trace>,
+    C<flat_settings>,
+    C<human>,
+    C<timeout>
+
+See the L<cluster stats docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-stats.html>
+for more information.
+
+=head2 C<get_settings()>
+
+    $response = $e->cluster->get_settings()
+
+The C<get_settings()> method is used to retrieve cluster-wide settings that
+have been set with the L</put_settings()> method.
+
+Query string parameters:
+    C<error_trace>,
+    C<flat_settings>,
+    C<human>,
+    C<include_defaults>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<cluster settings docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-update-settings.html>
+for more information.
+
+=head2 C<put_settings()>
+
+    $response = $e->cluster->put_settings( %settings );
+
+The C<put_settings()> method is used to set cluster-wide settings, either
+transiently (which don't survive restarts) or permanently (which do survive
+restarts).
+
+For instance:
+
+    $response = $e->cluster->put_settings(
+        body => {
+            transient => { "discovery.zen.minimum_master_nodes" => 6 }
+        }
+    );
+
+Query string parameters:
+    C<error_trace>,
+    C<flat_settings>,
+    C<human>
+
+See the L<cluster settings docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-update-settings.html>
+ for more information.
+
+=head2 C<state()>
+
+    $response = $e->cluster->state(
+        metric => $metric | \@metrics   # optional
+        index  => $index  | \@indices   # optional
+    );
+
+The C<state()> method returns the current cluster state from the master node,
+or from the responding node if C<local> is set to C<true>.
+
+It returns all metrics by default, but these can be limited to any of:
+    C<_all>,
+    C<blocks>,
+    C<metadata>,
+    C<nodes>,
+    C<routing_table>
+
+Metrics for indices can be limited to particular indices with the C<index>
+parameter.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<flat_settings>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<local>,
+    C<master_timeout>,
+    C<wait_for_metadata_version>,
+    C<wait_for_timeout>
+
+See the L<cluster state docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-state.html>
+for more information.
+
+=head2 C<allocation_explain()>
+
+    $response = $e->cluster->allocation_explain(
+        body => { ... shard selectors ...}  # optional
+    );
+
+Returns information about why a shard is allocated or unallocated or why.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<include_disk_info>,
+    C<include_yes_decisions>
+
+See the L<cluster allocation explain docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-allocation-explain.html>
+for more information.
+
+=head2 C<pending_tasks()>
+
+    $response = $e->cluster->pending_tasks();
+
+Returns a list of cluster-level tasks still pending on the master node.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<local>,
+    C<master_timeout>
+
+See the L<pending tasks docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-pending.html>
+for more information.
+
+=head2 C<reroute()>
+
+    $e->cluster->reroute(
+        body => { commands }
+    );
+
+
+The C<reroute()> method is used to manually reallocate shards from one
+node to another.  The C<body> should contain the C<commands> indicating
+which changes should be made. For instance:
+
+    $e->cluster->reroute(
+        body => {
+            commands => [
+                { move => {
+                    index     => 'test',
+                    shard     => 0,
+                    from_node => 'node_1',
+                    to_node   => 'node_2
+                }},
+                { allocate => {
+                    index     => 'test',
+                    shard     => 1,
+                    node      => 'node_3'
+                }}
+            ]
+        }
+    );
+
+Query string parameters:
+    C<dry_run>,
+    C<error_trace>,
+    C<explain>,
+    C<human>,
+    C<master_timeout>,
+    C<metric>,
+    C<retry_failed>,
+    C<timeout>
+
+See the L<reroute docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-reroute.html>
+for more information.
+
+=head2 C<remote_info()>
+
+    $response = $e->cluster->remote_info();
+
+The C<remote_info()> API retrieves all of the configured remote cluster information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<remote_info docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-remote-info.html>
+for more information.
+
+
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/DanglingIndices.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/DanglingIndices.pm
new file mode 100644
index 0000000..f46df09
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/DanglingIndices.pm
@@ -0,0 +1,43 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::DanglingIndices;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('dangling_indices');
+
+1;
+
+__END__
+
+# ABSTRACT: Dangling indices feature of Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+The full documentation for Dangling Indices is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/master/indices.html#dangling-indices-api>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    $response = $es->dangling_indices->list_dangling_indices();
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Enrich.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Enrich.pm
new file mode 100644
index 0000000..e5e15cc
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Enrich.pm
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Enrich;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('enrich');
+
+1;
+
+__END__
+
+# ABSTRACT: Enrich feature of Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+The full documentation for Enrich feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/enrich-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->enrich->get_policy(
+        'name' => $name
+    );
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Eql.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Eql.pm
new file mode 100644
index 0000000..9bb96c0
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Eql.pm
@@ -0,0 +1,46 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Eql;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('eql');
+
+1;
+
+__END__
+
+# ABSTRACT: Eql feature of Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+The full documentation for Eql feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/eql-search-api.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->eql->search(
+        'index' => $index,
+        'body'  => {...}
+    );
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Features.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Features.pm
new file mode 100644
index 0000000..39c6d2f
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Features.pm
@@ -0,0 +1,57 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Features;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('features');
+
+1;
+
+__END__
+
+# ABSTRACT: Features API for Search::Elasticsearch 8.x
+
+
+=head1 SYNOPSIS
+
+    my $response = $es->features->get_features(...);
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<features>
+namespace, to support the API for the
+L<Features|https://www.elastic.co/guide/en/elasticsearch/reference/current/features-apis.html> plugin for Elasticsearch.
+
+=head1 METHODS
+
+The full documentation for the Features plugin is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/features-apis.html>
+
+=head2 C<explore()>
+
+    $response = $es->features->get_features();
+
+The C<get_features()> method Gets a list of features which can be included in snapshots
+using the feature_states field when creating a snapshot.
+
+See the L<get_features|https://www.elastic.co/guide/en/elasticsearch/reference/current/get-features-api.html>
+for more information.
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Fleet.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Fleet.pm
new file mode 100644
index 0000000..c0f801a
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Fleet.pm
@@ -0,0 +1,51 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Fleet;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('fleet');
+
+1;
+
+__END__
+
+# ABSTRACT: Fleet APIs for Search::Elasticsearch 8.x
+
+The following APIs support Fleet’s use of Elasticsearch as a data store for internal
+agent and action data. These APIs are experimental and for internal use by Fleet only.
+
+=head1 SYNOPSIS
+
+    my $response = $es->fleet->search(...);
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<fleet>
+namespace, to support the API for the
+L<Fleet|https://www.elastic.co/guide/en/elasticsearch/reference/current/fleet-apis.html> plugin for Elasticsearch.
+
+=head1 METHODS
+
+The full documentation for the Fleet plugin is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/fleet-apis.html>
+
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Graph.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Graph.pm
new file mode 100644
index 0000000..0b02008
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Graph.pm
@@ -0,0 +1,66 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Graph;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('graph');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Graph API for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->graph->explore(...);
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<graph>
+namespace, to support the API for the
+L<Graph|https://www.elastic.co/guide/en/x-pack/current/xpack-graph.html> plugin for Elasticsearch.
+
+=head1 METHODS
+
+The full documentation for the Graph plugin is available here:
+L<https://www.elastic.co/guide/en/graph/current/index.html>
+
+=head2 C<explore()>
+
+    $response = $es->graph->explore(
+        index => $index | \@indices,        # optional
+        type  => $type  | \@types,          # optional
+        body  => {...}
+    )
+
+The C<explore()> method allows you to discover vertices and connections which relate
+to your query.
+
+See the L<explore docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/graph-explore-api.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<routing>,
+    C<timeout>
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/ILM.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/ILM.pm
new file mode 100644
index 0000000..9b7f079
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/ILM.pm
@@ -0,0 +1,221 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::ILM;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use Search::Elasticsearch::Util qw(parse_params);
+use namespace::clean;
+__PACKAGE__->_install_api('ilm');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing index lifecycle management APIs for Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+This module provides methods to use the index lifecycle management feature.
+
+The full documentation for ILM is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/index-lifecycle-management.html>
+
+=head1 POLICY METHODS
+
+=head2 C<put_lifecycle()>
+
+    $response = $es->ilm->put_lifecycle(
+        policy  => $policy  # required
+        body    => {...}    # required
+    )
+
+The C<put_lifecycle()> method creates or updates a lifecycle policy.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM put_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-put-lifecycle.html>
+for more information.
+
+=head2 C<put_lifecycle()>
+
+    $response = $es->ilm->put_lifecycle(
+        policy  => $policy  # required
+        body    => {...}    # required
+    )
+
+The C<put_lifecycle()> method creates or updates a lifecycle policy.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM put_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-put-lifecycle.html>
+for more information.
+
+
+=head2 C<get_lifecycle()>
+
+    $response = $es->ilm->get_lifecycle(
+        policy  => $policy  # required
+    )
+
+The C<get_lifecycle()> method retrieves the specified policy
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM get_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-get-lifecycle.html>
+for more information.
+
+=head2 C<delete_lifecycle()>
+
+    $response = $es->ilm->delete_lifecycle(
+        policy  => $policy  # required
+    )
+
+The C<delete_lifecycle()> method deletes the specified policy
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM delete_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-remove-lifecycle.html>
+for more information.
+
+=head1 INDEX MANAGEMENT METHODS
+
+=head2 C<move_to_step()>
+
+    $response = $es->ilm->move_to_step(
+        index  => $index,       # required
+        body   => {...}         # required
+    )
+
+The C<move_to_step()> method triggers execution of a specific step in the lifecycle policy.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM move_to_step docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-move-to-step.html>
+for more information.
+
+
+=head2 C<retry()>
+
+    $response = $es->ilm->retry(
+        index  => $index,       # required
+    )
+
+The C<retry()> method retries executing the policy for an index that is in the ERROR step.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM retry docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-retry.html>
+for more information.
+
+
+=head2 C<remove_lifecycle()>
+
+    $response = $es->ilm->remove_lifecycle(
+        index  => $index  # required
+    )
+
+The C<remove_lifecycle()> method removes a lifecycle from the specified index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM remove_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-remove-lifecycle.html>
+for more information.
+
+=head2 C<explain_lifecycle()>
+
+    $response = $es->ilm->explain_lifecycle(
+        index  => $index  # required
+    )
+
+The C<explain_lifecycle()> method returns information about the index’s current lifecycle state.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM explain_lifecycle docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-explain-lifecycle.html>
+for more information.
+
+
+=head1 OPERATION MANAGEMENT APIS
+
+=head2 C<status()>
+
+    $response = $es->ilm->status;
+
+The C<status()> method returns the current operating mode for ILM.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM status docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-get-status.html>
+for more information.
+
+=head2 C<start()>
+
+    $response = $es->ilm->start;
+
+The C<start()> method starts the index lifecycle management process.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM start docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-start.html>
+for more information.
+
+=head2 C<stop()>
+
+    $response = $es->ilm->stop;
+
+The C<stop()> method stops the index lifecycle management process.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<ILM stop docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-stop.html>
+for more information.
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Indices.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Indices.pm
new file mode 100644
index 0000000..4ff568b
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Indices.pm
@@ -0,0 +1,968 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Indices;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+__PACKAGE__->_install_api('indices');
+
+1;
+
+__END__
+
+# ABSTRACT: A client for running index-level requests
+
+=head1 DESCRIPTION
+
+This module provides methods to make index-level requests, such as
+creating and deleting indices, managing type mappings, index settings,
+index templates and aliases.
+
+It does L<Search::Elasticsearch::Role::Client::Direct>.
+
+=head1 INDEX METHODS
+
+=head2 C<create()>
+
+    $result = $e->indices->create(
+        index => 'my_index'             # required
+
+        body  => {                      # optional
+            index settings
+            mappings
+            aliases
+        }
+    );
+
+The C<create()> method is used to create an index. Optionally, index
+settings, type mappings, and aliases can be added at the same time.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<include_type_name>,
+    C<master_timeout>,
+    C<timeout>,
+    C<update_all_types>,
+    C<wait_for_active_shards>
+
+See the L<create index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html>
+for more information.
+
+=head2 C<get()>
+
+    $response = $e->indices->get(
+        index   => 'index'   | \@indices    # required
+    );
+
+Returns the aliases, settings, and mappings for the specified indices.
+
+See the L<get index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-index.html>.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<flat_settings>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<include_defaults>,
+    C<include_type_name>,
+    C<local>,
+    C<master_timeout>
+
+=head2 C<exists()>
+
+    $bool = $e->indices->exists(
+        index => 'index' | \@indices    # required
+    );
+
+The C<exists()> method returns C<1> or the empty string to indicate
+whether the specified index or indices exist.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<flat_settings>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<include_defaults>,
+    C<local>
+
+See the L<index exists docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-indices-exists.html>
+for more information.
+
+=head2 C<delete()>
+
+    $response = $e->indices->delete(
+        index => 'index' | \@indices    # required
+    );
+
+The C<delete()> method deletes the specified indices.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<delete index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html>
+for more information.
+
+=head2 C<close()>
+
+    $response = $e->indices->close(
+        index => 'index' | \@indices    # required
+    );
+
+The C<close()> method closes the specified indices, reducing resource usage
+but allowing them to be reopened later.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>
+    C<master_timeout>,
+    C<timeout>
+
+See the L<close index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html>
+for more information.
+
+=head2 C<open()>
+
+    $response = $e->indices->open(
+        index => 'index' | \@indices    # required
+    );
+
+The C<open()> method opens closed indices.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<open index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html>
+for more information.
+
+=head2 C<rollover()>
+
+    $response = $e->indices->rollover(
+        alias     => $alias,                    # required
+        new_index => $index,                    # optional
+        body      => { rollover conditions }    # optional
+    );
+
+Rollover an index pointed to by C<alias> if it meets rollover conditions
+(eg max age, max docs) to a new index name.
+
+Query string parameters:
+    C<dry_run>,
+    C<error_trace>,
+    C<human>,
+    C<include_type_name>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<rollover index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-rollover-index.html>
+for more information.
+
+=head2 C<shrink()>
+
+    $response = $e->shrink(
+        index  => $index,                           # required
+        target => $target,                          # required
+        body   => { mappings, settings aliases }    # optional
+    );
+
+The shrink API shrinks the shards of an index down to a single shard (or to a factor
+of the original shards).
+
+Query string parameters:
+    C<copy_settings>,
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<shrink index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-shrink-index.html>
+for more information.
+
+=head2 C<split()>
+
+    $response = $e->split(
+        index  => $index,                           # required
+        target => $target,                          # required
+    );
+
+The split API splits a shard into multiple shards.
+
+Query string parameters:
+    C<copy_settings>,
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<split index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-split-index.html>
+for more information.
+
+=head2 C<freeze()>
+
+    $response = $e->indices->freeze(
+        $index => $index    # required
+    );
+
+The C<freeze()> API is used to freeze an index, which puts it in a state which has almost no
+overhead on the cluster.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<filter_path>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<freeze index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/freeze-index-api.html>
+for more information.
+
+=head2 C<unfreeze()>
+
+    $response = $e->indices->unfreeze(
+        $index => $index    # required
+    );
+
+The C<unfreeze()> API is used to return a frozen index to its normal state.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<filter_path>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>,
+    C<timeout>,
+    C<wait_for_active_shards>
+
+See the L<unfreeze index docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/freeze-index-api.html>
+for more information.
+
+=head2 C<clear_cache()>
+
+    $response = $e->indices->clear_cache(
+        index => 'index' | \@indices        # optional
+    );
+
+The C<clear_cache()> method is used to clear the in-memory filter, fielddata,
+or id cache for the specified indices.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<fielddata>,
+    C<fields>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<query>,
+    C<request>
+
+See the L<clear_cache docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-clearcache.html>
+for more information.
+
+=head2 C<refresh()>
+
+    $response = $e->indices->refresh(
+        index => 'index' | \@indices    # optional
+    );
+
+The C<refresh()> method refreshes the specified indices (or all indices),
+allowing recent changes to become visible to search. This process normally
+happens automatically once every second by default.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>
+
+See the L<refresh index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html>
+for more information.
+
+=head2 C<flush()>
+
+    $response = $e->indices->flush(
+        index => 'index' | \@indices    # optional
+    );
+
+The C<flush()> method causes the specified indices (or all indices) to be
+written to disk with an C<fsync>, and clears out the transaction log.
+This process normally happens automatically.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<force>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<wait_if_ongoing>
+
+See the L<flush index docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-flush.html>
+for more information.
+
+=head2 C<flush_synced()>
+
+    $respnse = $e->indices->flush_synced(
+        index => 'index' | \@indices    # optional
+    );
+
+The C<flush_synced()> method does a synchronised L<flush()> on the primaries and replicas of
+all the specified indices.  In other words, after flushing it tries to write a C<sync_id>
+on the primaries and replicas to mark them as containing the same documents.  During
+recovery, if a replica has the same C<sync_id> as the primary, then it doesn't need to check
+whether the segment files on primary and replica are the same, and it can move on
+directly to just replaying the translog.  This can greatly speed up recovery.
+
+Synced flushes happens automatically in the background on indices that have not received any
+writes for a while, but the L<flush_synced()> method can be used to trigger this process
+manually, eg before shutting down.  Any new commits immediately break the sync.
+
+See the L<flush synced docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-synced-flush.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>
+
+=head2 C<forcemerge()>
+
+    $response = $e->indices->forcemerge(
+        index => 'index' | \@indices    # optional
+    );
+
+The C<forcemerge()> method rewrites all the data in an index into at most
+C<max_num_segments>.  This is a very heavy operation and should only be run
+with care, and only on indices that are no longer being updated.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<flush>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<max_num_segments>,
+    C<only_expunge_deletes>
+
+See the L<forcemerge docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html>
+for more information.
+
+=head2 C<get_upgrade()>
+
+    $response = $e->indices->get_upgrade(
+        index => 'index' | \@indices    # optional
+    );
+
+The C<get_upgrade()> method returns information about which indices need to be
+upgraded, which can be done with the C<upgrade()> method.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>
+
+See the L<upgrade docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-upgrade.html>
+for more information.
+
+=head2 C<upgrade()>
+
+    $response = $e->indices->upgrade(
+        index => 'index' | \@indices    # optional
+    );
+
+The C<upgrade()> method upgrades all segments in the specified indices to the latest format.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<only_ancient_segments>,
+    C<wait_for_completion>
+
+See the L<upgrade docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-upgrade.html>
+for more information.
+
+=head1 MAPPING METHODS
+
+=head2 C<put_mapping()>
+
+    $response = $e->indices->put_mapping(
+        index => 'index' | \@indices    # optional,
+        type  => 'type',                # optional
+
+        body  => { mapping }            # required
+    )
+
+The C<put_mapping()> method is used to create or update a type
+mapping on an existing index.  Mapping updates are allowed to add new
+fields, but not to overwrite or change existing fields.
+
+For instance:
+
+    $response = $e->indices->put_mapping(
+        index   => 'users',
+        type    => 'user',
+        body    => {
+            user => {
+                properties => {
+                    name => { type => 'string'  },
+                    age  => { type => 'integer' }
+                }
+            }
+        }
+    );
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<include_type_name>,
+    C<master_timeout>,
+    C<timeout>,
+    C<update_all_types>
+
+See the L<put_mapping docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html>
+for more information.
+
+
+=head2 C<get_mapping()>
+
+    $result = $e->indices->get_mapping(
+        index => 'index' | \@indices    # optional,
+        type  => 'type'  | \@types      # optional
+    );
+
+The C<get_mapping()> method returns the type definitions for one, more or
+all types in one, more or all indices.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<include_type_name>,
+    C<local>,
+    C<master_timeout>
+
+See the L<get_mapping docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html>
+for more information.
+
+=head2 C<get_field_mapping()>
+
+    $result = $e->indices->get_field_mapping(
+        index => 'index'  | \@indices    # optional,
+        type  => 'type'   | \@types      # optional,
+        fields => 'field' | \@fields     # required
+
+        include_defaults => 0 | 1
+    );
+
+The C<get_field_mapping()> method returns the field definitions for one, more or
+all fields in one, more or all types and indices.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<include_defaults>,
+    C<include_type_name>,
+    C<local>
+
+See the L<get_mapping docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html>
+for more information.
+
+=head2 C<exists_type()>
+
+    $bool = $e->indices->exists_type(
+        index => 'index' | \@indices    # required,
+        type  => 'type'  | \@types      # required
+    );
+
+The C<exists_type()> method checks for the existence of all specified types
+in all specified indices, and returns C<1> or the empty string.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<local>
+
+See the L<exists_type docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-types-exists.html>
+for more information.
+
+=head1 ALIAS METHODS
+
+=head2 C<update_aliases()>
+
+    $response = $e->indices->update_aliases(
+        body => { actions }             # required
+    );
+
+The C<update_aliases()> method changes (by adding or removing) multiple
+index aliases atomically. For instance:
+
+    $response = $e->indices->update_aliases(
+        body => {
+            actions => [
+                { add    => { alias => 'current', index => 'logs_2013_09' }},
+                { remove => { alias => 'current', index => 'logs_2013_08' }}
+            ]
+        }
+    );
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<update_aliases docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html>
+for more information.
+
+=head2 C<put_alias()>
+
+    $response = $e->indices->put_alias(
+        index => 'index' | \@indices,       # required
+        name  => 'alias',                   # required
+
+        body  => { alias defn }             # optional
+    );
+
+The C<put_alias()> method creates an index alias. For instance:
+
+    $response = $e->indices->put_alias(
+        index => 'my_index',
+        name  => 'twitter',
+        body => {
+            filter => { term => { user_id => 'twitter' }}
+        }
+    );
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<put_alias docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html>
+for more information.
+
+=head2 C<get_alias()>
+
+    $result = $e->indices->get_alias(
+        index   => 'index' | \@indices,     # optional
+        name    => 'alias' | \@aliases      # optional
+    );
+
+The C<get_alias()> method returns the alias definitions for the specified
+aliases in the specified indices.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<local>
+
+See the L<get_alias docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html>
+for more information.
+
+=head2 C<exists_alias()>
+
+    $bool = $e->indices->exists_alias(
+        index   => 'index' | \@indices,     # optional
+        name    => 'alias' | \@aliases      # required
+    );
+
+The C<exists_alias()> method returns C<1> or the empty string depending on
+whether the specified aliases exist in the specified indices.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<local>
+
+See the L<exists_alias docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html>
+for more information.
+
+=head2 C<delete_alias()>
+
+    $response = $e->indices->delete_alias(
+        index   => 'index' | \@indices        # required,
+        name    => 'alias' | \@aliases        # required
+    );
+
+The C<delete_alias()> method deletes one or more aliases from one or more
+indices.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<delete_alias docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html>
+for more information.
+
+=head1 SETTINGS METHODS
+
+=head2 C<put_settings()>
+
+    $response = $e->indices->put_settings(
+        index   => 'index' | \@indices      # optional
+
+        body    => { settings }
+    );
+
+The C<put_settings()> method sets the index settings for the specified
+indices or all indices. For instance:
+
+    $response = $e->indices->put_settings(
+        body => {
+            "index.refresh_interval" => -1
+        }
+    );
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<flat_settings>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>,
+    C<preserve_existing>,
+    C<timeout>
+
+See the L<put_settings docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html>
+for more information.
+
+=head2 C<get_settings()>
+
+    $result = $e->indices->get_settings(
+        index   => 'index' | \@indices      # optional
+        name    => 'name'  | \@names        # optional
+    );
+
+The C<get_settings()> method retrieves the index settings for the specified
+indices or all indices.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<flat_settings>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<include_defaults>,
+    C<local>,
+    C<master_timeout>
+
+See the L<get_settings docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-settings.html>
+for more information.
+
+=head1 TEMPLATE METHODS
+
+=head2 C<put_template()>
+
+    $response = $e->indices->put_template(
+        name => 'template'                  # required
+        body => { template defn }           # required
+    );
+
+The C<put_template()> method is used to create or update index templates.
+
+Query string parameters:
+    C<create>,
+    C<error_trace>,
+    C<flat_settings>,
+    C<human>,
+    C<include_type_name>,
+    C<master_timeout>,
+    C<order>,
+    C<timeout>
+
+See the L<put_template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html>
+for more information.
+
+=head2 C<get_template()>
+
+    $result = $e->indices->get_template(
+        name  => 'template' | \@templates # optional
+    );
+
+The C<get_template()> method is used to retrieve a named template.
+
+Query string parameters:
+    C<error_trace>,
+    C<flat_settings>,
+    C<human>,
+    C<include_type_name>,
+    C<local>,
+    C<master_timeout>
+
+See the L<get_template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html>
+for more information.
+
+=head2 C<exists_template()>
+
+    $result = $e->indices->exists_template(
+        name  => 'template'                 # optional
+    );
+
+The C<exists_template()> method is used to check whether the named template exists.
+
+Query string parameters:
+    C<error_trace>,
+    C<flat_settings>,
+    C<human>,
+    C<local>,
+    C<master_timeout>
+
+See the L<get_template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html>
+for more information.
+
+=head2 C<delete_template()>
+
+    $response = $e->indices->delete_template(
+        name  => 'template'                 # required
+    );
+
+The C<delete_template()> method is used to delete a named template.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>,
+    C<version>,
+    C<version_type>
+
+See the L<delete_template docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html>
+for more information.
+
+
+=head1 STATS METHODS
+
+=head2 C<stats()>
+
+    $result = $e->indices->stats(
+        index   => 'index'  | \@indices      # optional
+        metric  => 'metric' | \@metrics      # optional
+    );
+
+The C<stats()> method returns statistical information about one, more or all
+indices. By default it returns all metrics, but you can limit which metrics
+are returned by specifying the C<metric>.
+
+Allowed metrics are:
+    C<_all>,
+    C<completion>
+    C<docs>,
+    C<fielddata>,
+    C<filter_cache>,
+    C<flush>,
+    C<get>,
+    C<id_cache>,
+    C<indexing>,
+    C<merge>,
+    C<percolate>,
+    C<query_cache>,
+    C<refresh>,
+    C<request_cache>,
+    C<search>,
+    C<segments>,
+    C<store>
+
+
+Query string parameters:
+    C<completion_fields>,
+    C<error_trace>,
+    C<fielddata_fields>,
+    C<fields>,
+    C<groups>,
+    C<human>,
+    C<include_segment_file_sizes>,
+    C<level>,
+    C<types>
+
+See the L<stats docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-stats.html>
+for more information.
+
+=head2 C<recovery()>
+
+    $result = $e->indices->recovery(
+        index   => 'index' | \@indices      # optional
+    );
+
+Provides insight into on-going shard recoveries.
+
+Query string parameters:
+    C<active_only>,
+    C<detailed>,
+    C<error_trace>,
+    C<human>
+
+See the L<recovery docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-recovery.html>
+for more information.
+
+=head2 C<segments()>
+
+    $result = $e->indices->segments(
+        index   => 'index' | \@indices      # optional
+    );
+
+The C<segments()> method is used to return information about the segments
+that an index contains.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<verbose>
+
+See the L<segments docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-segments.html>
+for more information.
+
+=head2 C<shard_stores()>
+
+    $result = $e->indices->shard_stores(
+        index   => 'index' | \@indices      # optional
+    );
+
+The C<shard_stores()> method is used to find out which nodes contain
+copies of which shards, whether the shards are allocated or not.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<status>
+
+See the L<shard_stores docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-shards-stores.html>
+for more information.
+
+=head1 QUERY AND ANALYSIS METHODS
+
+=head2 C<analyze()>
+
+    $result = $e->indices->analyze(
+        index   => 'index'                  # optional,
+        body    => 'text to analyze'
+    );
+
+The C<analyze()> method passes the text in the C<body> through the specified
+C<analyzer>, C<tokenizer> or token C<filter> - which may be global, or associated
+with a particular index or field - and returns the tokens.  Very useful
+for debugging analyzer configurations.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<analyze docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html>
+for more information.
+
+=head2 C<validate_query()>
+
+    $result = $e->indices->validate_query(
+        index   => 'index' | \@indices,     # optional
+        type    => 'type'  | \@types,       # optional
+
+        body    => { query }
+    );
+
+The C<validate_query()> method accepts a query in the C<body> and checks
+whether the query is valid or not.  Most useful when C<explain> is set
+to C<true>, in which case it includes an execution plan in the output.
+
+Query string parameters:
+    C<all_shards>,
+    C<allow_no_indices>,
+    C<analyze_wildcard>,
+    C<analyzer>,
+    C<default_operator>,
+    C<df>,
+    C<error_trace>,
+    C<explain>,
+    C<expand_wildcards>,
+    C<ignore_unavailable>,
+    C<lenient>,
+    C<q>,
+    C<rewrite>
+
+See the L<validate_query docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-validate.html>
+for more information.
+
+
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Ingest.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Ingest.pm
new file mode 100644
index 0000000..97bae31
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Ingest.pm
@@ -0,0 +1,130 @@
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Client::8_0::Direct::Ingest;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+__PACKAGE__->_install_api('ingest');
+
+1;
+
+__END__
+
+# ABSTRACT: A client for accessing the Ingest API
+
+=head1 DESCRIPTION
+
+This module provides methods to access the Ingest API, such as creating,
+getting, deleting and simulating ingest pipelines.
+
+It does L<Search::Elasticsearch::Role::Client::Direct>.
+
+=head1 METHODS
+
+=head2 C<put_pipeline()>
+
+    $response = $e->ingest->put_pipeline(
+        id   => $id,                # required
+        body => { pipeline defn }   # required
+    );
+
+The C<put_pipeline()> method creates or updates a pipeline with the specified ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<put pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/put-pipeline-api.html>
+for more information.
+
+=head2 C<get_pipeline()>
+
+    $response = $e->ingest->get_pipeline(
+        id   => \@id,               # optional
+    );
+
+The C<get_pipeline()> method returns pipelines with the specified IDs (or all pipelines).
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<get pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/get-pipeline-api.html>
+for more information.
+
+=head2 C<delete_pipeline()>
+
+    $response = $e->ingest->delete_pipeline(
+        id   => $id,                # required
+    );
+
+The C<delete_pipeline()> method deletes the pipeline with the specified ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<delete pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/delete-pipeline-api.html>
+for more information.
+
+=head2 C<simulate()>
+
+    $response = $e->ingest->put_pipeline(
+        id   => $id,                # optional
+        body => { simulate args }   # required
+    );
+
+The C<simulate()> method executes the pipeline specified by ID or inline in the body
+against the docs provided in the body and provides debugging output of the execution
+process.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<verbose>
+
+See the L<simulate pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/simulate-pipeline-api.html>
+for more information.
+
+
+=head2 C<simulate()>
+
+    $response = $e->ingest->put_pipeline(
+        id   => $id,                # optional
+        body => { simulate args }   # required
+    );
+
+The C<simulate()> method executes the pipeline specified by ID or inline in the body
+against the docs provided in the body and provides debugging output of the execution
+process.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<verbose>
+
+See the L<simulate pipeline docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/simulate-pipeline-api.html>
+for more information.
+
+=head2 C<processor_grok>
+
+    $response = $e->inges->processor_grok()
+
+Retrieves the configured patterns associated with the Grok processor.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<grok processor docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/grok-processor.html>
+for more information.
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/License.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/License.pm
new file mode 100644
index 0000000..8d326fa
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/License.pm
@@ -0,0 +1,134 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::License;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('license');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing License API for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->license->get();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<license>
+namespace, to support the API for the License plugin for Elasticsearch.
+
+=head1 METHODS
+
+The full documentation for the License plugin is available here:
+L<https://www.elastic.co/guide/en/x-pack/current/license-management.html>
+
+=head2 C<get()>
+
+    $response = $es->license->get()
+
+The C<get()> method returns the currently installed license.
+
+See the L<license.get docs|https://www.elastic.co/guide/en/x-pack/current/listing-licenses.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<local>
+
+=head2 C<post()>
+
+    $response = $es->license->post(
+        body     => {...}          # required
+    );
+
+The C<post()> method adds or updates the license for the cluster. The C<body>
+can be passed as JSON or as a string.
+
+See the L<license.put docs|https://www.elastic.co/guide/en/x-pack/current/installing-license.html>
+for more information.
+
+Query string parameters:
+    C<acknowledge>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_basic_status()>
+
+    $response = $es->license->get_basic_status()
+
+This API enables you to check the status of your basic license.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<get-basic-status docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/get-basic-status.html> for more.
+
+=head2 C<post_start_basic()>
+
+    $response = $es->license->post_start_basic()
+
+This API enables you to  initiate an indefinite basic license, which gives access to all the basic features.
+
+Query string parameters:
+    C<acknowledge>,
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<post-start-basic docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/start-basic.html> for more.
+
+
+=head2 C<get_trial_status()>
+
+    $response = $es->license->get_trial_status()
+
+This API enables you to check the status of your trial license.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<get-trial-status docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/get-trial-status.html> for more.
+
+=head2 C<post_start_trial()>
+
+    $response = $es->license->post_start_trial()
+
+This API enables you to upgrade from a basic license to a 30-day trial license, which gives
+access to the platinum features.
+
+Query string parameters:
+    C<acknowledge>,
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<post-start-trial docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/start-trial.html> for more.
+
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Logstash.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Logstash.pm
new file mode 100644
index 0000000..95eefca
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Logstash.pm
@@ -0,0 +1,47 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Logstash;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('logstash');
+
+1;
+
+__END__
+
+# ABSTRACT: Logstash API for Search::Elasticsearch 8.x
+
+
+=head1 SYNOPSIS
+
+    my $response = $es->logstash->get_pipeline(...);
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<logstash>
+namespace, to support the API for the
+L<Logstash|https://www.elastic.co/guide/en/elasticsearch/reference/current/logstash-apis.html> plugin for Elasticsearch.
+
+=head1 METHODS
+
+The full documentation for the Features plugin is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/logstash-apis.html>
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/ML.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/ML.pm
new file mode 100644
index 0000000..5cbdcce
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/ML.pm
@@ -0,0 +1,842 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::ML;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('ml');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing ML API for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->ml->start_datafeed(...)
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with an C<ml>
+namespace, to support the
+L<Machine Learning APIs|https://www.elastic.co/guide/en/x-pack/7.0/xpack-ml.html>.
+
+The full documentation for the ML feature is available here:
+L<https://www.elastic.co/guide/en/x-pack/7.0/xpack-ml.html>
+
+=head1 DATAFEED METHODS
+
+=head2 C<put_datafeed()>
+
+    $response = $es->ml->put_datafeed(
+        datafeed_id => $id      # required
+        body        => {...}    # required
+    )
+
+The C<put_datafeed()> method enables you to instantiate a datafeed.
+
+See the L<put_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_datafeed()>
+
+    $response = $es->xpack->ml->delete_datafeed(
+        datafeed_id => $id      # required
+    )
+
+The C<delete_datafeed()> method enables you to delete a datafeed.
+
+See the L<delete_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<force>,
+    C<human>
+
+=head2 C<start_datafeed()>
+
+    $response = $es->ml->start_datafeed(
+        datafeed_id => $id      # required
+    )
+
+The C<start_datafeed()> method enables you to start a datafeed.
+
+See the L<start_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-start-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<end>,
+    C<error_trace>,
+    C<human>,
+    C<start>,
+    C<timeout>
+
+=head2 C<stop_datafeed()>
+
+    $response = $es->ml->stop_datafeed(
+        datafeed_id => $id      # required
+    )
+
+The C<stop_datafeed()> method enables you to stop a datafeed.
+
+See the L<stop_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-stop-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_datafeeds>,
+    C<error_trace>,
+    C<force>,
+    C<human>,
+    C<timeout>
+
+=head2 C<get_datafeeds()>
+
+    $response = $es->ml->get_datafeeds(
+        datafeed_id => $id      # optional
+    )
+
+The C<get_datafeeds()> method enables you to retrieve configuration information for datafeeds.
+
+See the L<get_datafeeds docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_datafeeds>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_datafeed_stats()>
+
+    $response = $es->ml->get_datafeed_stats(
+        datafeed_id => $id      # optional
+    )
+
+The C<get_datafeed_stats()> method enables you to retrieve configuration information for datafeeds.
+
+See the L<get_datafeed_stats docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-datafeed-stats.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_datafeeds>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<preview_datafeed()>
+
+    $response = $es->ml->preview_datafeed(
+        datafeed_id => $id      # required
+    )
+
+The C<preview_datafeed()> method enables you to preview a datafeed.
+
+See the L<preview_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-preview-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<update_datafeed()>
+
+    $response = $es->ml->update_datafeed(
+        datafeed_id => $id      # required
+        body        => {...}    # required
+    )
+
+The C<update_datafeed()> method enables you to update certain properties of a datafeed.
+
+See the L<update_datafeed docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-datafeed.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 JOB METHODS
+
+=head2 C<put_job()>
+
+    $response = $es->ml->put_job(
+        job_id => $id           # required
+        body        => {...}    # required
+    )
+
+The C<put_job()> method enables you to instantiate a job.
+
+See the L<put_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-job.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_job()>
+
+    $response = $es->ml->delete_job(
+        job_id => $id           # required
+    )
+
+The C<delete_job()> method enables you to delete a job.
+
+See the L<delete_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-job.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<force>,
+    C<human>,
+    C<wait_for_completion>
+
+=head2 C<open_job()>
+
+    $response = $es->ml->open_job(
+        job_id => $id           # required
+    )
+
+The C<open_job()> method enables you to open a closed job.
+
+See the L<open_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-open-job.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<close_job()>
+
+    $response = $es->ml->close_job(
+        job_id => $id           # required
+    )
+
+The C<close_job()> method enables you to close an open job.
+
+See the L<close_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-close-job.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_jobs>,
+    C<error_trace>,
+    C<force>,
+    C<human>,
+    C<timeout>
+
+=head2 C<get_jobs()>
+
+    $response = $es->ml->get_jobs(
+        job_id => $id           # optional
+    )
+
+The C<get_jobs()> method enables you to retrieve configuration information for jobs.
+
+See the L<get_jobs docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-job.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_jobs>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_job_stats()>
+
+    $response = $es->ml->get_jobs_stats(
+        job_id => $id           # optional
+    )
+
+The C<get_jobs_stats()> method enables you to retrieve usage information for jobs.
+
+See the L<get_job_statss docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-job-stats.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_jobs>,
+    C<error_trace>,
+    C<human>
+
+
+=head2 C<flush_job()>
+
+    $response = $es->ml->flush_job(
+        job_id => $id           # required
+    )
+
+The C<flush_job()> method forces any buffered data to be processed by the job.
+
+See the L<flush_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-flush-job.html>
+for more information.
+
+Query string parameters:
+    C<advance_time>,
+    C<calc_interm>,
+    C<end>,
+    C<error_trace>,
+    C<human>,
+    C<skip_time>,
+    C<start>
+
+=head2 C<post_data()>
+
+    $response = $es->ml->post_data(
+        job_id => $id           # required
+        body   => [data]        # required
+    )
+
+The C<post_data()> method enables you to send data to an anomaly detection job for analysis.
+
+See the L<post_data docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-post-data.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<reset_end>,
+    C<reset_start>
+
+=head2 C<update_job()>
+
+    $response = $es->ml->update_job(
+        job_id => $id           # required
+        body        => {...}    # required
+    )
+
+The C<update_job()> method enables you to update certain properties of a job.
+
+See the L<update_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-job.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_expired_data>
+
+    $response = $es->ml->delete_expired_data(
+    )
+
+The C<delete_expired_data()> method deletes expired machine learning data.
+
+See the L<delete_expired_data docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-expired-data.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+
+=head1 CALENDAR METHODS
+
+=head2 C<put_calendar()>
+
+    $response = $es->ml->put_calendar(
+        calendar_id => $id      # required
+        body        => {...}    # optional
+    )
+
+The C<put_calendar()> method creates a new calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<put calendar docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-calendar.html>
+for more information.
+
+=head2 C<delete_calendar()>
+
+    $response = $es->ml->delete_calendar(
+        calendar_id => $id      # required
+    )
+
+The C<delete_calendar()> method deletes the specified calendar
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<delete_calendar docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar.html>
+for more information.
+
+=head2 C<put_calendar_job()>
+
+    $response = $es->ml->put_calendar_job(
+        calendar_id => $id,     # required
+        job_id      => $id      # required
+    )
+
+The C<put_calendar_job()> method adds a job to a calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<put_calendar_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-calendar-job.html>
+for more information.
+
+=head2 C<delete_calendar_job()>
+
+    $response = $es->ml->delete_calendar_job(
+        calendar_id => $id,     # required
+        job_id      => $id      # required
+    )
+
+The C<delete_calendar_job()> method deletes a job from a calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<delete_calendar_job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar-job.html>
+for more information.
+
+=head2 C<put_calendar_event()>
+
+    $response = $es->ml->post_calendar_events(
+        calendar_id => $id,     # required
+        body        => {...}    # required
+    )
+
+The C<post_calendar_events()> method adds scheduled events to a calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<post_calendar_events docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-post-calendar-events.html>
+for more information.
+
+
+=head2 C<delete_calendar_event()>
+
+    $response = $es->ml->delete_calendar_event(
+        calendar_id => $id,     # required
+        event_id    => $id      # required
+    )
+
+The C<delete_calendar_event()> method deletes an event from a calendar.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<delete_calendar_event docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar-event.html>
+for more information.
+
+=head2 C<get_calendars()>
+
+    $response = $es->ml->get_calendars(
+        calendar_id => $id,     # optional
+    )
+
+The C<get_calendars()> method returns the specified calendar or all calendars.
+
+Query string parameters:
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<size>
+
+See the L<get_calendars docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-calendar-event.html>
+for more information.
+
+=head2 C<get_calendar_events()>
+
+    $response = $es->ml->get_calendar_events(
+        calendar_id => $id,     # required
+    )
+
+The C<get_calendar_events()> method retrieves events from a calendar.
+
+Query string parameters:
+    C<end>,
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<job_id>,
+    C<size>,
+    C<start>
+
+See the L<get_calendar_events docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-calendar-event.html>
+for more information.
+
+=head1 FILTER METHODS
+
+=head2 C<put_filter()>
+
+    $response = $es->ml->put_filter(
+        filter_id   => $id,     # required
+        body        => {...}    # required
+    )
+
+The C<put_filter()> method creates a named filter.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<put_filter docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-put-filter.html>
+for more information.
+
+=head2 C<update_filter()>
+
+    $response = $es->ml->update_filter(
+        filter_id   => $id,     # required
+        body        => {...}    # required
+    )
+
+The C<update_filter()> method updates the description of a filter, adds items, or removes items.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<update_filter docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-filter.html>
+for more information.
+
+=head2 C<get_filters()>
+
+    $response = $es->ml->get_filters(
+        filter_id   => $id,     # optional
+    )
+
+The C<get_filters()> method retrieves a named filter or all filters.
+
+Query string parameters:
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<size>
+
+See the L<get_filters docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-filters.html>
+for more information.
+
+=head2 C<delete_filter()>
+
+    $response = $es->ml->delete_filter(
+        filter_id   => $id,     # required
+    )
+
+The C<delete_filter()> method deletes a named filter.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+See the L<delete_filters docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-filter.html>
+for more information.
+
+=head1 FORECAST METHODS
+
+=head2 C<forecast()>
+
+    $response = $es->ml->forecast(
+        job_id      => $id      # required
+    )
+
+The C<forecast()> method enables you to create a new forecast
+
+See the L<forecast docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-forecast.html>
+for more information.
+
+Query string parameters:
+    C<duration>,
+    C<error_trace>,
+    C<expires_in>,
+    C<human>
+
+=head2 C<delete_forecast()>
+
+    $response = $es->ml->delete_forecast(
+        forecast_id => $id,     # required
+        job_id      => $id      # required
+    )
+
+The C<delete_forecast()> method enables you to delete an existing forecast.
+
+See the L<delete_forecast docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-forecast.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_forecasts>,
+    C<error_trace>,
+    C<human>,
+    C<timeout>
+
+=head1 MODEL SNAPSHOT METHODS
+
+=head2 C<delete_model_snapshot()>
+
+    $response = $es->ml->delete_model_snapshot(
+        snapshot_id => $id      # required
+    )
+
+The C<delete_model_snapshot()> method enables you to delete an existing model snapshot.
+
+See the L<delete_model_snapshot docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-snapshot.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_model_snapshots()>
+
+    $response = $es->ml->get_model_snapshots(
+        job_id      => $job_id,         # required
+        snapshot_id => $snapshot_id     # optional
+    )
+
+The C<get_model_snapshots()> method enables you to retrieve information about model snapshots.
+
+See the L<get_model_snapshots docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-snapshot.html>
+for more information.
+
+Query string parameters:
+    C<desc>,
+    C<end>,
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<size>,
+    C<sort>,
+    C<start>
+
+=head2 C<revert_model_snapshot()>
+
+    $response = $es->ml->revert_model_snapshot(
+        job_id      => $job_id,         # required
+        snapshot_id => $snapshot_id     # required
+    )
+
+The C<revert_model_snapshots()> method enables you to revert to a specific snapshot.
+
+See the L<revert_model_snapshot docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-revert-snapshot.html>
+for more information.
+
+Query string parameters:
+    C<delete_intervening_results>,
+    C<error_trace>,
+    C<human>
+
+=head2 C<update_model_snapshot()>
+
+    $response = $es->ml->update_model_snapshot(
+        job_id      => $job_id,         # required
+        snapshot_id => $snapshot_id     # required
+    )
+
+The C<update_model_snapshots()> method enables you to update certain properties of a snapshot.
+
+See the L<update_model_snapshot docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-update-snapshot.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 RESULT METHODS
+
+=head2 C<get_buckets()>
+
+    $response = $es->ml->get_buckets(
+        job_id      => $job_id,         # required
+        timestamp   => $timestamp       # optional
+    )
+
+The C<get_buckets()> method enables you to retrieve job results for one or more buckets.
+
+See the L<get_buckets docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-bucket.html>
+for more information.
+
+Query string parameters:
+    C<anomaly_score>,
+    C<desc>,
+    C<end>,
+    C<error_trace>,
+    C<exclude_interim>,
+    C<expand>,
+    C<from>,
+    C<human>,
+    C<size>,
+    C<sort>,
+    C<start>
+
+=head2 C<get_overall_buckets()>
+
+    $response = $es->ml->get_overall_buckets(
+        job_id      => $job_id,         # required
+    )
+
+The C<get_overall_buckets()> method retrieves overall bucket results that summarize the bucket results of multiple jobs.
+
+See the L<get_overall_buckets docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-overall-buckets.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_jobs>,
+    C<bucket_span>,
+    C<end>,
+    C<error_trace>,
+    C<exclude_interim>,
+    C<human>,
+    C<overall_score>,
+    C<start>,
+    C<top_n>
+
+=head2 C<get_categories()>
+
+    $response = $es->ml->get_categories(
+        job_id      => $job_id,         # required
+        category_id => $category_id     # optional
+    )
+
+The C<get_categories()> method enables you to retrieve job results for one or more categories.
+
+See the L<get_categories docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-category.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<from>,
+    C<human>,
+    C<size>
+
+
+=head2 C<get_influencers()>
+
+    $response = $es->ml->get_influencers(
+        job_id      => $job_id,         # required
+    )
+
+The C<get_influencers()> method enables you to retrieve job results for one or more influencers.
+
+See the L<get_influencers docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-influencer.html>
+for more information.
+
+Query string parameters:
+    C<desc>,
+    C<end>,
+    C<error_trace>,
+    C<exclude_interim>,
+    C<expand>,
+    C<from>,
+    C<human>,
+    C<influencer_score>,
+    C<size>,
+    C<sort>,
+    C<start>
+
+=head2 C<get_records()>
+
+    $response = $es->ml->get_records(
+        job_id      => $job_id,         # required
+    )
+
+The C<get_records()> method enables you to retrieve anomaly records for a job.
+
+See the L<get_records docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-get-record.html>
+for more information.
+
+Query string parameters:
+    C<desc>,
+    C<end>,
+    C<error_trace>,
+    C<exclude_interim>,
+    C<expand>,
+    C<from>,
+    C<human>,
+    C<record_score>,
+    C<size>,
+    C<sort>,
+    C<start>
+
+=head1 FILE STRUCTURE METHODS
+
+=head2 C<find_file_structure>
+
+
+    $response = $es->ml->find_file_structure(
+        body    => { ... },         # required
+    )
+
+The C<find_file_structure()> method finds the structure of a text file which contains data
+that is suitable to be ingested into Elasticsearch.
+
+See the L<find_file_structure docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-find-file-structure.html>
+for more information.
+
+Query string parameters:
+    C<charset>,
+    C<column_names>,
+    C<delimiter>,
+    C<error_trace>,
+    C<explain>,
+    C<format>,
+    C<grok_pattern>,
+    C<has_header_row>,
+    C<human>,
+    C<lines_to_sample>,
+    C<quote>,
+    C<should_trim_fields>,
+    C<timeout>,
+    C<timestamp_field>,
+    C<timestamp_format>
+
+
+
+=head1 INFO METHODS
+
+
+=head2 C<info>
+
+    $response = $es->ml->info();
+
+The C<info()> method returns defaults and limits used by machine learning.
+
+See the L<find_file_structure docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/get-ml-info.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 UPGRADE METHODS
+
+=head2 C<set_upgrade_mode>
+
+    $response = $es->ml->set_upgrade_mode();
+
+The C<set_upgrade_mode()> method sets a cluster wide C<upgrade_mode> setting that prepares
+machine learning indices for an upgrade.
+
+See the L<set_upgrade_mode docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-set-upgrade-mode.html>
+for more information.
+
+Query string parameters:
+    C<enabled>,
+    C<error_trace>,
+    C<human>,
+    C<timeout>
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Migration.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Migration.pm
new file mode 100644
index 0000000..8a71168
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Migration.pm
@@ -0,0 +1,102 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Migration;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('migration');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Migration API for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->migration->deprecations();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<migration>
+namespace, to support the API
+L<Migration APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api.html>.
+
+=head1 METHODS
+
+The full documentation for the Migration APIs is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api.html>
+
+=head2 C<deprecations()>
+
+    $response = $es->migration->deprecations(
+        index => $index      # optional
+    )
+
+The C<deprecations()> API is to be used to retrieve information about different cluster, node,
+and index level settings that use deprecated features that will be removed or changed in the
+next major version.
+
+See the L<deprecations docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-deprecation.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_assistance()>
+
+    $response = $es->migration->get_assistance(
+        index => $index | \@indices      # optional
+    )
+
+The C<get_assistance()> API analyzes existing indices in the cluster and returns the information
+about indices that require some changes before the cluster can be upgraded to the next major version.
+
+See the L<get_assistance docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-assistance.html>
+for more information.
+
+Query string parameters:
+    C<allow_no_indices>,
+    C<error_trace>,
+    C<expand_wildcards>,
+    C<human>,
+    C<ignore_unavailable>
+
+=head2 C<upgrade()>
+
+    $response = $es->migration->upgrade(
+        index => $index       # required
+    )
+
+The C<upgrade()> API performs the upgrade of internal indices to make them compatible with the
+next major version.
+
+See the L<upgrade() docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/migration-api-upgrade.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<wait_for_completion>
+
+
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Monitoring.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Monitoring.pm
new file mode 100644
index 0000000..9e01fed
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Monitoring.pm
@@ -0,0 +1,35 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Monitoring;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('monitoring');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Monitoring for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->monitoring( body => {...} )
\ No newline at end of file
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Nodes.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Nodes.pm
new file mode 100644
index 0000000..4869589
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Nodes.pm
@@ -0,0 +1,203 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Nodes;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+__PACKAGE__->_install_api('nodes');
+
+1;
+
+__END__
+
+# ABSTRACT: A client for running node-level requests
+
+=head1 DESCRIPTION
+
+This module provides methods to make node-level requests, such as
+retrieving node info and stats.
+
+It does L<Search::Elasticsearch::Role::Client::Direct>.
+
+=head1 METHODS
+
+
+=head2 C<info()>
+
+    $response = $e->nodes->info(
+        node_id => $node_id | \@node_ids       # optional
+        metric  => $metric  | \@metrics        # optional
+    );
+
+The C<info()> method returns static information about the nodes in the
+cluster, such as the configured maximum number of file handles, the maximum
+configured heap size or the threadpool settings.
+
+Allowed metrics are:
+    C<http>,
+    C<jvm>,
+    C<network>,
+    C<os>,
+    C<plugin>,
+    C<process>,
+    C<settings>,
+    C<thread_pool>,
+    C<timeout>,
+    C<transport>
+
+Query string parameters:
+    C<error_trace>,
+    C<flat_settings>,
+    C<human>
+
+See the L<node_info docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-info.html>
+for more information.
+
+=head2 C<stats()>
+
+    $response = $e->nodes->stats(
+        node_id      => $node_id    | \@node_ids       # optional
+        metric       => $metric     | \@metrics        # optional
+        index_metric => $ind_metric | \@ind_metrics    # optional
+    );
+
+The C<stats()> method returns statistics about the nodes in the
+cluster, such as the number of currently open file handles, the current
+heap memory usage or the current number of threads in use.
+
+Stats can be returned for all nodes, or limited to particular nodes
+with the C<node_id> parameter. By default all metrics are returned, but
+these can be limited to those specified in the C<metric> parameter.
+
+Allowed metrics are:
+    C<_all>,
+    C<breaker>,
+    C<fs>,
+    C<http>,
+    C<include_segment_file_sizes>,
+    C<indices>,
+    C<jvm>,
+    C<network>,
+    C<os>,
+    C<process>,
+    C<thread_pool>,
+    C<timeout>,
+    C<transport>
+
+If the C<indices> metric (or C<_all>) is specified, then
+L<indices_stats|Search::Elasticsearch::Client::8_0::Direct::Indices/indices_stats()>
+information is returned on a per-node basis. Which indices stats are
+returned can be controlled with the C<index_metric> parameter:
+
+    $response = $e->nodes->stats(
+        node_id       => 'node_1',
+        metric        => ['indices','fs']
+        index_metric  => ['docs','fielddata']
+    );
+
+Allowed index metrics are:
+    C<_all>,
+    C<completion>
+    C<docs>,
+    C<fielddata>,
+    C<filter_cache>,
+    C<flush>,
+    C<get>,
+    C<id_cache>,
+    C<indexing>,
+    C<merge>,
+    C<percolate>,
+    C<query_cache>,
+    C<refresh>,
+    C<search>,
+    C<segments>,
+    C<store>,
+    C<warmer>
+
+
+Query string parameters:
+    C<completion_fields>,
+    C<error_trace>,
+    C<fielddata_fields>,
+    C<fields>,
+    C<groups>,
+    C<human>,
+    C<level>,
+    C<types>
+
+See the L<stats docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-stats.html>
+for more information.
+
+=head2 C<hot_threads()>
+
+    $response = $e->nodes->hot_threads(
+        node_id => $node_id | \@node_ids       # optional
+    )
+
+The C<hot_threads()> method is a useful tool for diagnosing busy nodes. It
+takes a snapshot of which threads are consuming the most CPU.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<ignore_idle_threads>,
+    C<interval>,
+    C<snapshots>,
+    C<threads>,
+    C<timeout>,
+    C<type>
+
+See the L<hot_threads docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-hot-threads.html>
+for more information.
+
+=head2 C<reload_secure_settings()>
+
+    $response = $e->nodes->reload_secure_settings(
+        node_id => $node_id | \@node_ids    # optional
+    );
+
+The C<reload_secure_settings()> API will reload the reloadable settings stored in the keystore
+on each node.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<timeout>
+
+See the L<reload-secure-settings docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/secure-settings.html>
+for more information.
+
+=head2 C<usage()>
+
+    $response = $e->nodes->usage(
+        node_id => $node_id | \@node_ids       # optional
+        metric  => $metric  | \@metrics        # optional
+    )
+
+The C<usage()> API retrieve sinformation on the usage of features for each node.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<timeout>
+
+See the L<nodes_usage docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-usage.html>
+for more information.
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Rollup.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Rollup.pm
new file mode 100644
index 0000000..98463e2
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Rollup.pm
@@ -0,0 +1,186 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Rollup;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('rollup');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Rollups for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->rollup->search( body => {...} )
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<rollup>
+namespace, to support the
+L<Rollup APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-apis.html>.
+
+The full documentation for the Rollups feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-rollup.html>
+
+
+=head1 GENERAL METHODS
+
+=head2 C<search()>
+
+    $response = $es->rollup->search(
+        index   => $index | \@indices,      # optional
+        body    => {...}                    # optional
+    )
+
+The C<search()> method executes a normal search but can join the results from ordinary indices with
+those from rolled up indices.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<typed_keys>
+
+See the L<rollup search docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-search.html>
+for more information.
+
+=head1 JOB METHODS
+
+=head2 C<put_job()>
+
+    $response = $es->rollup->put_job(
+        id      => $id,                     # required
+        body    => {...}                    # optional
+    )
+
+The C<put_job()> method creates a rollup job which will rollup matching indices to a rolled up index
+in the background.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup create job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-put-job.html>
+for more information.
+
+=head2 C<delete_job()>
+
+    $response = $es->rollup->delete_job(
+        id      => $id,                     # required
+    )
+
+The C<delete_job()> method deletes a rollup job by ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup delete job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-delete-job.html>
+for more information.
+
+=head2 C<get_jobs()>
+
+    $response = $es->rollup->get_jobs(
+        id      => $id,     # optional
+    )
+
+The C<get_job()> method retrieves a rollup job by ID, or all jobs if not specified.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup get jobs docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-get-job.html>
+for more information.
+
+=head2 C<start_job()>
+
+    $response = $es->rollup->start_job(
+        id      => $id,     # required
+    )
+
+The C<start_job()> method starts the specified rollup job.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup start job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-start-job.html>
+for more information.
+
+=head2 C<stop_job()>
+
+    $response = $es->rollup->stop_job(
+        id      => $id,     # required
+    )
+
+The C<stop_job()> method stops the specified rollup job.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<rollup stop job docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-stop-job.html>
+for more information.
+
+=head1 DATA METHODS
+
+=head2 C<get_rollup_caps()>
+
+    $response = $es->rollup->get_rollup_caps(
+        id => $index    # optional
+    )
+
+The C<get_rollup_caps()> method returns the capabilities of any rollup jobs that have been configured for a specific index or index pattern.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<get rollup caps docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-get-rollup-caps.html>
+for more information.
+
+=head2 C<get_rollup_index_caps()>
+
+    $response = $es->rollup->get_rollup_index_caps(
+        id => $index    # optional
+    )
+
+The C<get_rollup_index_caps()> method returns the rollup capabilities of all jobs inside of a rollup index.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<get rollup index caps docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-get-rollup-index-caps.html>
+for more information.
+
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/SQL.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/SQL.pm
new file mode 100644
index 0000000..43c0440
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/SQL.pm
@@ -0,0 +1,96 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::SQL;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('sql');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing SQL for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->sql->query( body => {...} )
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with an C<sql>
+namespace, to support the
+L<SQL APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest.html>.
+
+The full documentation for the SQL feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-sql.html>
+
+=head1 GENERAL METHODS
+
+=head2 C<query()>
+
+    $response = $es->sql->query(
+        body    => {...} # required
+    )
+
+The C<query()> method executes an SQL query and returns the results.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<format>,
+    C<human>
+
+See the L<query docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest.html>
+for more information.
+
+=head2 C<translate()>
+
+    $response = $es->sql->translate(
+        body    => {...} # required
+    )
+
+The C<translate()> method takes an SQL query and returns the query DSL which would be executed.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<translate docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-translate.html>
+for more information.
+
+=head2 C<clear_cursor()>
+
+    $response = $es->sql->clear_cursor(
+        body    => {...} # required
+    )
+
+The C<clear_cursor()> method cleans up an ongoing scroll request.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<query docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-rest.html>
+for more information.
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/SSL.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/SSL.pm
new file mode 100644
index 0000000..b0b768c
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/SSL.pm
@@ -0,0 +1,49 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::SSL;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('ssl');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing SSL for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->ssl->certificates()
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with an C<ssl>
+namespace, to support the
+L<SSL APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-ssl.html>.
+
+=head1 GENERAL METHODS
+
+=head2 C<certificates()>
+
+    $response = $es->ssl->certificates()
+
+The C<certificates()> method returns all the certificate information on a single node of Elasticsearch.
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/SearchableSnapshots.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/SearchableSnapshots.pm
new file mode 100644
index 0000000..bd9b628
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/SearchableSnapshots.pm
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::SearchableSnapshots;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('searchable_snapshots');
+
+1;
+
+__END__
+
+# ABSTRACT: Searchable Snapshots feature of Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+The full documentation for Searchable Snapshots feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/searchable-snapshots-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->searchable_snapshots->repository_stats(
+        'repository' => $repository
+    );
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Security.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Security.pm
new file mode 100644
index 0000000..3bfb525
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Security.pm
@@ -0,0 +1,461 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Security;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('security');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Security API for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->security->authenticate();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<security>
+namespace, to support the
+L<Security APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api.html>.
+
+The full documentation for the Security feature is available here:
+L<https://www.elastic.co/guide/en/x-pack/current/xpack-security.html>
+
+=head1 GENERAL METHODS
+
+=head2 C<authenticate()>
+
+    $response = $es->security->authenticate()
+
+The C<authenticate()> method checks that the C<userinfo> is correct and returns
+a list of which roles are assigned to the user.
+
+See the L<authenticate docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-authenticate.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<clear_cached_realms()>
+
+    $response = $es->security->clear_cached_realms(
+        realms => $realms       # required  (comma-separated string)
+    );
+
+The C<clear_cached_realms()> method clears the caches for the specified realms
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<usernames>
+
+See the L<clear_cached_realms docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-cache.html>
+for more information.
+
+
+=head1 USER METHODS
+
+=head2 C<put_user()>
+
+    $response = $es->security->put_user(
+        username => $username,     # required
+        body     => {...}          # required
+    );
+
+The C<put_user()> method creates a new user or updates an existing user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_user()>
+
+    $response = $es->security->get_user(
+        username => $username | \@usernames     # optional
+    );
+
+The C<get_user()> method retrieves info for the specified users (or all users).
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_user()>
+
+    $response = $es->security->delete_user(
+        username => $username       # required
+    );
+
+The C<delete_user()> method deletes the specified user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<change_password()>
+
+    $response = $es->security->change_password(
+        username => $username       # optional
+        body => {
+            password => $password   # required
+        }
+    )
+
+The C<change_password()> method changes the password for the specified user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+=head2 C<disable_user()>
+
+    $response = $es->security->disable_user(
+        username => $username       # required
+    );
+
+The C<disable_user()> method disables the specified user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<enable_user()>
+
+    $response = $es->security->enable_user(
+        username => $username       # required
+    );
+
+The C<enable_user()> method enables the specified user.
+
+See the L<User Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 ROLE METHODS
+
+=head2 C<put_role()>
+
+    $response = $es->security->put_role(
+        name => $name,             # required
+        body     => {...}          # required
+    );
+
+The C<put_role()> method creates a new role or updates an existing role.
+
+See the L<Role Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_role()>
+
+    $response = $es->security->get_role(
+        name => $name | \@names     # optional
+    );
+
+The C<get_role()> method retrieves info for the specified roles (or all roles).
+
+See the L<Role Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_role()>
+
+    $response = $es->security->delete_role(
+        name => $name       # required
+    );
+
+The C<delete_role()> method deletes the specified role.
+
+See the L<Role Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<clear_cached_roles()>
+
+    $response = $es->security->clear_cached_roles(
+        names => $names       # required  (comma-separated string)
+    );
+
+The C<clear_cached_roles()> method clears the caches for the specified roles.
+
+See the L<Role Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+
+=head1 ROLE MAPPING METHODS
+
+=head2 C<put_role_mapping()>
+
+    $response = $es->security->put_role_mapping(
+        name => $name,             # required
+        body     => {...}          # required
+    );
+
+The C<put_role_mapping()> method creates a new role mapping or updates an existing role mapping.
+
+See the L<Role Mapping docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<get_role_mapping()>
+
+    $response = $es->security->get_role_mapping(
+        name => $name,             # optional
+    );
+
+The C<get_role_mapping()> method retrieves one or more role mappings.
+
+See the L<Role Mapping docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_role_mapping()>
+
+    $response = $es->security->delete_role_mapping(
+        name => $name,             # required
+    );
+
+The C<delete_role_mapping()> method deletes a role mapping.
+
+See the L<Role Mapping docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 TOKEN METHODS
+
+=head2 C<get_token()>
+
+    $response = $es->security->get_token(
+        body     => {...}          # required
+    );
+
+The C<get_token()> method enables you to create bearer tokens for access without
+requiring basic authentication.
+
+See the L<Token Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-tokens.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<invalidate_token()>
+
+    $response = $es->security->invalidate_token(
+        body     => {...}          # required
+    );
+
+The C<invalidate_token()> method enables you to invalidate bearer tokens for access without
+requiring basic authentication.
+
+See the L<Token Management docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-tokens.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head1 API KEY METHODS
+
+=head2 C<create_api_key()>
+
+    $response = $es->security->create_api_key(
+        body    => {...}            # required
+    )
+
+The C<create_api_key()> API is used to create API keys which can be used for access instead
+of basic authentication.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<refresh>
+
+See the L<Create API Key docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html> for more.
+
+=head2 C<get_api_key()>
+
+    $response = $es->security->get_api_key(
+        id          => $id,         # optional
+        name        => $name,       # optional
+        realm_name  => $realm,      # optional
+        username    => $username    # optional
+    )
+
+The C<get_api_key()> API is used to get information about an API key.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<id>,
+    C<name>,
+    C<realm_name>,
+    C<username>
+
+See the L<Get API Key docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-api-key.html> for more.
+
+=head2 C<invalidate_api_key()>
+
+    $response = $es->security->invalidate_api_key(
+        id          => $id,         # optional
+        name        => $name,       # optional
+        realm_name  => $realm,      # optional
+        username    => $username    # optional
+    )
+
+The C<invalidate_api_key()> API is used to invalidate an API key.
+
+Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<id>,
+    C<name>,
+    C<realm_name>,
+    C<username>
+
+See the L<Invalidate API Key docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-api-key.html> for more.
+
+=head1 USER PRIVILEGE METHODS
+
+=head2 C<get_user_privileges()>
+
+    $response = $es->get_user_privileges();
+
+ The C<get_user_privileges()> method retrieves the privileges granted to the current user.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+=head2 C<has_privileges()>
+    $response = $es->has_privileges(
+        user    => $user,   # optional
+        body    => {...}    # required
+    );
+
+ The C<has_privileges()> method checks whether the current or specified user has the listed privileges.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<Has Privileges docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-has-privileges.html> for more.
+
+
+=head1 APPLICATION PRIVILEGE METHODS
+
+=head2 C<put_privileges()>
+
+    $response = $es->put_privileges(
+        application     => $application,    # required
+        name            => $name,           # required
+        body            => {...}            # required
+    );
+
+ The C<put_privileges()> method creates or updates the named privilege for a particular application.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<refresh>
+
+See the L<Create or Update Application Privileges docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html> for more.
+
+=head2 C<get_privileges()>
+
+    $response = $es->get_privileges(
+        application     => $application,    # required
+        name            => $name,           # required
+    );
+
+ The C<get_privileges()> method retrieves the named privilege for a particular application.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>
+
+See the L<Get Application Privileges docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-privileges.html> for more.
+
+=head2 C<delete_privileges()>
+
+    $response = $es->delete_privileges(
+        application     => $application,    # required
+        name            => $name,           # required
+    );
+
+ The C<delete_privileges()> method deletes the named privilege for a particular application.
+
+ Query string parameters:
+    C<error_trace>,
+    C<filter_path>,
+    C<human>,
+    C<refresh>
+
+See the L<Delete Application Privileges docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-privilege.html> for more.
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Shutdown.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Shutdown.pm
new file mode 100644
index 0000000..7af39db
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Shutdown.pm
@@ -0,0 +1,43 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Shutdown;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('shutdown');
+
+1;
+
+__END__
+
+# ABSTRACT: Shutdown API of Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+The full documentation for Shutdown API is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/node-lifecycle-api.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->shutdown->get_node();
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Slm.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Slm.pm
new file mode 100644
index 0000000..3564440
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Slm.pm
@@ -0,0 +1,43 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Slm;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('slm');
+
+1;
+
+__END__
+
+# ABSTRACT: Snapshot lifecycle management feature of Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+The full documentation for Snapshot lifecycle management feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-lifecycle-management-api.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->slm->get_status();
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Snapshot.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Snapshot.pm
new file mode 100644
index 0000000..ae13ecd
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Snapshot.pm
@@ -0,0 +1,190 @@
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Client::8_0::Direct::Snapshot;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+__PACKAGE__->_install_api('snapshot');
+
+1;
+
+__END__
+
+# ABSTRACT: A client for managing snapshot/restore
+
+=head1 DESCRIPTION
+
+This module provides methods to manage snapshot/restore, or backups.
+It can create, get and delete configured backup repositories, and
+create, get, delete and restore snapshots of your cluster or indices.
+
+It does L<Search::Elasticsearch::Role::Client::Direct>.
+
+=head1 METHODS
+
+
+=head2 C<create_repository()>
+
+    $e->snapshot->create_repository(
+        repository  => 'repository',        # required
+        body        => { defn }             # required
+    );
+
+Create a repository for backups.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>,
+    C<verify>
+
+See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
+for more information.
+
+=head2 C<get_repository()>
+
+    $e->snapshot->get_repository(
+        repository  => 'repository' | \@repositories    # optional
+    );
+
+Retrieve existing repositories.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<local>,
+    C<master_timeout>
+
+See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
+for more information.
+
+=head2 C<verify_repository()>
+
+    $e->snapshot->verify_repository(
+        repository  => 'repository' # required
+    );
+
+Verify existing repository.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
+for more information.
+
+=head2 C<delete_repository()>
+
+    $e->snapshot->delete_repository(
+        repository  => 'repository' | \@repositories    # required
+    );
+
+Delete repositories by name.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<timeout>
+
+See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
+for more information.
+
+=head2 C<create()>
+
+    $e->snapshot->create(
+        repository  => 'repository',     # required
+        snapshot    => 'snapshot',       # required,
+
+        body        => { snapshot defn } # optional
+    );
+
+Create a snapshot of the whole cluster or individual indices in the named
+repository.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<wait_for_completion>
+
+=head2 C<get()>
+
+    $e->snapshot->get(
+        repository  => 'repository'                   # required
+        snapshot    => 'snapshot'   | \@snapshots     # required
+    );
+
+Retrieve snapshots in the named repository.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>,
+    C<verbose>
+
+See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
+for more information.
+
+=head2 C<delete()>
+
+    $e->snapshot->delete(
+        repository  => 'repository',              # required
+        snapshot    => 'snapshot'                 # required
+    );
+
+Delete snapshot in the named repository.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>
+
+See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
+for more information.
+
+
+=head2 C<restore()>
+
+    $e->snapshot->restore(
+        repository  => 'repository',              # required
+        snapshot    => 'snapshot'                 # required
+
+        body        => { what to restore }        # optional
+    );
+
+Restore a named snapshot.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>,
+    C<wait_for_completion>
+
+See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
+for more information.
+
+=head2 C<status()>
+
+    $result = $e->snapshot->status(
+        repository  => 'repository',              # optional
+        snapshot    => 'snapshot' | \@snapshots   # optional
+    );
+
+Returns status information about the specified snapshots.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<ignore_unavailable>,
+    C<master_timeout>
+
+See the L<"snapshot/restore docs"|http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-snapshot.html>
+for more information.
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Tasks.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Tasks.pm
new file mode 100644
index 0000000..e6f0b03
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Tasks.pm
@@ -0,0 +1,97 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Tasks;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+__PACKAGE__->_install_api('tasks');
+
+1;
+
+__END__
+
+# ABSTRACT: A client for accessing the Task Management API
+
+=head1 DESCRIPTION
+
+This module provides methods to access the Task Management API, such as listing
+tasks and cancelling tasks.
+
+It does L<Search::Elasticsearch::Role::Client::Direct>.
+
+=head1 METHODS
+
+=head2 C<list()>
+
+    $response = $e->tasks->list(
+        task_id => $task_id  # optional
+    );
+
+The C<list()> method returns all running tasks or, if a C<task_id> is specified, info
+about that task.
+
+Query string parameters:
+    C<actions>,
+    C<detailed>,
+    C<error_trace>,
+    C<group_by>,
+    C<human>,
+    C<nodes>,
+    C<parent_task_id>,
+    C<timeout>,
+    C<wait_for_completion>
+
+See the L<task management docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
+for more information.
+
+=head2 C<get()>
+
+    $response = $e->tasks->get(
+        task_id => $task_id  # required
+    );
+
+The C<get()> method returns the task with the specified ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<wait_for_completion>
+
+See the L<task management docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
+for more information.
+
+=head2 C<cancel()>
+
+    $response = $e->tasks->cancel(
+        task_id => $task_id  # required
+    );
+
+The C<cancel()> method attempts to cancel the specified C<task_id> or multiple tasks.
+
+Query string parameters:
+    C<actions>,
+    C<error_trace>,
+    C<human>,
+    C<nodes>,
+    C<parent_task_id>,
+    C<timeout>
+
+See the L<task management docs|http://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html>
+for more information.
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Transform.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Transform.pm
new file mode 100644
index 0000000..ab0f643
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Transform.pm
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Transform;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('transform');
+
+1;
+
+__END__
+
+# ABSTRACT: Transform feature of Search::Elasticsearch 8.x
+
+=head2 DESCRIPTION
+
+The full documentation for Transform feature is available here:
+L<https://www.elastic.co/guide/en/elasticsearch/reference/current/transform-apis.html>
+
+=head1 FOLLOW METHODS
+
+=head2 C<follow()>
+
+    my $response = $es->transform->get_transform(
+        'transform_id' => $transform_id
+    );
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/Watcher.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/Watcher.pm
new file mode 100644
index 0000000..23e9d92
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/Watcher.pm
@@ -0,0 +1,225 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::Watcher;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('watcher');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing Watcher API for Search::Elasticsearch 8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->watcher->start();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<watcher>
+namespace, to support the
+L<Watcher APIs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api.html>.
+
+=head1 METHODS
+
+The full documentation for the Watcher feature is available here:
+L<https://www.elastic.co/guide/en/x-pack/current/xpack-alerting.html>
+
+=head2 C<put_watch()>
+
+    $response = $es->watcher->put_watch(
+        id    => $watch_id,     # required
+        body  => {...}
+    );
+
+The C<put_watch()> method is used to register a new watcher or to update
+an existing watcher.
+
+See the L<put_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-put-watch.html>
+for more information.
+
+Query string parameters:
+    C<active>,
+    C<error_trace>,
+    C<human>,
+    C<if_primary_term>,
+    C<if_seq_no>,
+    C<master_timeout>,
+    C<version>
+
+=head2 C<get_watch()>
+
+    $response = $es->watcher->get_watch(
+        id    => $watch_id,     # required
+    );
+
+The C<get_watch()> method is used to retrieve a watch by ID.
+
+See the L<get_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-get-watch.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<delete_watch()>
+
+    $response = $es->watcher->delete_watch(
+        id    => $watch_id,     # required
+    );
+
+The C<delete_watch()> method is used to delete a watch by ID.
+
+Query string parameters:
+    C<error_trace>,
+    C<force>,
+    C<human>,
+    C<master_timeout>
+
+See the L<delete_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-delete-watch.html>
+for more information.
+
+=head2 C<execute_watch()>
+
+    $response = $es->watcher->execute_watch(
+        id    => $watch_id,     # optional
+        body  => {...}          # optional
+    );
+
+The C<execute_watch()> method forces the execution of a previously
+registered watch.  Optional parameters may be passed in the C<body>.
+
+Query string parameters:
+    C<debug>,
+    C<error_trace>,
+    C<human>
+
+See the L<execute_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-execute-watch.html>
+for more information.
+
+=head2 C<ack_watch()>
+
+    $response = $es->watcher->ack_watch(
+        watch_id => $watch_id,                  # required
+        action_id => $action_id | \@action_ids  # optional
+    );
+
+The C<ack_watch()> method is used to manually throttle the execution of
+a watch.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>
+
+See the L<ack_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-ack-watch.html>
+for more information.
+
+=head2 C<activate_watch()>
+
+    $response = $es->watcher->activate_watch(
+        watch_id => $watch_id,                  # required
+    );
+
+The C<activate_watch()> method is used to activate a deactive watch.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>
+
+See the L<activate_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-activate-watch.html>
+for more information.
+
+=head2 C<deactivate_watch()>
+
+    $response = $es->watcher->deactivate_watch(
+        watch_id => $watch_id,                  # required
+    );
+
+The C<deactivate_watch()> method is used to deactivate an active watch.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>,
+    C<master_timeout>
+
+See the L<deactivate_watch docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-deactivate-watch.html>
+for more information.
+
+=head2 C<stats()>
+
+    $response = $es->watcher->stats(
+        metric => $metric       # optional
+    );
+
+The C<stats()> method returns information about the status of the watcher plugin.
+
+See the L<stats docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-stats.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<stop()>
+
+    $response = $es->watcher->stop();
+
+The C<stop()> method stops the watcher service if it is running.
+
+See the L<stop docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-stop.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<start()>
+
+    $response = $es->watcher->start();
+
+The C<start()> method starts the watcher service if it is not already running.
+
+See the L<start docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-start.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+=head2 C<restart()>
+
+    $response = $es->watcher->restart();
+
+The C<restart()> method stops then starts the watcher service.
+
+See the L<restart docs|https://www.elastic.co/guide/en/elasticsearch/reference/current/watcher-api-restart.html>
+for more information.
+
+Query string parameters:
+    C<error_trace>,
+    C<human>
+
+
+
diff --git a/lib/Search/Elasticsearch/Client/8_0/Direct/XPack.pm b/lib/Search/Elasticsearch/Client/8_0/Direct/XPack.pm
new file mode 100644
index 0000000..5ca4da3
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Direct/XPack.pm
@@ -0,0 +1,60 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Direct::XPack;
+
+use Moo;
+with 'Search::Elasticsearch::Client::8_0::Role::API';
+with 'Search::Elasticsearch::Role::Client::Direct';
+use namespace::clean;
+
+__PACKAGE__->_install_api('xpack');
+
+1;
+
+__END__
+
+# ABSTRACT: Plugin providing XPack APIs for Search::Elasticsearch v8.x
+
+=head1 SYNOPSIS
+
+    my $response = $es->xpack->info();
+
+=head2 DESCRIPTION
+
+This class extends the L<Search::Elasticsearch> client with a C<xpack>
+namespace.
+
+=head1 METHODS
+
+=head2 C<info()>
+
+    my $response = $es->xpack->info();
+
+Provides general information about the installed X-Pack features.
+
+See the L<info|https://www.elastic.co/guide/en/elasticsearch/reference/current/info-api.html>
+for more information.
+
+=head2 C<usage()>
+
+    my $response = $es->xpack->usage();
+
+Provides usage information about the installed X-Pack features.
+
+See the L<usage|https://www.elastic.co/guide/en/elasticsearch/reference/current/usage-api.html>
+for more information.
\ No newline at end of file
diff --git a/lib/Search/Elasticsearch/Client/8_0/Role/API.pm b/lib/Search/Elasticsearch/Client/8_0/Role/API.pm
new file mode 100644
index 0000000..a32e575
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Role/API.pm
@@ -0,0 +1,7092 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Role::API;
+
+use Moo::Role;
+with 'Search::Elasticsearch::Role::API';
+
+use Search::Elasticsearch::Util qw(throw);
+use namespace::clean;
+
+has 'api_version' => ( is => 'ro', default => '8_0' );
+
+our %API;
+
+#===================================
+sub api {
+#===================================
+    my $name = $_[1] || return \%API;
+    return $API{$name}
+        || throw( 'Internal', "Unknown api name ($name)" );
+}
+
+#===================================
+%API = (
+#===================================
+
+    'bulk.metadata' => {
+        params => {
+            '_index'                 => '_index',
+            'index'                  => '_index',
+            '_id'                    => '_id',
+            'id'                     => '_id',
+            'pipeline'               => 'pipeline',
+            'routing'                => 'routing',
+            '_routing'               => 'routing',
+            'parent'                 => 'parent',
+            '_parent'                => 'parent',
+            'timestamp'              => 'timestamp',
+            '_timestamp'             => 'timestamp',
+            'ttl'                    => 'ttl',
+            '_ttl'                   => 'ttl',
+            'version'                => 'version',
+            '_version'               => 'version',
+            'version_type'           => 'version_type',
+            '_version_type'          => 'version_type',
+            'if_seq_no'              => 'if_seq_no',
+            'if_primary_term'        => 'if_primary_term',
+            'lang'                   => 'lang',
+            'require_alias'          => 'require_alias',
+            'refresh'                => 'refresh',
+            'retry_on_conflict'      => 'retru_on_conflict',
+            'wait_for_active_shards' => 'wait_for_active_shards',
+            '_source'                => '_source',
+            '_source_excludes'       => '_source_excludes',
+            '_source_includes'       => '_source_includes',
+            'timeout'                => 'timeout'
+        }
+    },
+    'bulk.update' => {
+        params => [
+            '_source',          '_source_includes',
+            '_source_excludes', 'detect_noop',
+            'doc',              'doc_as_upsert',
+            'fields',           'retry_on_conflict',
+            'scripted_upsert',  'script',
+            'upsert',           'lang',
+            'params'
+        ]
+    },
+    'bulk.required' => { params => ['index'] },
+
+#=== AUTOGEN - START ===
+
+    'bulk' => {
+        body   => { required => 1 },
+        doc    => "docs-bulk",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_bulk" ], [ {}, "_bulk" ] ],
+        qs     => {
+            _source                => "list",
+            _source_excludes       => "list",
+            _source_includes       => "list",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            pipeline               => "string",
+            refresh                => "enum",
+            require_alias          => "boolean",
+            routing                => "string",
+            timeout                => "time",
+            type                   => "string",
+            wait_for_active_shards => "string",
+        },
+        serialize => "bulk",
+    },
+
+    'clear_scroll' => {
+        body   => {},
+        doc    => "clear-scroll-api",
+        method => "DELETE",
+        parts  => { scroll_id => { multi => 1 } },
+        paths  => [
+            [ { scroll_id => 2 }, "_search", "scroll", "{scroll_id}" ],
+            [ {}, "_search", "scroll" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'close_point_in_time' => {
+        body   => {},
+        doc    => "point-in-time-api",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_pit" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'count' => {
+        body   => {},
+        doc    => "search-count",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  =>
+            [ [ { index => 0 }, "{index}", "_count" ], [ {}, "_count" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            analyze_wildcard   => "boolean",
+            analyzer           => "string",
+            default_operator   => "enum",
+            df                 => "string",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_throttled   => "boolean",
+            ignore_unavailable => "boolean",
+            lenient            => "boolean",
+            min_score          => "number",
+            preference         => "string",
+            q                  => "string",
+            routing            => "list",
+            terminate_after    => "number",
+        },
+    },
+
+    'create' => {
+        body   => { required => 1 },
+        doc    => "docs-index_",
+        method => "PUT",
+        parts  => { id => {}, index => {} },
+        paths  =>
+            [ [ { id => 2, index => 0 }, "{index}", "_create", "{id}" ] ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            pipeline               => "string",
+            refresh                => "enum",
+            routing                => "string",
+            timeout                => "time",
+            version                => "number",
+            version_type           => "enum",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'delete' => {
+        doc    => "docs-delete",
+        method => "DELETE",
+        parts  => { id => {}, index => {} },
+        paths  => [ [ { id => 2, index => 0 }, "{index}", "_doc", "{id}" ] ],
+        qs     => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            if_primary_term        => "number",
+            if_seq_no              => "number",
+            refresh                => "enum",
+            routing                => "string",
+            timeout                => "time",
+            version                => "number",
+            version_type           => "enum",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'delete_by_query' => {
+        body   => { required => 1 },
+        doc    => "docs-delete-by-query",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}", "_delete_by_query" ] ],
+        qs     => {
+            allow_no_indices       => "boolean",
+            analyze_wildcard       => "boolean",
+            analyzer               => "string",
+            conflicts              => "enum",
+            default_operator       => "enum",
+            df                     => "string",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            from                   => "number",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            lenient                => "boolean",
+            max_docs               => "number",
+            preference             => "string",
+            q                      => "string",
+            refresh                => "boolean",
+            request_cache          => "boolean",
+            requests_per_second    => "number",
+            routing                => "list",
+            scroll                 => "time",
+            scroll_size            => "number",
+            search_timeout         => "time",
+            search_type            => "enum",
+            slices                 => "number|string",
+            sort                   => "list",
+            stats                  => "list",
+            terminate_after        => "number",
+            timeout                => "time",
+            version                => "boolean",
+            wait_for_active_shards => "string",
+            wait_for_completion    => "boolean",
+        },
+    },
+
+    'delete_by_query_rethrottle' => {
+        doc    => "docs-delete-by-query",
+        method => "POST",
+        parts  => { task_id => {} },
+        paths  => [
+            [   { task_id => 1 }, "_delete_by_query",
+                "{task_id}",      "_rethrottle",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            requests_per_second => "number",
+        },
+    },
+
+    'delete_script' => {
+        doc    => "modules-scripting",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 1 }, "_scripts", "{id}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'exists' => {
+        doc    => "docs-get",
+        method => "HEAD",
+        parts  => { id => {}, index => {} },
+        paths  => [ [ { id => 2, index => 0 }, "{index}", "_doc", "{id}" ] ],
+        qs     => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            refresh          => "boolean",
+            routing          => "string",
+            stored_fields    => "list",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'exists_source' => {
+        doc    => "docs-get",
+        method => "HEAD",
+        parts  => { id => {}, index => {} },
+        paths  =>
+            [ [ { id => 2, index => 0 }, "{index}", "_source", "{id}" ] ],
+        qs => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            refresh          => "boolean",
+            routing          => "string",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'explain' => {
+        body  => {},
+        doc   => "search-explain",
+        parts => { id => {}, index => {} },
+        paths =>
+            [ [ { id => 2, index => 0 }, "{index}", "_explain", "{id}" ] ],
+        qs => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            analyze_wildcard => "boolean",
+            analyzer         => "string",
+            default_operator => "enum",
+            df               => "string",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            lenient          => "boolean",
+            preference       => "string",
+            q                => "string",
+            routing          => "string",
+            stored_fields    => "list",
+        },
+    },
+
+    'field_caps' => {
+        body  => {},
+        doc   => "search-field-caps",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_field_caps" ],
+            [ {}, "_field_caps" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            fields             => "list",
+            filter_path        => "list",
+            filters            => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_unmapped   => "boolean",
+            types              => "list",
+        },
+    },
+
+    'get' => {
+        doc   => "docs-get",
+        parts => { id => {}, index => {} },
+        paths => [ [ { id => 2, index => 0 }, "{index}", "_doc", "{id}" ] ],
+        qs    => {
+            _source                => "list",
+            _source_excludes       => "list",
+            _source_includes       => "list",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            force_synthetic_source => "boolean",
+            human                  => "boolean",
+            preference             => "string",
+            realtime               => "boolean",
+            refresh                => "boolean",
+            routing                => "string",
+            stored_fields          => "list",
+            version                => "number",
+            version_type           => "enum",
+        },
+    },
+
+    'get_script' => {
+        doc   => "modules-scripting",
+        parts => { id => {} },
+        paths => [ [ { id => 1 }, "_scripts", "{id}" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'get_script_context' => {
+        doc   => "painless-contexts",
+        parts => {},
+        paths => [ [ {}, "_script_context" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'get_script_languages' => {
+        doc   => "modules-scripting",
+        parts => {},
+        paths => [ [ {}, "_script_language" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'get_source' => {
+        doc   => "docs-get",
+        parts => { id => {}, index => {} },
+        paths =>
+            [ [ { id => 2, index => 0 }, "{index}", "_source", "{id}" ] ],
+        qs => {
+            _source          => "list",
+            _source_excludes => "list",
+            _source_includes => "list",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            refresh          => "boolean",
+            routing          => "string",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'index' => {
+        body   => { required => 1 },
+        doc    => "docs-index_",
+        method => "POST",
+        parts  => { id => {}, index => {} },
+        paths  => [
+            [ { id    => 2, index => 0 }, "{index}", "_doc", "{id}" ],
+            [ { index => 0 }, "{index}", "_doc" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            if_primary_term        => "number",
+            if_seq_no              => "number",
+            op_type                => "enum",
+            pipeline               => "string",
+            refresh                => "enum",
+            require_alias          => "boolean",
+            routing                => "string",
+            timeout                => "time",
+            version                => "number",
+            version_type           => "enum",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'info' => {
+        doc   => "index",
+        parts => {},
+        paths => [ [ {} ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'knn_search' => {
+        body  => {},
+        doc   => "search-search",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}", "_knn_search" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            routing     => "list",
+        },
+    },
+
+    'mget' => {
+        body  => { required => 1 },
+        doc   => "docs-multi-get",
+        parts => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_mget" ], [ {}, "_mget" ] ],
+        qs    => {
+            _source                => "list",
+            _source_excludes       => "list",
+            _source_includes       => "list",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            force_synthetic_source => "boolean",
+            human                  => "boolean",
+            preference             => "string",
+            realtime               => "boolean",
+            refresh                => "boolean",
+            routing                => "string",
+            stored_fields          => "list",
+        },
+    },
+
+    'msearch' => {
+        body  => { required => 1 },
+        doc   => "search-multi-search",
+        parts => { index => { multi => 1 } },
+        paths =>
+            [ [ { index => 0 }, "{index}", "_msearch" ], [ {}, "_msearch" ] ],
+        qs => {
+            ccs_minimize_roundtrips       => "boolean",
+            error_trace                   => "boolean",
+            filter_path                   => "list",
+            human                         => "boolean",
+            max_concurrent_searches       => "number",
+            max_concurrent_shard_requests => "number",
+            pre_filter_shard_size         => "number",
+            rest_total_hits_as_int        => "boolean",
+            search_type                   => "enum",
+            typed_keys                    => "boolean",
+        },
+        serialize => "bulk",
+    },
+
+    'msearch_template' => {
+        body  => { required => 1 },
+        doc   => "search-multi-search",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_msearch", "template" ],
+            [ {}, "_msearch", "template" ],
+        ],
+        qs => {
+            ccs_minimize_roundtrips => "boolean",
+            error_trace             => "boolean",
+            filter_path             => "list",
+            human                   => "boolean",
+            max_concurrent_searches => "number",
+            rest_total_hits_as_int  => "boolean",
+            search_type             => "enum",
+            typed_keys              => "boolean",
+        },
+        serialize => "bulk",
+    },
+
+    'mtermvectors' => {
+        body  => {},
+        doc   => "docs-multi-termvectors",
+        parts => { index => {} },
+        paths => [
+            [ { index => 0 }, "{index}", "_mtermvectors" ],
+            [ {}, "_mtermvectors" ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            field_statistics => "boolean",
+            fields           => "list",
+            filter_path      => "list",
+            human            => "boolean",
+            ids              => "list",
+            offsets          => "boolean",
+            payloads         => "boolean",
+            positions        => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            routing          => "string",
+            term_statistics  => "boolean",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'open_point_in_time' => {
+        doc    => "point-in-time-api",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}", "_pit" ] ],
+        qs     => {
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            keep_alive         => "string",
+            preference         => "string",
+            routing            => "string",
+        },
+    },
+
+    'ping' => {
+        doc    => "index",
+        method => "HEAD",
+        parts  => {},
+        paths  => [ [ {} ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'put_script' => {
+        body   => { required => 1 },
+        doc    => "modules-scripting",
+        method => "PUT",
+        parts  => { context => {}, id => {} },
+        paths  => [
+            [ { context => 2, id => 1 }, "_scripts", "{id}", "{context}" ],
+            [ { id => 1 }, "_scripts", "{id}" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'rank_eval' => {
+        body  => { required => 1 },
+        doc   => "search-rank-eval",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_rank_eval" ],
+            [ {}, "_rank_eval" ]
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            search_type        => "enum",
+        },
+    },
+
+    'reindex' => {
+        body   => { required => 1 },
+        doc    => "docs-reindex",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_reindex" ] ],
+        qs     => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            max_docs               => "number",
+            refresh                => "boolean",
+            requests_per_second    => "number",
+            scroll                 => "time",
+            slices                 => "number|string",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+            wait_for_completion    => "boolean",
+        },
+    },
+
+    'reindex_rethrottle' => {
+        doc    => "docs-reindex",
+        method => "POST",
+        parts  => { task_id => {} },
+        paths  =>
+            [ [ { task_id => 1 }, "_reindex", "{task_id}", "_rethrottle" ] ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            requests_per_second => "number",
+        },
+    },
+
+    'render_search_template' => {
+        body  => {},
+        doc   => "render-search-template-api",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_render", "template", "{id}" ],
+            [ {}, "_render", "template" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'scripts_painless_execute' => {
+        body  => {},
+        doc   => "painless-execute-api",
+        parts => {},
+        paths => [ [ {}, "_scripts", "painless", "_execute" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'scroll' => {
+        body  => {},
+        doc   => "",
+        parts => { scroll_id => {} },
+        paths => [
+            [ { scroll_id => 2 }, "_search", "scroll", "{scroll_id}" ],
+            [ {}, "_search", "scroll" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            rest_total_hits_as_int => "boolean",
+            scroll                 => "time",
+        },
+    },
+
+    'search' => {
+        body  => {},
+        doc   => "search-search",
+        parts => { index => { multi => 1 } },
+        paths =>
+            [ [ { index => 0 }, "{index}", "_search" ], [ {}, "_search" ] ],
+        qs => {
+            _source                       => "list",
+            _source_excludes              => "list",
+            _source_includes              => "list",
+            allow_no_indices              => "boolean",
+            allow_partial_search_results  => "boolean",
+            analyze_wildcard              => "boolean",
+            analyzer                      => "string",
+            batched_reduce_size           => "number",
+            ccs_minimize_roundtrips       => "boolean",
+            default_operator              => "enum",
+            df                            => "string",
+            docvalue_fields               => "list",
+            error_trace                   => "boolean",
+            expand_wildcards              => "enum",
+            explain                       => "boolean",
+            filter_path                   => "list",
+            force_synthetic_source        => "boolean",
+            from                          => "number",
+            human                         => "boolean",
+            ignore_throttled              => "boolean",
+            ignore_unavailable            => "boolean",
+            lenient                       => "boolean",
+            max_concurrent_shard_requests => "number",
+            min_compatible_shard_node     => "string",
+            pre_filter_shard_size         => "number",
+            preference                    => "string",
+            q                             => "string",
+            request_cache                 => "boolean",
+            rest_total_hits_as_int        => "boolean",
+            routing                       => "list",
+            scroll                        => "time",
+            search_type                   => "enum",
+            seq_no_primary_term           => "boolean",
+            size                          => "number",
+            sort                          => "list",
+            stats                         => "list",
+            stored_fields                 => "list",
+            suggest_field                 => "string",
+            suggest_mode                  => "enum",
+            suggest_size                  => "number",
+            suggest_text                  => "string",
+            terminate_after               => "number",
+            timeout                       => "time",
+            track_scores                  => "boolean",
+            track_total_hits              => "boolean|long",
+            typed_keys                    => "boolean",
+            version                       => "boolean",
+        },
+    },
+
+    'search_mvt' => {
+        body   => {},
+        doc    => "search-vector-tile-api",
+        method => "POST",
+        parts  => {
+            field => {},
+            index => { multi => 1 },
+            x     => {},
+            y     => {},
+            zoom  => {}
+        },
+        paths => [
+            [   { field => 2, index => 0, x => 4, y => 5, zoom => 3 },
+                "{index}", "_mvt", "{field}", "{zoom}", "{x}", "{y}",
+            ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            exact_bounds     => "boolean",
+            extent           => "int",
+            filter_path      => "list",
+            grid_precision   => "int",
+            grid_type        => "enum",
+            human            => "boolean",
+            size             => "int",
+            track_total_hits => "boolean|long",
+            with_labels      => "boolean",
+        },
+    },
+
+    'search_shards' => {
+        doc   => "search-shards",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_search_shards" ],
+            [ {}, "_search_shards" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            local              => "boolean",
+            preference         => "string",
+            routing            => "string",
+        },
+    },
+
+    'search_template' => {
+        body  => { required => 1 },
+        doc   => "search-template",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_search", "template" ],
+            [ {}, "_search", "template" ],
+        ],
+        qs => {
+            allow_no_indices        => "boolean",
+            ccs_minimize_roundtrips => "boolean",
+            error_trace             => "boolean",
+            expand_wildcards        => "enum",
+            explain                 => "boolean",
+            filter_path             => "list",
+            human                   => "boolean",
+            ignore_throttled        => "boolean",
+            ignore_unavailable      => "boolean",
+            preference              => "string",
+            profile                 => "boolean",
+            rest_total_hits_as_int  => "boolean",
+            routing                 => "list",
+            scroll                  => "time",
+            search_type             => "enum",
+            typed_keys              => "boolean",
+        },
+    },
+
+    'terms_enum' => {
+        body  => {},
+        doc   => "search-terms-enum",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}", "_terms_enum" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'termvectors' => {
+        body  => {},
+        doc   => "docs-termvectors",
+        parts => { id => {}, index => {} },
+        paths => [
+            [ { id    => 2, index => 0 }, "{index}", "_termvectors", "{id}" ],
+            [ { index => 0 }, "{index}", "_termvectors" ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            field_statistics => "boolean",
+            fields           => "list",
+            filter_path      => "list",
+            human            => "boolean",
+            offsets          => "boolean",
+            payloads         => "boolean",
+            positions        => "boolean",
+            preference       => "string",
+            realtime         => "boolean",
+            routing          => "string",
+            term_statistics  => "boolean",
+            version          => "number",
+            version_type     => "enum",
+        },
+    },
+
+    'update' => {
+        body   => { required => 1 },
+        doc    => "docs-update",
+        method => "POST",
+        parts  => { id => {}, index => {} },
+        paths  =>
+            [ [ { id => 2, index => 0 }, "{index}", "_update", "{id}" ] ],
+        qs => {
+            _source                => "list",
+            _source_excludes       => "list",
+            _source_includes       => "list",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            if_primary_term        => "number",
+            if_seq_no              => "number",
+            lang                   => "string",
+            refresh                => "enum",
+            require_alias          => "boolean",
+            retry_on_conflict      => "number",
+            routing                => "string",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'update_by_query' => {
+        body   => {},
+        doc    => "docs-update-by-query",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}", "_update_by_query" ] ],
+        qs     => {
+            allow_no_indices       => "boolean",
+            analyze_wildcard       => "boolean",
+            analyzer               => "string",
+            conflicts              => "enum",
+            default_operator       => "enum",
+            df                     => "string",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            from                   => "number",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            lenient                => "boolean",
+            max_docs               => "number",
+            pipeline               => "string",
+            preference             => "string",
+            q                      => "string",
+            refresh                => "boolean",
+            request_cache          => "boolean",
+            requests_per_second    => "number",
+            routing                => "list",
+            scroll                 => "time",
+            scroll_size            => "number",
+            search_timeout         => "time",
+            search_type            => "enum",
+            slices                 => "number|string",
+            sort                   => "list",
+            stats                  => "list",
+            terminate_after        => "number",
+            timeout                => "time",
+            version                => "boolean",
+            version_type           => "boolean",
+            wait_for_active_shards => "string",
+            wait_for_completion    => "boolean",
+        },
+    },
+
+    'update_by_query_rethrottle' => {
+        doc    => "docs-update-by-query",
+        method => "POST",
+        parts  => { task_id => {} },
+        paths  => [
+            [   { task_id => 1 }, "_update_by_query",
+                "{task_id}",      "_rethrottle",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            requests_per_second => "number",
+        },
+    },
+
+    'async_search.delete' => {
+        doc    => "async-search",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 1 }, "_async_search", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'async_search.get' => {
+        doc   => "async-search",
+        parts => { id => {} },
+        paths => [ [ { id => 1 }, "_async_search", "{id}" ] ],
+        qs    => {
+            error_trace                 => "boolean",
+            filter_path                 => "list",
+            human                       => "boolean",
+            keep_alive                  => "time",
+            typed_keys                  => "boolean",
+            wait_for_completion_timeout => "time",
+        },
+    },
+
+    'async_search.status' => {
+        doc   => "async-search",
+        parts => { id => {} },
+        paths => [ [ { id => 2 }, "_async_search", "status", "{id}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'async_search.submit' => {
+        body   => {},
+        doc    => "async-search",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [ { index => 0 }, "{index}", "_async_search" ],
+            [ {}, "_async_search" ],
+        ],
+        qs => {
+            _source                       => "list",
+            _source_excludes              => "list",
+            _source_includes              => "list",
+            allow_no_indices              => "boolean",
+            allow_partial_search_results  => "boolean",
+            analyze_wildcard              => "boolean",
+            analyzer                      => "string",
+            batched_reduce_size           => "number",
+            default_operator              => "enum",
+            df                            => "string",
+            docvalue_fields               => "list",
+            error_trace                   => "boolean",
+            expand_wildcards              => "enum",
+            explain                       => "boolean",
+            filter_path                   => "list",
+            from                          => "number",
+            human                         => "boolean",
+            ignore_throttled              => "boolean",
+            ignore_unavailable            => "boolean",
+            keep_alive                    => "time",
+            keep_on_completion            => "boolean",
+            lenient                       => "boolean",
+            max_concurrent_shard_requests => "number",
+            preference                    => "string",
+            q                             => "string",
+            request_cache                 => "boolean",
+            routing                       => "list",
+            search_type                   => "enum",
+            seq_no_primary_term           => "boolean",
+            size                          => "number",
+            sort                          => "list",
+            stats                         => "list",
+            stored_fields                 => "list",
+            suggest_field                 => "string",
+            suggest_mode                  => "enum",
+            suggest_size                  => "number",
+            suggest_text                  => "string",
+            terminate_after               => "number",
+            timeout                       => "time",
+            track_scores                  => "boolean",
+            track_total_hits              => "boolean|long",
+            typed_keys                    => "boolean",
+            version                       => "boolean",
+            wait_for_completion_timeout   => "time",
+        },
+    },
+
+    'autoscaling.delete_autoscaling_policy' => {
+        doc    => "autoscaling-delete-autoscaling-policy",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_autoscaling", "policy", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'autoscaling.get_autoscaling_capacity' => {
+        doc   => "autoscaling-get-autoscaling-capacity",
+        parts => {},
+        paths => [ [ {}, "_autoscaling", "capacity" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'autoscaling.get_autoscaling_policy' => {
+        doc   => "autoscaling-get-autoscaling-policy",
+        parts => { name => {} },
+        paths => [ [ { name => 2 }, "_autoscaling", "policy", "{name}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'autoscaling.put_autoscaling_policy' => {
+        body   => { required => 1 },
+        doc    => "autoscaling-put-autoscaling-policy",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_autoscaling", "policy", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'cat.aliases' => {
+        doc   => "cat-alias",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 2 }, "_cat", "aliases", "{name}" ],
+            [ {}, "_cat", "aliases" ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            expand_wildcards => "enum",
+            filter_path      => "list",
+            format           => "string",
+            h                => "list",
+            help             => "boolean",
+            human            => "boolean",
+            local            => "boolean",
+            s                => "list",
+            v                => "boolean",
+        },
+    },
+
+    'cat.allocation' => {
+        doc   => "cat-allocation",
+        parts => { node_id => { multi => 1 } },
+        paths => [
+            [ { node_id => 2 }, "_cat", "allocation", "{node_id}" ],
+            [ {}, "_cat", "allocation" ],
+        ],
+        qs => {
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.component_templates' => {
+        doc   => "cat-compoentn-templates",
+        parts => { name => {} },
+        paths => [
+            [ { name => 2 }, "_cat", "component_templates", "{name}" ],
+            [ {}, "_cat", "component_templates" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.count' => {
+        doc   => "cat-count",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "count", "{index}" ],
+            [ {}, "_cat", "count" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            v           => "boolean",
+        },
+    },
+
+    'cat.fielddata' => {
+        doc   => "cat-fielddata",
+        parts => { fields => { multi => 1 } },
+        paths => [
+            [ { fields => 2 }, "_cat", "fielddata", "{fields}" ],
+            [ {}, "_cat", "fielddata" ],
+        ],
+        qs => {
+            bytes       => "enum",
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            v           => "boolean",
+        },
+    },
+
+    'cat.health' => {
+        doc   => "cat-health",
+        parts => {},
+        paths => [ [ {}, "_cat", "health" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            time        => "enum",
+            ts          => "boolean",
+            v           => "boolean",
+        },
+    },
+
+    'cat.help' => {
+        doc   => "cat",
+        parts => {},
+        paths => [ [ {}, "_cat" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+        },
+    },
+
+    'cat.indices' => {
+        doc   => "cat-indices",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "indices", "{index}" ],
+            [ {}, "_cat", "indices" ],
+        ],
+        qs => {
+            bytes                     => "enum",
+            error_trace               => "boolean",
+            expand_wildcards          => "enum",
+            filter_path               => "list",
+            format                    => "string",
+            h                         => "list",
+            health                    => "enum",
+            help                      => "boolean",
+            human                     => "boolean",
+            include_unloaded_segments => "boolean",
+            master_timeout            => "time",
+            pri                       => "boolean",
+            s                         => "list",
+            time                      => "enum",
+            v                         => "boolean",
+        },
+    },
+
+    'cat.master' => {
+        doc   => "cat-master",
+        parts => {},
+        paths => [ [ {}, "_cat", "master" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.ml_data_frame_analytics' => {
+        doc   => "cat-dfanalytics",
+        parts => { id => {} },
+        paths => [
+            [ { id => 4 }, "_cat", "ml", "data_frame", "analytics", "{id}" ],
+            [ {}, "_cat", "ml", "data_frame", "analytics" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.ml_datafeeds' => {
+        doc   => "cat-datafeeds",
+        parts => { datafeed_id => {} },
+        paths => [
+            [   { datafeed_id => 3 }, "_cat",
+                "ml",                 "datafeeds",
+                "{datafeed_id}"
+            ],
+            [ {}, "_cat", "ml", "datafeeds" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.ml_jobs' => {
+        doc   => "cat-anomaly-detectors",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 3 }, "_cat", "ml", "anomaly_detectors",
+                "{job_id}"
+            ],
+            [ {}, "_cat", "ml", "anomaly_detectors" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.ml_trained_models' => {
+        doc   => "cat-trained-model",
+        parts => { model_id => {} },
+        paths => [
+            [   { model_id => 3 }, "_cat",
+                "ml",              "trained_models",
+                "{model_id}"
+            ],
+            [ {}, "_cat", "ml", "trained_models" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            from           => "int",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            s              => "list",
+            size           => "int",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.nodeattrs' => {
+        doc   => "cat-nodeattrs",
+        parts => {},
+        paths => [ [ {}, "_cat", "nodeattrs" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.nodes' => {
+        doc   => "cat-nodes",
+        parts => {},
+        paths => [ [ {}, "_cat", "nodes" ] ],
+        qs    => {
+            bytes                     => "enum",
+            error_trace               => "boolean",
+            filter_path               => "list",
+            format                    => "string",
+            full_id                   => "boolean",
+            h                         => "list",
+            help                      => "boolean",
+            human                     => "boolean",
+            include_unloaded_segments => "boolean",
+            master_timeout            => "time",
+            s                         => "list",
+            time                      => "enum",
+            v                         => "boolean",
+        },
+    },
+
+    'cat.pending_tasks' => {
+        doc   => "cat-pending-tasks",
+        parts => {},
+        paths => [ [ {}, "_cat", "pending_tasks" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.plugins' => {
+        doc   => "cat-plugins",
+        parts => {},
+        paths => [ [ {}, "_cat", "plugins" ] ],
+        qs    => {
+            error_trace       => "boolean",
+            filter_path       => "list",
+            format            => "string",
+            h                 => "list",
+            help              => "boolean",
+            human             => "boolean",
+            include_bootstrap => "boolean",
+            local             => "boolean",
+            master_timeout    => "time",
+            s                 => "list",
+            v                 => "boolean",
+        },
+    },
+
+    'cat.recovery' => {
+        doc   => "cat-recovery",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "recovery", "{index}" ],
+            [ {}, "_cat", "recovery" ],
+        ],
+        qs => {
+            active_only => "boolean",
+            bytes       => "enum",
+            detailed    => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            time        => "enum",
+            v           => "boolean",
+        },
+    },
+
+    'cat.repositories' => {
+        doc   => "cat-repositories",
+        parts => {},
+        paths => [ [ {}, "_cat", "repositories" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.segments' => {
+        doc   => "cat-segments",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "segments", "{index}" ],
+            [ {}, "_cat", "segments" ],
+        ],
+        qs => {
+            bytes       => "enum",
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            h           => "list",
+            help        => "boolean",
+            human       => "boolean",
+            s           => "list",
+            v           => "boolean",
+        },
+    },
+
+    'cat.shards' => {
+        doc   => "cat-shards",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cat", "shards", "{index}" ],
+            [ {}, "_cat", "shards" ],
+        ],
+        qs => {
+            bytes          => "enum",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.snapshots' => {
+        doc   => "cat-snapshots",
+        parts => { repository => { multi => 1 } },
+        paths => [
+            [ { repository => 2 }, "_cat", "snapshots", "{repository}" ],
+            [ {}, "_cat", "snapshots" ],
+        ],
+        qs => {
+            error_trace        => "boolean",
+            filter_path        => "list",
+            format             => "string",
+            h                  => "list",
+            help               => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            s                  => "list",
+            time               => "enum",
+            v                  => "boolean",
+        },
+    },
+
+    'cat.tasks' => {
+        doc   => "tasks",
+        parts => {},
+        paths => [ [ {}, "_cat", "tasks" ] ],
+        qs    => {
+            actions        => "list",
+            detailed       => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            nodes          => "list",
+            parent_task_id => "string",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.templates' => {
+        doc   => "cat-templates",
+        parts => { name => {} },
+        paths => [
+            [ { name => 2 }, "_cat", "templates", "{name}" ],
+            [ {}, "_cat", "templates" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            v              => "boolean",
+        },
+    },
+
+    'cat.thread_pool' => {
+        doc   => "cat-thread-pool",
+        parts => { thread_pool_patterns => { multi => 1 } },
+        paths => [
+            [   { thread_pool_patterns => 2 }, "_cat",
+                "thread_pool",                 "{thread_pool_patterns}",
+            ],
+            [ {}, "_cat", "thread_pool" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+            s              => "list",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'cat.transforms' => {
+        doc   => "cat-transforms",
+        parts => { transform_id => {} },
+        paths => [
+            [ { transform_id => 2 }, "_cat", "transforms", "{transform_id}" ],
+            [ {}, "_cat", "transforms" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            format         => "string",
+            from           => "int",
+            h              => "list",
+            help           => "boolean",
+            human          => "boolean",
+            s              => "list",
+            size           => "int",
+            time           => "enum",
+            v              => "boolean",
+        },
+    },
+
+    'ccr.delete_auto_follow_pattern' => {
+        doc    => "ccr-delete-auto-follow-pattern",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_ccr", "auto_follow", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.follow' => {
+        body   => { required => 1 },
+        doc    => "ccr-put-follow",
+        method => "PUT",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ccr", "follow" ] ],
+        qs     => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'ccr.follow_info' => {
+        doc   => "ccr-get-follow-info",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}", "_ccr", "info" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.follow_stats' => {
+        doc   => "ccr-get-follow-stats",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}", "_ccr", "stats" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.forget_follower' => {
+        body   => { required => 1 },
+        doc    => "ccr-post-forget-follower",
+        method => "POST",
+        parts  => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_ccr", "forget_follower" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.get_auto_follow_pattern' => {
+        doc   => "ccr-get-auto-follow-pattern",
+        parts => { name => {} },
+        paths => [
+            [ { name => 2 }, "_ccr", "auto_follow", "{name}" ],
+            [ {}, "_ccr", "auto_follow" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.pause_auto_follow_pattern' => {
+        doc    => "ccr-pause-auto-follow-pattern",
+        method => "POST",
+        parts  => { name => {} },
+        paths  =>
+            [ [ { name => 2 }, "_ccr", "auto_follow", "{name}", "pause" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.pause_follow' => {
+        doc    => "ccr-post-pause-follow",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ccr", "pause_follow" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.put_auto_follow_pattern' => {
+        body   => { required => 1 },
+        doc    => "ccr-put-auto-follow-pattern",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_ccr", "auto_follow", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.resume_auto_follow_pattern' => {
+        doc    => "ccr-resume-auto-follow-pattern",
+        method => "POST",
+        parts  => { name => {} },
+        paths  =>
+            [ [ { name => 2 }, "_ccr", "auto_follow", "{name}", "resume" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.resume_follow' => {
+        body   => {},
+        doc    => "ccr-post-resume-follow",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ccr", "resume_follow" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.stats' => {
+        doc   => "ccr-get-stats",
+        parts => {},
+        paths => [ [ {}, "_ccr", "stats" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ccr.unfollow' => {
+        doc    => "ccr-post-unfollow",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ccr", "unfollow" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'cluster.allocation_explain' => {
+        body  => {},
+        doc   => "cluster-allocation-explain",
+        parts => {},
+        paths => [ [ {}, "_cluster", "allocation", "explain" ] ],
+        qs    => {
+            error_trace           => "boolean",
+            filter_path           => "list",
+            human                 => "boolean",
+            include_disk_info     => "boolean",
+            include_yes_decisions => "boolean",
+        },
+    },
+
+    'cluster.delete_component_template' => {
+        doc    => "indices-component-template",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_component_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'cluster.delete_voting_config_exclusions' => {
+        doc    => "voting-config-exclusions",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_cluster", "voting_config_exclusions" ] ],
+        qs     => {
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            master_timeout   => "time",
+            wait_for_removal => "boolean",
+        },
+    },
+
+    'cluster.exists_component_template' => {
+        doc    => "indices-component-template",
+        method => "HEAD",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_component_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'cluster.get_component_template' => {
+        doc   => "indices-component-template",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 1 }, "_component_template", "{name}" ],
+            [ {}, "_component_template" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'cluster.get_settings' => {
+        doc   => "cluster-get-settings",
+        parts => {},
+        paths => [ [ {}, "_cluster", "settings" ] ],
+        qs    => {
+            error_trace      => "boolean",
+            filter_path      => "list",
+            flat_settings    => "boolean",
+            human            => "boolean",
+            include_defaults => "boolean",
+            master_timeout   => "time",
+            timeout          => "time",
+        },
+    },
+
+    'cluster.health' => {
+        doc   => "cluster-health",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 2 }, "_cluster", "health", "{index}" ],
+            [ {}, "_cluster", "health" ],
+        ],
+        qs => {
+            error_trace                     => "boolean",
+            expand_wildcards                => "enum",
+            filter_path                     => "list",
+            human                           => "boolean",
+            level                           => "enum",
+            local                           => "boolean",
+            master_timeout                  => "time",
+            timeout                         => "time",
+            wait_for_active_shards          => "string",
+            wait_for_events                 => "enum",
+            wait_for_no_initializing_shards => "boolean",
+            wait_for_no_relocating_shards   => "boolean",
+            wait_for_nodes                  => "string",
+            wait_for_status                 => "enum",
+        },
+    },
+
+    'cluster.pending_tasks' => {
+        doc   => "cluster-pending",
+        parts => {},
+        paths => [ [ {}, "_cluster", "pending_tasks" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'cluster.post_voting_config_exclusions' => {
+        doc    => "voting-config-exclusions",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_cluster", "voting_config_exclusions" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            node_ids       => "string",
+            node_names     => "string",
+            timeout        => "time",
+        },
+    },
+
+    'cluster.put_component_template' => {
+        body   => { required => 1 },
+        doc    => "indices-component-template",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_component_template", "{name}" ] ],
+        qs     => {
+            create         => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'cluster.put_settings' => {
+        body   => { required => 1 },
+        doc    => "cluster-update-settings",
+        method => "PUT",
+        parts  => {},
+        paths  => [ [ {}, "_cluster", "settings" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            flat_settings  => "boolean",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'cluster.remote_info' => {
+        doc   => "cluster-remote-info",
+        parts => {},
+        paths => [ [ {}, "_remote", "info" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'cluster.reroute' => {
+        body   => {},
+        doc    => "cluster-reroute",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_cluster", "reroute" ] ],
+        qs     => {
+            dry_run        => "boolean",
+            error_trace    => "boolean",
+            explain        => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            metric         => "list",
+            retry_failed   => "boolean",
+            timeout        => "time",
+        },
+    },
+
+    'cluster.state' => {
+        doc   => "cluster-state",
+        parts => { index => { multi => 1 }, metric => { multi => 1 } },
+        paths => [
+            [   { index => 3, metric => 2 }, "_cluster",
+                "state",                     "{metric}",
+                "{index}",
+            ],
+            [ { metric => 2 }, "_cluster", "state", "{metric}" ],
+            [ {}, "_cluster", "state" ],
+        ],
+        qs => {
+            allow_no_indices          => "boolean",
+            error_trace               => "boolean",
+            expand_wildcards          => "enum",
+            filter_path               => "list",
+            flat_settings             => "boolean",
+            human                     => "boolean",
+            ignore_unavailable        => "boolean",
+            local                     => "boolean",
+            master_timeout            => "time",
+            wait_for_metadata_version => "number",
+            wait_for_timeout          => "time",
+        },
+    },
+
+    'cluster.stats' => {
+        doc   => "cluster-stats",
+        parts => { node_id => { multi => 1 } },
+        paths => [
+            [ { node_id => 3 }, "_cluster", "stats", "nodes", "{node_id}" ],
+            [ {}, "_cluster", "stats" ],
+        ],
+        qs => {
+            error_trace   => "boolean",
+            filter_path   => "list",
+            flat_settings => "boolean",
+            human         => "boolean",
+            timeout       => "time",
+        },
+    },
+
+    'dangling_indices.delete_dangling_index' => {
+        doc    => "modules-gateway-dangling-indices",
+        method => "DELETE",
+        parts  => { index_uuid => {} },
+        paths  => [ [ { index_uuid => 1 }, "_dangling", "{index_uuid}" ] ],
+        qs     => {
+            accept_data_loss => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            master_timeout   => "time",
+            timeout          => "time",
+        },
+    },
+
+    'dangling_indices.import_dangling_index' => {
+        doc    => "modules-gateway-dangling-indices",
+        method => "POST",
+        parts  => { index_uuid => {} },
+        paths  => [ [ { index_uuid => 1 }, "_dangling", "{index_uuid}" ] ],
+        qs     => {
+            accept_data_loss => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            master_timeout   => "time",
+            timeout          => "time",
+        },
+    },
+
+    'dangling_indices.list_dangling_indices' => {
+        doc   => "modules-gateway-dangling-indices",
+        parts => {},
+        paths => [ [ {}, "_dangling" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'enrich.delete_policy' => {
+        doc    => "delete-enrich-policy-api",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_enrich", "policy", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'enrich.execute_policy' => {
+        doc    => "execute-enrich-policy-api",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  =>
+            [ [ { name => 2 }, "_enrich", "policy", "{name}", "_execute" ] ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'enrich.get_policy' => {
+        doc   => "get-enrich-policy-api",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 2 }, "_enrich", "policy", "{name}" ],
+            [ {}, "_enrich", "policy" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'enrich.put_policy' => {
+        body   => { required => 1 },
+        doc    => "put-enrich-policy-api",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_enrich", "policy", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'enrich.stats' => {
+        doc   => "enrich-stats-api",
+        parts => {},
+        paths => [ [ {}, "_enrich", "_stats" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'eql.delete' => {
+        doc    => "eql-search-api",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_eql", "search", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'eql.get' => {
+        doc   => "eql-search-api",
+        parts => { id => {} },
+        paths => [ [ { id => 2 }, "_eql", "search", "{id}" ] ],
+        qs    => {
+            error_trace                 => "boolean",
+            filter_path                 => "list",
+            human                       => "boolean",
+            keep_alive                  => "time",
+            wait_for_completion_timeout => "time",
+        },
+    },
+
+    'eql.get_status' => {
+        doc   => "eql-search-api",
+        parts => { id => {} },
+        paths => [ [ { id => 3 }, "_eql", "search", "status", "{id}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'eql.search' => {
+        body  => { required => 1 },
+        doc   => "eql-search-api",
+        parts => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_eql", "search" ] ],
+        qs    => {
+            error_trace                 => "boolean",
+            filter_path                 => "list",
+            human                       => "boolean",
+            keep_alive                  => "time",
+            keep_on_completion          => "boolean",
+            wait_for_completion_timeout => "time",
+        },
+    },
+
+    'features.get_features' => {
+        doc   => "get-features-api",
+        parts => {},
+        paths => [ [ {}, "_features" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'features.reset_features' => {
+        doc    => "modules-snapshots",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_features", "_reset" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'fleet.global_checkpoints' => {
+        doc   => "get-global-checkpoints",
+        parts => { index => {} },
+        paths =>
+            [ [ { index => 0 }, "{index}", "_fleet", "global_checkpoints" ] ],
+        qs => {
+            checkpoints      => "list",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            timeout          => "time",
+            wait_for_advance => "boolean",
+            wait_for_index   => "boolean",
+        },
+    },
+
+    'fleet.msearch' => {
+        body  => { required => 1 },
+        parts => { index    => {} },
+        paths => [
+            [ { index => 0 }, "{index}", "_fleet", "_fleet_msearch" ],
+            [ {}, "_fleet", "_fleet_msearch" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+        serialize => "bulk",
+    },
+
+    'fleet.search' => {
+        body  => {},
+        parts => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_fleet", "_fleet_search" ] ],
+        qs    => {
+            allow_partial_search_results => "boolean",
+            error_trace                  => "boolean",
+            filter_path                  => "list",
+            human                        => "boolean",
+            wait_for_checkpoints         => "list",
+            wait_for_checkpoints_timeout => "time",
+        },
+    },
+
+    'graph.explore' => {
+        body  => {},
+        doc   => "graph-explore-api",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}", "_graph", "explore" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            routing     => "string",
+            timeout     => "time",
+        },
+    },
+
+    'ilm.delete_lifecycle' => {
+        doc    => "ilm-delete-lifecycle",
+        method => "DELETE",
+        parts  => { policy => {} },
+        paths  => [ [ { policy => 2 }, "_ilm", "policy", "{policy}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.explain_lifecycle' => {
+        doc   => "ilm-explain-lifecycle",
+        parts => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_ilm", "explain" ] ],
+        qs    => {
+            error_trace  => "boolean",
+            filter_path  => "list",
+            human        => "boolean",
+            only_errors  => "boolean",
+            only_managed => "boolean",
+        },
+    },
+
+    'ilm.get_lifecycle' => {
+        doc   => "ilm-get-lifecycle",
+        parts => { policy => {} },
+        paths => [
+            [ { policy => 2 }, "_ilm", "policy", "{policy}" ],
+            [ {}, "_ilm", "policy" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.get_status' => {
+        doc   => "ilm-get-status",
+        parts => {},
+        paths => [ [ {}, "_ilm", "status" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.migrate_to_data_tiers' => {
+        body   => {},
+        doc    => "ilm-migrate-to-data-tiers",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ilm", "migrate_to_data_tiers" ] ],
+        qs     => {
+            dry_run     => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'ilm.move_to_step' => {
+        body   => {},
+        doc    => "ilm-move-to-step",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 2 }, "_ilm", "move", "{index}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.put_lifecycle' => {
+        body   => {},
+        doc    => "ilm-put-lifecycle",
+        method => "PUT",
+        parts  => { policy => {} },
+        paths  => [ [ { policy => 2 }, "_ilm", "policy", "{policy}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.remove_policy' => {
+        doc    => "ilm-remove-policy",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ilm", "remove" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.retry' => {
+        doc    => "ilm-retry-policy",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_ilm", "retry" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.start' => {
+        doc    => "ilm-start",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ilm", "start" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ilm.stop' => {
+        doc    => "ilm-stop",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ilm", "stop" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.add_block' => {
+        doc    => "index-modules-blocks",
+        method => "PUT",
+        parts  => { block => {}, index => { multi => 1 } },
+        paths  => [
+            [ { block => 2, index => 0 }, "{index}", "_block", "{block}" ]
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            timeout            => "time",
+        },
+    },
+
+    'indices.analyze' => {
+        body  => {},
+        doc   => "indices-analyze",
+        parts => { index => {} },
+        paths =>
+            [ [ { index => 0 }, "{index}", "_analyze" ], [ {}, "_analyze" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.clear_cache' => {
+        doc    => "indices-clearcache",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [ { index => 0 }, "{index}", "_cache", "clear" ],
+            [ {}, "_cache", "clear" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            fielddata          => "boolean",
+            fields             => "list",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            query              => "boolean",
+            request            => "boolean",
+        },
+    },
+
+    'indices.clone' => {
+        body   => {},
+        doc    => "indices-clone-index",
+        method => "PUT",
+        parts  => { index => {}, target => {} },
+        paths  => [
+            [ { index => 0, target => 2 }, "{index}", "_clone", "{target}" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.close' => {
+        doc    => "indices-open-close",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}", "_close" ] ],
+        qs     => {
+            allow_no_indices       => "boolean",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.create' => {
+        body   => {},
+        doc    => "indices-create-index",
+        method => "PUT",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}" ] ],
+        qs     => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.create_data_stream' => {
+        doc    => "data-streams",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_data_stream", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.data_streams_stats' => {
+        doc   => "data-streams",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 1 }, "_data_stream", "{name}", "_stats" ],
+            [ {}, "_data_stream", "_stats" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.delete' => {
+        doc    => "indices-delete-index",
+        method => "DELETE",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}" ] ],
+        qs     => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            timeout            => "time",
+        },
+    },
+
+    'indices.delete_alias' => {
+        doc    => "indices-aliases",
+        method => "DELETE",
+        parts  => { index => { multi => 1 }, name => { multi => 1 } },
+        paths  =>
+            [ [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ] ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.delete_data_stream' => {
+        doc    => "data-streams",
+        method => "DELETE",
+        parts  => { name => { multi => 1 } },
+        paths  => [ [ { name => 1 }, "_data_stream", "{name}" ] ],
+        qs     => {
+            error_trace      => "boolean",
+            expand_wildcards => "enum",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'indices.delete_index_template' => {
+        doc    => "indices-templates",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_index_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.delete_template' => {
+        doc    => "indices-templates",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.disk_usage' => {
+        doc    => "indices-disk-usage",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_disk_usage" ] ],
+        qs     => {
+            allow_no_indices    => "boolean",
+            error_trace         => "boolean",
+            expand_wildcards    => "enum",
+            filter_path         => "list",
+            flush               => "boolean",
+            human               => "boolean",
+            ignore_unavailable  => "boolean",
+            run_expensive_tasks => "boolean",
+        },
+    },
+
+    'indices.downsample' => {
+        body   => { required => 1 },
+        doc    => "xpack-rollup",
+        method => "POST",
+        parts  =>
+            { index => { required => 1 }, target_index => { required => 1 } },
+        paths => [
+            [   { index => 0, target_index => 2 }, "{index}",
+                "_downsample",                     "{target_index}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.exists' => {
+        doc    => "indices-exists",
+        method => "HEAD",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}" ] ],
+        qs     => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            flat_settings      => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_defaults   => "boolean",
+            local              => "boolean",
+        },
+    },
+
+    'indices.exists_alias' => {
+        doc    => "indices-aliases",
+        method => "HEAD",
+        parts  => { index => { multi => 1 }, name => { multi => 1 } },
+        paths  => [
+            [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ],
+            [ { name  => 1 }, "_alias", "{name}" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            local              => "boolean",
+        },
+    },
+
+    'indices.exists_index_template' => {
+        doc    => "indices-templates",
+        method => "HEAD",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_index_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            flat_settings  => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.exists_template' => {
+        doc    => "indices-templates",
+        method => "HEAD",
+        parts  => { name => { multi => 1 } },
+        paths  => [ [ { name => 1 }, "_template", "{name}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            flat_settings  => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.field_usage_stats' => {
+        doc   => "field-usage-stats",
+        parts => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_field_usage_stats" ] ],
+        qs    => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            fields             => "list",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'indices.flush' => {
+        doc    => "indices-flush",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  =>
+            [ [ { index => 0 }, "{index}", "_flush" ], [ {}, "_flush" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            force              => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            wait_if_ongoing    => "boolean",
+        },
+    },
+
+    'indices.forcemerge' => {
+        doc    => "indices-forcemerge",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [ { index => 0 }, "{index}", "_forcemerge" ],
+            [ {}, "_forcemerge" ],
+        ],
+        qs => {
+            allow_no_indices     => "boolean",
+            error_trace          => "boolean",
+            expand_wildcards     => "enum",
+            filter_path          => "list",
+            flush                => "boolean",
+            human                => "boolean",
+            ignore_unavailable   => "boolean",
+            max_num_segments     => "number",
+            only_expunge_deletes => "boolean",
+            wait_for_completion  => "boolean",
+        },
+    },
+
+    'indices.get' => {
+        doc   => "indices-get-index",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}" ] ],
+        qs    => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            features           => "enum",
+            filter_path        => "list",
+            flat_settings      => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_defaults   => "boolean",
+            local              => "boolean",
+            master_timeout     => "time",
+        },
+    },
+
+    'indices.get_alias' => {
+        doc   => "indices-aliases",
+        parts => { index => { multi => 1 }, name => { multi => 1 } },
+        paths => [
+            [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ],
+            [ { index => 0 }, "{index}", "_alias" ],
+            [ { name  => 1 }, "_alias",  "{name}" ],
+            [ {}, "_alias" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            local              => "boolean",
+        },
+    },
+
+    'indices.get_data_stream' => {
+        doc   => "data-streams",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 1 }, "_data_stream", "{name}" ],
+            [ {}, "_data_stream" ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            expand_wildcards => "enum",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'indices.get_field_mapping' => {
+        doc   => "indices-get-field-mapping",
+        parts => { fields => { multi => 1 }, index => { multi => 1 } },
+        paths => [
+            [   { fields => 3, index => 0 }, "{index}",
+                "_mapping",                  "field",
+                "{fields}",
+            ],
+            [ { fields => 2 }, "_mapping", "field", "{fields}" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_defaults   => "boolean",
+            local              => "boolean",
+        },
+    },
+
+    'indices.get_index_template' => {
+        doc   => "indices-templates",
+        parts => { name => {} },
+        paths => [
+            [ { name => 1 }, "_index_template", "{name}" ],
+            [ {}, "_index_template" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            flat_settings  => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.get_mapping' => {
+        doc   => "indices-get-mapping",
+        parts => { index => { multi => 1 } },
+        paths =>
+            [ [ { index => 0 }, "{index}", "_mapping" ], [ {}, "_mapping" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            local              => "boolean",
+            master_timeout     => "time",
+        },
+    },
+
+    'indices.get_settings' => {
+        doc   => "indices-get-settings",
+        parts => { index => { multi => 1 }, name => { multi => 1 } },
+        paths => [
+            [ { index => 0, name => 2 }, "{index}", "_settings", "{name}" ],
+            [ { index => 0 }, "{index}",   "_settings" ],
+            [ { name  => 1 }, "_settings", "{name}" ],
+            [ {}, "_settings" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            flat_settings      => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_defaults   => "boolean",
+            local              => "boolean",
+            master_timeout     => "time",
+        },
+    },
+
+    'indices.get_template' => {
+        doc   => "indices-templates",
+        parts => { name => { multi => 1 } },
+        paths =>
+            [ [ { name => 1 }, "_template", "{name}" ], [ {}, "_template" ] ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            flat_settings  => "boolean",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.migrate_to_data_stream' => {
+        doc    => "data-streams",
+        method => "POST",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_data_stream", "_migrate", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.modify_data_stream' => {
+        body   => { required => 1 },
+        doc    => "data-streams",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_data_stream", "_modify" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.open' => {
+        doc    => "indices-open-close",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}", "_open" ] ],
+        qs     => {
+            allow_no_indices       => "boolean",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.promote_data_stream' => {
+        doc    => "data-streams",
+        method => "POST",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_data_stream", "_promote", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'indices.put_alias' => {
+        body   => {},
+        doc    => "indices-aliases",
+        method => "PUT",
+        parts  => { index => { multi => 1 }, name => {} },
+        paths  =>
+            [ [ { index => 0, name => 2 }, "{index}", "_alias", "{name}" ] ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.put_index_template' => {
+        body   => { required => 1 },
+        doc    => "indices-templates",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_index_template", "{name}" ] ],
+        qs     => {
+            cause          => "string",
+            create         => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.put_mapping' => {
+        body   => { required => 1 },
+        doc    => "indices-put-mapping",
+        method => "PUT",
+        parts  => { index => { multi => 1 } },
+        paths  => [ [ { index => 0 }, "{index}", "_mapping" ] ],
+        qs     => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            timeout            => "time",
+            write_index_only   => "boolean",
+        },
+    },
+
+    'indices.put_settings' => {
+        body   => { required => 1 },
+        doc    => "indices-update-settings",
+        method => "PUT",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [ { index => 0 }, "{index}", "_settings" ],
+            [ {}, "_settings" ]
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            flat_settings      => "boolean",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+            preserve_existing  => "boolean",
+            timeout            => "time",
+        },
+    },
+
+    'indices.put_template' => {
+        body   => { required => 1 },
+        doc    => "indices-templates",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 1 }, "_template", "{name}" ] ],
+        qs     => {
+            create         => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            order          => "number",
+        },
+    },
+
+    'indices.recovery' => {
+        doc   => "indices-recovery",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_recovery" ],
+            [ {}, "_recovery" ]
+        ],
+        qs => {
+            active_only => "boolean",
+            detailed    => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'indices.refresh' => {
+        doc    => "indices-refresh",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  =>
+            [ [ { index => 0 }, "{index}", "_refresh" ], [ {}, "_refresh" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'indices.reload_search_analyzers' => {
+        doc   => "indices-reload-analyzers",
+        parts => { index => { multi => 1 } },
+        paths =>
+            [ [ { index => 0 }, "{index}", "_reload_search_analyzers" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'indices.resolve_index' => {
+        doc   => "indices-resolve-index-api",
+        parts => { name => { multi => 1 } },
+        paths => [ [ { name => 2 }, "_resolve", "index", "{name}" ] ],
+        qs    => {
+            error_trace      => "boolean",
+            expand_wildcards => "enum",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'indices.rollover' => {
+        body   => {},
+        doc    => "indices-rollover-index",
+        method => "POST",
+        parts  => { alias => {}, new_index => {} },
+        paths  => [
+            [   { alias => 0, new_index => 2 }, "{alias}",
+                "_rollover",                    "{new_index}",
+            ],
+            [ { alias => 0 }, "{alias}", "_rollover" ],
+        ],
+        qs => {
+            dry_run                => "boolean",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.segments' => {
+        doc   => "indices-segments",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_segments" ],
+            [ {}, "_segments" ]
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            verbose            => "boolean",
+        },
+    },
+
+    'indices.shard_stores' => {
+        doc   => "indices-shards-stores",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_shard_stores" ],
+            [ {}, "_shard_stores" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            status             => "list",
+        },
+    },
+
+    'indices.shrink' => {
+        body   => {},
+        doc    => "indices-shrink-index",
+        method => "PUT",
+        parts  => { index => {}, target => {} },
+        paths  => [
+            [ { index => 0, target => 2 }, "{index}", "_shrink", "{target}" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.simulate_index_template' => {
+        body   => {},
+        doc    => "indices-templates",
+        method => "POST",
+        parts  => { name => {} },
+        paths  => [
+            [ { name => 2 }, "_index_template", "_simulate_index", "{name}" ],
+        ],
+        qs => {
+            cause          => "string",
+            create         => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.simulate_template' => {
+        body   => {},
+        doc    => "indices-templates",
+        method => "POST",
+        parts  => { name => {} },
+        paths  => [
+            [ { name => 2 }, "_index_template", "_simulate", "{name}" ],
+            [ {}, "_index_template", "_simulate" ],
+        ],
+        qs => {
+            cause          => "string",
+            create         => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'indices.split' => {
+        body   => {},
+        doc    => "indices-split-index",
+        method => "PUT",
+        parts  => { index => {}, target => {} },
+        paths  => [
+            [ { index => 0, target => 2 }, "{index}", "_split", "{target}" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.stats' => {
+        doc   => "indices-stats",
+        parts => { index => { multi => 1 }, metric => { multi => 1 } },
+        paths => [
+            [ { index  => 0, metric => 2 }, "{index}", "_stats", "{metric}" ],
+            [ { index  => 0 }, "{index}", "_stats" ],
+            [ { metric => 1 }, "_stats",  "{metric}" ],
+            [ {}, "_stats" ],
+        ],
+        qs => {
+            completion_fields          => "list",
+            error_trace                => "boolean",
+            expand_wildcards           => "enum",
+            fielddata_fields           => "list",
+            fields                     => "list",
+            filter_path                => "list",
+            forbid_closed_indices      => "boolean",
+            groups                     => "list",
+            human                      => "boolean",
+            include_segment_file_sizes => "boolean",
+            include_unloaded_segments  => "boolean",
+            level                      => "enum",
+        },
+    },
+
+    'indices.unfreeze' => {
+        doc    => "unfreeze-index-api",
+        method => "POST",
+        parts  => { index => {} },
+        paths  => [ [ { index => 0 }, "{index}", "_unfreeze" ] ],
+        qs     => {
+            allow_no_indices       => "boolean",
+            error_trace            => "boolean",
+            expand_wildcards       => "enum",
+            filter_path            => "list",
+            human                  => "boolean",
+            ignore_unavailable     => "boolean",
+            master_timeout         => "time",
+            timeout                => "time",
+            wait_for_active_shards => "string",
+        },
+    },
+
+    'indices.update_aliases' => {
+        body   => { required => 1 },
+        doc    => "indices-aliases",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_aliases" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'indices.validate_query' => {
+        body  => {},
+        doc   => "search-validate",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_validate", "query" ],
+            [ {}, "_validate", "query" ],
+        ],
+        qs => {
+            all_shards         => "boolean",
+            allow_no_indices   => "boolean",
+            analyze_wildcard   => "boolean",
+            analyzer           => "string",
+            default_operator   => "enum",
+            df                 => "string",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            explain            => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            lenient            => "boolean",
+            q                  => "string",
+            rewrite            => "boolean",
+        },
+    },
+
+    'ingest.delete_pipeline' => {
+        doc    => "delete-pipeline-api",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_ingest", "pipeline", "{id}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'ingest.geo_ip_stats' => {
+        doc   => "geoip-stats-api",
+        parts => {},
+        paths => [ [ {}, "_ingest", "geoip", "stats" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ingest.get_pipeline' => {
+        doc   => "get-pipeline-api",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_ingest", "pipeline", "{id}" ],
+            [ {}, "_ingest", "pipeline" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            summary        => "boolean",
+        },
+    },
+
+    'ingest.processor_grok' => {
+        doc   => "",
+        parts => {},
+        paths => [ [ {}, "_ingest", "processor", "grok" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ingest.put_pipeline' => {
+        body   => { required => 1 },
+        doc    => "put-pipeline-api",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_ingest", "pipeline", "{id}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            if_version     => "int",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'ingest.simulate' => {
+        body  => { required => 1 },
+        doc   => "simulate-pipeline-api",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_ingest", "pipeline", "{id}", "_simulate" ],
+            [ {}, "_ingest", "pipeline", "_simulate" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            verbose     => "boolean",
+        },
+    },
+
+    'license.delete' => {
+        doc    => "delete-license",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_license" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'license.get' => {
+        doc   => "get-license",
+        parts => {},
+        paths => [ [ {}, "_license" ] ],
+        qs    => {
+            accept_enterprise => "boolean",
+            error_trace       => "boolean",
+            filter_path       => "list",
+            human             => "boolean",
+            local             => "boolean",
+        },
+    },
+
+    'license.get_basic_status' => {
+        doc   => "get-basic-status",
+        parts => {},
+        paths => [ [ {}, "_license", "basic_status" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'license.get_trial_status' => {
+        doc   => "get-trial-status",
+        parts => {},
+        paths => [ [ {}, "_license", "trial_status" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'license.post' => {
+        body   => {},
+        doc    => "update-license",
+        method => "PUT",
+        parts  => {},
+        paths  => [ [ {}, "_license" ] ],
+        qs     => {
+            acknowledge => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'license.post_start_basic' => {
+        doc    => "start-basic",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_license", "start_basic" ] ],
+        qs     => {
+            acknowledge => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'license.post_start_trial' => {
+        doc    => "start-trial",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_license", "start_trial" ] ],
+        qs     => {
+            acknowledge => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            type        => "string",
+        },
+    },
+
+    'logstash.delete_pipeline' => {
+        doc    => "logstash-api-delete-pipeline",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_logstash", "pipeline", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'logstash.get_pipeline' => {
+        doc   => "logstash-api-get-pipeline",
+        parts => { id => {} },
+        paths => [ [ { id => 2 }, "_logstash", "pipeline", "{id}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'logstash.put_pipeline' => {
+        body   => { required => 1 },
+        doc    => "logstash-api-put-pipeline",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_logstash", "pipeline", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'migration.deprecations' => {
+        doc   => "migration-api-deprecation",
+        parts => { index => {} },
+        paths => [
+            [ { index => 0 }, "{index}", "_migration", "deprecations" ],
+            [ {}, "_migration", "deprecations" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'migration.get_feature_upgrade_status' => {
+        doc   => "migration-api-feature-upgrade",
+        parts => {},
+        paths => [ [ {}, "_migration", "system_features" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'migration.post_feature_upgrade' => {
+        doc    => "migration-api-feature-upgrade",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_migration", "system_features" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.clear_trained_model_deployment_cache' => {
+        doc    => "clear-trained-model-deployment-cache",
+        method => "POST",
+        parts  => { model_id => { required => 1 } },
+        paths  => [
+            [   { model_id => 2 }, "_ml",
+                "trained_models",  "{model_id}",
+                "deployment",      "cache",
+                "_clear",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.close_job' => {
+        body   => {},
+        doc    => "ml-close-job",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_close",
+            ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            force          => "boolean",
+            human          => "boolean",
+            timeout        => "time",
+        },
+    },
+
+    'ml.delete_calendar' => {
+        doc    => "ml-delete-calendar",
+        method => "DELETE",
+        parts  => { calendar_id => {} },
+        paths  =>
+            [ [ { calendar_id => 2 }, "_ml", "calendars", "{calendar_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_calendar_event' => {
+        doc    => "ml-delete-calendar-event",
+        method => "DELETE",
+        parts  => { calendar_id => {}, event_id => {} },
+        paths  => [
+            [   { calendar_id => 2, event_id => 4 }, "_ml",
+                "calendars",                         "{calendar_id}",
+                "events",                            "{event_id}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_calendar_job' => {
+        doc    => "ml-delete-calendar-job",
+        method => "DELETE",
+        parts  => { calendar_id => {}, job_id => {} },
+        paths  => [
+            [   { calendar_id => 2, job_id => 4 },
+                "_ml", "calendars", "{calendar_id}", "jobs", "{job_id}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_data_frame_analytics' => {
+        doc    => "delete-dfanalytics",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  =>
+            [ [ { id => 3 }, "_ml", "data_frame", "analytics", "{id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            force       => "boolean",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'ml.delete_datafeed' => {
+        doc    => "ml-delete-datafeed",
+        method => "DELETE",
+        parts  => { datafeed_id => {} },
+        paths  =>
+            [ [ { datafeed_id => 2 }, "_ml", "datafeeds", "{datafeed_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            force       => "boolean",
+            human       => "boolean",
+        },
+    },
+
+    'ml.delete_expired_data' => {
+        body   => {},
+        doc    => "ml-delete-expired-data",
+        method => "DELETE",
+        parts  => { job_id => {} },
+        paths  => [
+            [ { job_id => 2 }, "_ml", "_delete_expired_data", "{job_id}" ],
+            [ {}, "_ml", "_delete_expired_data" ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            requests_per_second => "number",
+            timeout             => "time",
+        },
+    },
+
+    'ml.delete_filter' => {
+        doc    => "ml-delete-filter",
+        method => "DELETE",
+        parts  => { filter_id => {} },
+        paths  => [ [ { filter_id => 2 }, "_ml", "filters", "{filter_id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_forecast' => {
+        doc    => "ml-delete-forecast",
+        method => "DELETE",
+        parts  => { forecast_id => {}, job_id => {} },
+        paths  => [
+            [   { forecast_id => 4, job_id => 2 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "_forecast",                       "{forecast_id}",
+            ],
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_forecast",
+            ],
+        ],
+        qs => {
+            allow_no_forecasts => "boolean",
+            error_trace        => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+            timeout            => "time",
+        },
+    },
+
+    'ml.delete_job' => {
+        doc    => "ml-delete-job",
+        method => "DELETE",
+        parts  => { job_id => {} },
+        paths  =>
+            [ [ { job_id => 2 }, "_ml", "anomaly_detectors", "{job_id}" ] ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            force               => "boolean",
+            human               => "boolean",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'ml.delete_model_snapshot' => {
+        doc    => "ml-delete-snapshot",
+        method => "DELETE",
+        parts  => { job_id => {}, snapshot_id => {} },
+        paths  => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.delete_trained_model' => {
+        doc    => "delete-trained-models",
+        method => "DELETE",
+        parts  => { model_id => {} },
+        paths  =>
+            [ [ { model_id => 2 }, "_ml", "trained_models", "{model_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            force       => "boolean",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'ml.delete_trained_model_alias' => {
+        doc    => "delete-trained-models-aliases",
+        method => "DELETE",
+        parts  => { model_alias => {}, model_id => {} },
+        paths  => [
+            [   { model_alias => 4, model_id => 2 }, "_ml",
+                "trained_models",                    "{model_id}",
+                "model_aliases",                     "{model_alias}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.estimate_model_memory' => {
+        body   => { required => 1 },
+        doc    => "ml-apis",
+        method => "POST",
+        parts  => {},
+        paths  =>
+            [ [ {}, "_ml", "anomaly_detectors", "_estimate_model_memory" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.evaluate_data_frame' => {
+        body   => { required => 1 },
+        doc    => "evaluate-dfanalytics",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ml", "data_frame", "_evaluate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.explain_data_frame_analytics' => {
+        body  => {},
+        doc   => "explain-dfanalytics",
+        parts => { id => {} },
+        paths => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics",
+                "{id}",      "_explain"
+            ],
+            [ {}, "_ml", "data_frame", "analytics", "_explain" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.flush_job' => {
+        body   => {},
+        doc    => "ml-flush-job",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_flush",
+            ],
+        ],
+        qs => {
+            advance_time => "string",
+            calc_interim => "boolean",
+            end          => "string",
+            error_trace  => "boolean",
+            filter_path  => "list",
+            human        => "boolean",
+            skip_time    => "string",
+            start        => "string",
+        },
+    },
+
+    'ml.forecast' => {
+        body   => {},
+        doc    => "ml-forecast",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_forecast",
+            ],
+        ],
+        qs => {
+            duration         => "time",
+            error_trace      => "boolean",
+            expires_in       => "time",
+            filter_path      => "list",
+            human            => "boolean",
+            max_model_memory => "string",
+        },
+    },
+
+    'ml.get_buckets' => {
+        body  => {},
+        doc   => "ml-get-bucket",
+        parts => { job_id => {}, timestamp => {} },
+        paths => [
+            [   { job_id => 2, timestamp => 5 }, "_ml",
+                "anomaly_detectors",             "{job_id}",
+                "results",                       "buckets",
+                "{timestamp}",
+            ],
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "buckets",
+            ],
+        ],
+        qs => {
+            anomaly_score   => "double",
+            desc            => "boolean",
+            end             => "string",
+            error_trace     => "boolean",
+            exclude_interim => "boolean",
+            expand          => "boolean",
+            filter_path     => "list",
+            from            => "int",
+            human           => "boolean",
+            size            => "int",
+            sort            => "string",
+            start           => "string",
+        },
+    },
+
+    'ml.get_calendar_events' => {
+        doc   => "ml-get-calendar-event",
+        parts => { calendar_id => {} },
+        paths => [
+            [   { calendar_id => 2 }, "_ml",
+                "calendars",          "{calendar_id}",
+                "events",
+            ],
+        ],
+        qs => {
+            end         => "time",
+            error_trace => "boolean",
+            filter_path => "list",
+            from        => "int",
+            human       => "boolean",
+            job_id      => "string",
+            size        => "int",
+            start       => "string",
+        },
+    },
+
+    'ml.get_calendars' => {
+        body  => {},
+        doc   => "ml-get-calendar",
+        parts => { calendar_id => {} },
+        paths => [
+            [ { calendar_id => 2 }, "_ml", "calendars", "{calendar_id}" ],
+            [ {}, "_ml", "calendars" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            from        => "int",
+            human       => "boolean",
+            size        => "int",
+        },
+    },
+
+    'ml.get_categories' => {
+        body  => {},
+        doc   => "ml-get-category",
+        parts => { category_id => {}, job_id => {} },
+        paths => [
+            [   { category_id => 5, job_id => 2 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "results",                         "categories",
+                "{category_id}",
+            ],
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "categories",
+            ],
+        ],
+        qs => {
+            error_trace           => "boolean",
+            filter_path           => "list",
+            from                  => "int",
+            human                 => "boolean",
+            partition_field_value => "string",
+            size                  => "int",
+        },
+    },
+
+    'ml.get_data_frame_analytics' => {
+        doc   => "get-dfanalytics",
+        parts => { id => {} },
+        paths => [
+            [ { id => 3 }, "_ml", "data_frame", "analytics", "{id}" ],
+            [ {}, "_ml", "data_frame", "analytics" ],
+        ],
+        qs => {
+            allow_no_match    => "boolean",
+            error_trace       => "boolean",
+            exclude_generated => "boolean",
+            filter_path       => "list",
+            from              => "int",
+            human             => "boolean",
+            size              => "int",
+        },
+    },
+
+    'ml.get_data_frame_analytics_stats' => {
+        doc   => "get-dfanalytics-stats",
+        parts => { id => {} },
+        paths => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics",
+                "{id}",      "_stats"
+            ],
+            [ {}, "_ml", "data_frame", "analytics", "_stats" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            from           => "int",
+            human          => "boolean",
+            size           => "int",
+            verbose        => "boolean",
+        },
+    },
+
+    'ml.get_datafeed_stats' => {
+        doc   => "ml-get-datafeed-stats",
+        parts => { datafeed_id => {} },
+        paths => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_stats",
+            ],
+            [ {}, "_ml", "datafeeds", "_stats" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+        },
+    },
+
+    'ml.get_datafeeds' => {
+        doc   => "ml-get-datafeed",
+        parts => { datafeed_id => {} },
+        paths => [
+            [ { datafeed_id => 2 }, "_ml", "datafeeds", "{datafeed_id}" ],
+            [ {}, "_ml", "datafeeds" ],
+        ],
+        qs => {
+            allow_no_match    => "boolean",
+            error_trace       => "boolean",
+            exclude_generated => "boolean",
+            filter_path       => "list",
+            human             => "boolean",
+        },
+    },
+
+    'ml.get_filters' => {
+        doc   => "ml-get-filter",
+        parts => { filter_id => {} },
+        paths => [
+            [ { filter_id => 2 }, "_ml", "filters", "{filter_id}" ],
+            [ {}, "_ml", "filters" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            from        => "int",
+            human       => "boolean",
+            size        => "int",
+        },
+    },
+
+    'ml.get_influencers' => {
+        body  => {},
+        doc   => "ml-get-influencer",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "influencers",
+            ],
+        ],
+        qs => {
+            desc             => "boolean",
+            end              => "string",
+            error_trace      => "boolean",
+            exclude_interim  => "boolean",
+            filter_path      => "list",
+            from             => "int",
+            human            => "boolean",
+            influencer_score => "double",
+            size             => "int",
+            sort             => "string",
+            start            => "string",
+        },
+    },
+
+    'ml.get_job_stats' => {
+        doc   => "ml-get-job-stats",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_stats",
+            ],
+            [ {}, "_ml", "anomaly_detectors", "_stats" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+        },
+    },
+
+    'ml.get_jobs' => {
+        doc   => "ml-get-job",
+        parts => { job_id => {} },
+        paths => [
+            [ { job_id => 2 }, "_ml", "anomaly_detectors", "{job_id}" ],
+            [ {}, "_ml", "anomaly_detectors" ],
+        ],
+        qs => {
+            allow_no_match    => "boolean",
+            error_trace       => "boolean",
+            exclude_generated => "boolean",
+            filter_path       => "list",
+            human             => "boolean",
+        },
+    },
+
+    'ml.get_memory_stats' => {
+        doc   => "get-ml-memory",
+        parts => { node_id => {} },
+        paths => [
+            [ { node_id => 2 }, "_ml", "memory", "{node_id}", "_stats" ],
+            [ {}, "_ml", "memory", "_stats" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'ml.get_model_snapshot_upgrade_stats' => {
+        doc   => "ml-get-job-model-snapshot-upgrade-stats",
+        parts => { job_id => {}, snapshot_id => {} },
+        paths => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+                "_upgrade",                        "_stats",
+            ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+        },
+    },
+
+    'ml.get_model_snapshots' => {
+        body  => {},
+        doc   => "ml-get-snapshot",
+        parts => { job_id => {}, snapshot_id => {} },
+        paths => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+            ],
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "model_snapshots",
+            ],
+        ],
+        qs => {
+            desc        => "boolean",
+            end         => "time",
+            error_trace => "boolean",
+            filter_path => "list",
+            from        => "int",
+            human       => "boolean",
+            size        => "int",
+            sort        => "string",
+            start       => "time",
+        },
+    },
+
+    'ml.get_overall_buckets' => {
+        body  => {},
+        doc   => "ml-get-overall-buckets",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "overall_buckets",
+            ],
+        ],
+        qs => {
+            allow_no_match  => "boolean",
+            bucket_span     => "string",
+            end             => "string",
+            error_trace     => "boolean",
+            exclude_interim => "boolean",
+            filter_path     => "list",
+            human           => "boolean",
+            overall_score   => "double",
+            start           => "string",
+            top_n           => "int",
+        },
+    },
+
+    'ml.get_records' => {
+        body  => {},
+        doc   => "ml-get-record",
+        parts => { job_id => {} },
+        paths => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "results",           "records",
+            ],
+        ],
+        qs => {
+            desc            => "boolean",
+            end             => "string",
+            error_trace     => "boolean",
+            exclude_interim => "boolean",
+            filter_path     => "list",
+            from            => "int",
+            human           => "boolean",
+            record_score    => "double",
+            size            => "int",
+            sort            => "string",
+            start           => "string",
+        },
+    },
+
+    'ml.get_trained_models' => {
+        doc   => "get-trained-models",
+        parts => { model_id => {} },
+        paths => [
+            [ { model_id => 2 }, "_ml", "trained_models", "{model_id}" ],
+            [ {}, "_ml", "trained_models" ],
+        ],
+        qs => {
+            allow_no_match           => "boolean",
+            decompress_definition    => "boolean",
+            error_trace              => "boolean",
+            exclude_generated        => "boolean",
+            filter_path              => "list",
+            from                     => "int",
+            human                    => "boolean",
+            include                  => "string",
+            include_model_definition => "boolean",
+            size                     => "int",
+            tags                     => "list",
+        },
+    },
+
+    'ml.get_trained_models_stats' => {
+        doc   => "get-trained-models-stats",
+        parts => { model_id => {} },
+        paths => [
+            [   { model_id => 2 }, "_ml",
+                "trained_models",  "{model_id}",
+                "_stats",
+            ],
+            [ {}, "_ml", "trained_models", "_stats" ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            from           => "int",
+            human          => "boolean",
+            size           => "int",
+        },
+    },
+
+    'ml.infer_trained_model' => {
+        body   => { required => 1 },
+        doc    => "infer-trained-model",
+        method => "POST",
+        parts  => { model_id => { required => 1 } },
+        paths  => [
+            [   { model_id => 2 }, "_ml",
+                "trained_models",  "{model_id}",
+                "_infer",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'ml.info' => {
+        doc   => "get-ml-info",
+        parts => {},
+        paths => [ [ {}, "_ml", "info" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.open_job' => {
+        body   => {},
+        doc    => "ml-open-job",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_open"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.post_calendar_events' => {
+        body   => { required => 1 },
+        doc    => "ml-post-calendar-event",
+        method => "POST",
+        parts  => { calendar_id => {} },
+        paths  => [
+            [   { calendar_id => 2 }, "_ml",
+                "calendars",          "{calendar_id}",
+                "events",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.post_data' => {
+        body   => { required => 1 },
+        doc    => "ml-post-data",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_data"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            reset_end   => "string",
+            reset_start => "string",
+        },
+        serialize => "bulk",
+    },
+
+    'ml.preview_data_frame_analytics' => {
+        body  => {},
+        doc   => "preview-dfanalytics",
+        parts => { id => {} },
+        paths => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics",
+                "{id}",      "_preview"
+            ],
+            [ {}, "_ml", "data_frame", "analytics", "_preview" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.preview_datafeed' => {
+        body  => {},
+        doc   => "ml-preview-datafeed",
+        parts => { datafeed_id => {} },
+        paths => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_preview",
+            ],
+            [ {}, "_ml", "datafeeds", "_preview" ],
+        ],
+        qs => {
+            end         => "string",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            start       => "string",
+        },
+    },
+
+    'ml.put_calendar' => {
+        body   => {},
+        doc    => "ml-put-calendar",
+        method => "PUT",
+        parts  => { calendar_id => {} },
+        paths  =>
+            [ [ { calendar_id => 2 }, "_ml", "calendars", "{calendar_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_calendar_job' => {
+        doc    => "ml-put-calendar-job",
+        method => "PUT",
+        parts  => { calendar_id => {}, job_id => {} },
+        paths  => [
+            [   { calendar_id => 2, job_id => 4 },
+                "_ml", "calendars", "{calendar_id}", "jobs", "{job_id}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_data_frame_analytics' => {
+        body   => { required => 1 },
+        doc    => "put-dfanalytics",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  =>
+            [ [ { id => 3 }, "_ml", "data_frame", "analytics", "{id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_datafeed' => {
+        body   => { required => 1 },
+        doc    => "ml-put-datafeed",
+        method => "PUT",
+        parts  => { datafeed_id => {} },
+        paths  =>
+            [ [ { datafeed_id => 2 }, "_ml", "datafeeds", "{datafeed_id}" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_throttled   => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'ml.put_filter' => {
+        body   => { required => 1 },
+        doc    => "ml-put-filter",
+        method => "PUT",
+        parts  => { filter_id => {} },
+        paths  => [ [ { filter_id => 2 }, "_ml", "filters", "{filter_id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_job' => {
+        body   => { required => 1 },
+        doc    => "ml-put-job",
+        method => "PUT",
+        parts  => { job_id => {} },
+        paths  =>
+            [ [ { job_id => 2 }, "_ml", "anomaly_detectors", "{job_id}" ] ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_throttled   => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'ml.put_trained_model' => {
+        body   => { required => 1 },
+        doc    => "put-trained-models",
+        method => "PUT",
+        parts  => { model_id => {} },
+        paths  =>
+            [ [ { model_id => 2 }, "_ml", "trained_models", "{model_id}" ] ],
+        qs => {
+            defer_definition_decompression => "boolean",
+            error_trace                    => "boolean",
+            filter_path                    => "list",
+            human                          => "boolean",
+        },
+    },
+
+    'ml.put_trained_model_alias' => {
+        doc    => "put-trained-models-aliases",
+        method => "PUT",
+        parts  => { model_alias => {}, model_id => {} },
+        paths  => [
+            [   { model_alias => 4, model_id => 2 }, "_ml",
+                "trained_models",                    "{model_id}",
+                "model_aliases",                     "{model_alias}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            reassign    => "boolean",
+        },
+    },
+
+    'ml.put_trained_model_definition_part' => {
+        body   => { required => 1 },
+        doc    => "put-trained-model-definition-part",
+        method => "PUT",
+        parts  => { model_id => {}, part => {} },
+        paths  => [
+            [   { model_id => 2, part => 4 }, "_ml",
+                "trained_models",             "{model_id}",
+                "definition",                 "{part}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.put_trained_model_vocabulary' => {
+        body   => { required => 1 },
+        doc    => "put-trained-model-vocabulary",
+        method => "PUT",
+        parts  => { model_id => {} },
+        paths  => [
+            [   { model_id => 2 }, "_ml",
+                "trained_models",  "{model_id}",
+                "vocabulary",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.reset_job' => {
+        doc    => "ml-reset-job",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_reset",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'ml.revert_model_snapshot' => {
+        body   => {},
+        doc    => "ml-revert-snapshot",
+        method => "POST",
+        parts  => { job_id => {}, snapshot_id => {} },
+        paths  => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+                "_revert",
+            ],
+        ],
+        qs => {
+            delete_intervening_results => "boolean",
+            error_trace                => "boolean",
+            filter_path                => "list",
+            human                      => "boolean",
+        },
+    },
+
+    'ml.set_upgrade_mode' => {
+        doc    => "ml-set-upgrade-mode",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ml", "set_upgrade_mode" ] ],
+        qs     => {
+            enabled     => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'ml.start_data_frame_analytics' => {
+        body   => {},
+        doc    => "start-dfanalytics",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics",
+                "{id}",      "_start"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'ml.start_datafeed' => {
+        body   => {},
+        doc    => "ml-start-datafeed",
+        method => "POST",
+        parts  => { datafeed_id => {} },
+        paths  => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_start",
+            ],
+        ],
+        qs => {
+            end         => "string",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            start       => "string",
+            timeout     => "time",
+        },
+    },
+
+    'ml.start_trained_model_deployment' => {
+        doc    => "start-trained-model-deployment",
+        method => "POST",
+        parts  => { model_id => { required => 1 } },
+        paths  => [
+            [   { model_id => 2 }, "_ml",
+                "trained_models",  "{model_id}",
+                "deployment",      "_start",
+            ],
+        ],
+        qs => {
+            cache_size             => "string",
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            number_of_allocations  => "int",
+            queue_capacity         => "int",
+            threads_per_allocation => "int",
+            timeout                => "time",
+            wait_for               => "string",
+        },
+    },
+
+    'ml.stop_data_frame_analytics' => {
+        body   => {},
+        doc    => "stop-dfanalytics",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics", "{id}",
+                "_stop"
+            ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            force          => "boolean",
+            human          => "boolean",
+            timeout        => "time",
+        },
+    },
+
+    'ml.stop_datafeed' => {
+        body   => {},
+        doc    => "ml-stop-datafeed",
+        method => "POST",
+        parts  => { datafeed_id => {} },
+        paths  => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_stop",
+            ],
+        ],
+        qs => {
+            allow_no_datafeeds => "boolean",
+            allow_no_match     => "boolean",
+            error_trace        => "boolean",
+            filter_path        => "list",
+            force              => "boolean",
+            human              => "boolean",
+            timeout            => "time",
+        },
+    },
+
+    'ml.stop_trained_model_deployment' => {
+        body   => {},
+        doc    => "stop-trained-model-deployment",
+        method => "POST",
+        parts  => { model_id => { required => 1 } },
+        paths  => [
+            [   { model_id => 2 }, "_ml",
+                "trained_models",  "{model_id}",
+                "deployment",      "_stop",
+            ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            force          => "boolean",
+            human          => "boolean",
+        },
+    },
+
+    'ml.update_data_frame_analytics' => {
+        body   => { required => 1 },
+        doc    => "update-dfanalytics",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [
+            [   { id => 3 }, "_ml", "data_frame", "analytics",
+                "{id}",      "_update"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.update_datafeed' => {
+        body   => { required => 1 },
+        doc    => "ml-update-datafeed",
+        method => "POST",
+        parts  => { datafeed_id => {} },
+        paths  => [
+            [   { datafeed_id => 2 }, "_ml",
+                "datafeeds",          "{datafeed_id}",
+                "_update",
+            ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_throttled   => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'ml.update_filter' => {
+        body   => { required => 1 },
+        doc    => "ml-update-filter",
+        method => "POST",
+        parts  => { filter_id => {} },
+        paths  => [
+            [   { filter_id => 2 }, "_ml", "filters", "{filter_id}",
+                "_update"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.update_job' => {
+        body   => { required => 1 },
+        doc    => "ml-update-job",
+        method => "POST",
+        parts  => { job_id => {} },
+        paths  => [
+            [   { job_id => 2 },     "_ml",
+                "anomaly_detectors", "{job_id}",
+                "_update",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.update_model_snapshot' => {
+        body   => { required => 1 },
+        doc    => "ml-update-snapshot",
+        method => "POST",
+        parts  => { job_id => {}, snapshot_id => {} },
+        paths  => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+                "_update",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.upgrade_job_snapshot' => {
+        doc    => "ml-upgrade-job-model-snapshot",
+        method => "POST",
+        parts  => { job_id => {}, snapshot_id => {} },
+        paths  => [
+            [   { job_id => 2, snapshot_id => 4 }, "_ml",
+                "anomaly_detectors",               "{job_id}",
+                "model_snapshots",                 "{snapshot_id}",
+                "_upgrade",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            timeout             => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'ml.validate' => {
+        body   => { required => 1 },
+        doc    => "ml-jobs",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_ml", "anomaly_detectors", "_validate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ml.validate_detector' => {
+        body   => { required => 1 },
+        doc    => "ml-jobs",
+        method => "POST",
+        parts  => {},
+        paths  =>
+            [ [ {}, "_ml", "anomaly_detectors", "_validate", "detector" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'monitoring.bulk' => {
+        body   => { required => 1 },
+        doc    => "monitor-elasticsearch-cluster",
+        method => "POST",
+        parts  => { type => {} },
+        paths  => [
+            [ { type => 1 }, "_monitoring", "{type}", "bulk" ],
+            [ {}, "_monitoring", "bulk" ],
+        ],
+        qs => {
+            error_trace        => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+            interval           => "string",
+            system_api_version => "string",
+            system_id          => "string",
+        },
+        serialize => "bulk",
+    },
+
+    'nodes.clear_repositories_metering_archive' => {
+        doc    => "clear-repositories-metering-archive-api",
+        method => "DELETE",
+        parts  => { max_archive_version => {}, node_id => { multi => 1 } },
+        paths  => [
+            [   { max_archive_version => 3, node_id => 1 },
+                "_nodes", "{node_id}", "_repositories_metering",
+                "{max_archive_version}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'nodes.get_repositories_metering_info' => {
+        doc   => "get-repositories-metering-api",
+        parts => { node_id => { multi => 1 } },
+        paths => [
+            [   { node_id => 1 }, "_nodes",
+                "{node_id}",      "_repositories_metering",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'nodes.hot_threads' => {
+        doc   => "cluster-nodes-hot-threads",
+        parts => { node_id => { multi => 1 } },
+        paths => [
+            [ { node_id => 1 }, "_nodes", "{node_id}", "hot_threads" ],
+            [ {}, "_nodes", "hot_threads" ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            ignore_idle_threads => "boolean",
+            interval            => "time",
+            snapshots           => "number",
+            sort                => "enum",
+            threads             => "number",
+            timeout             => "time",
+            type                => "enum",
+        },
+    },
+
+    'nodes.info' => {
+        doc   => "cluster-nodes-info",
+        parts => { metric => { multi => 1 }, node_id => { multi => 1 } },
+        paths => [
+            [   { metric => 2, node_id => 1 }, "_nodes",
+                "{node_id}",                   "{metric}",
+            ],
+            [ { metric  => 1 }, "_nodes", "{metric}" ],
+            [ { node_id => 1 }, "_nodes", "{node_id}" ],
+            [ {}, "_nodes" ],
+        ],
+        qs => {
+            error_trace   => "boolean",
+            filter_path   => "list",
+            flat_settings => "boolean",
+            human         => "boolean",
+            timeout       => "time",
+        },
+    },
+
+    'nodes.reload_secure_settings' => {
+        body   => {},
+        doc    => "",
+        method => "POST",
+        parts  => { node_id => { multi => 1 } },
+        paths  => [
+            [   { node_id => 1 }, "_nodes",
+                "{node_id}",      "reload_secure_settings",
+            ],
+            [ {}, "_nodes", "reload_secure_settings" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'nodes.stats' => {
+        doc   => "cluster-nodes-stats",
+        parts => {
+            index_metric => { multi => 1 },
+            metric       => { multi => 1 },
+            node_id      => { multi => 1 },
+        },
+        paths => [
+            [   { index_metric => 4, metric => 3, node_id => 1 },
+                "_nodes", "{node_id}", "stats", "{metric}", "{index_metric}",
+            ],
+            [   { index_metric => 3, metric => 2 }, "_nodes",
+                "stats",                            "{metric}",
+                "{index_metric}",
+            ],
+            [   { metric => 3, node_id => 1 }, "_nodes",
+                "{node_id}",                   "stats",
+                "{metric}",
+            ],
+            [ { metric  => 2 }, "_nodes", "stats",     "{metric}" ],
+            [ { node_id => 1 }, "_nodes", "{node_id}", "stats" ],
+            [ {}, "_nodes", "stats" ],
+        ],
+        qs => {
+            completion_fields          => "list",
+            error_trace                => "boolean",
+            fielddata_fields           => "list",
+            fields                     => "list",
+            filter_path                => "list",
+            groups                     => "boolean",
+            human                      => "boolean",
+            include_segment_file_sizes => "boolean",
+            include_unloaded_segments  => "boolean",
+            level                      => "enum",
+            timeout                    => "time",
+            types                      => "list",
+        },
+    },
+
+    'nodes.usage' => {
+        doc   => "cluster-nodes-usage",
+        parts => { metric => { multi => 1 }, node_id => { multi => 1 } },
+        paths => [
+            [   { metric => 3, node_id => 1 }, "_nodes",
+                "{node_id}",                   "usage",
+                "{metric}",
+            ],
+            [ { metric  => 2 }, "_nodes", "usage",     "{metric}" ],
+            [ { node_id => 1 }, "_nodes", "{node_id}", "usage" ],
+            [ {}, "_nodes", "usage" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'rollup.delete_job' => {
+        doc    => "rollup-delete-job",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_rollup", "job", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.get_jobs' => {
+        doc   => "rollup-get-job",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_rollup", "job", "{id}" ],
+            [ {}, "_rollup", "job" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.get_rollup_caps' => {
+        doc   => "rollup-get-rollup-caps",
+        parts => { id => {} },
+        paths => [
+            [ { id => 2 }, "_rollup", "data", "{id}" ],
+            [ {}, "_rollup", "data" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.get_rollup_index_caps' => {
+        doc   => "rollup-get-rollup-index-caps",
+        parts => { index => {} },
+        paths => [ [ { index => 0 }, "{index}", "_rollup", "data" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.put_job' => {
+        body   => { required => 1 },
+        doc    => "rollup-put-job",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_rollup", "job", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.rollup_search' => {
+        body  => { required => 1 },
+        doc   => "rollup-search",
+        parts => { index => { multi => 1 } },
+        paths => [ [ { index => 0 }, "{index}", "_rollup_search" ] ],
+        qs    => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            rest_total_hits_as_int => "boolean",
+            typed_keys             => "boolean",
+        },
+    },
+
+    'rollup.start_job' => {
+        doc    => "rollup-start-job",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_rollup", "job", "{id}", "_start" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'rollup.stop_job' => {
+        doc    => "rollup-stop-job",
+        method => "POST",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_rollup", "job", "{id}", "_stop" ] ],
+        qs     => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            timeout             => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'searchable_snapshots.cache_stats' => {
+        doc   => "searchable-snapshots-apis",
+        parts => { node_id => { multi => 1 } },
+        paths => [
+            [   { node_id => 1 }, "_searchable_snapshots",
+                "{node_id}",      "cache",
+                "stats",
+            ],
+            [ {}, "_searchable_snapshots", "cache", "stats" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'searchable_snapshots.clear_cache' => {
+        doc    => "searchable-snapshots-apis",
+        method => "POST",
+        parts  => { index => { multi => 1 } },
+        paths  => [
+            [   { index => 0 },          "{index}",
+                "_searchable_snapshots", "cache",
+                "clear",
+            ],
+            [ {}, "_searchable_snapshots", "cache", "clear" ],
+        ],
+        qs => {
+            allow_no_indices   => "boolean",
+            error_trace        => "boolean",
+            expand_wildcards   => "enum",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+        },
+    },
+
+    'searchable_snapshots.mount' => {
+        body   => { required => 1 },
+        doc    => "searchable-snapshots-api-mount-snapshot",
+        method => "POST",
+        parts  => { repository => {}, snapshot => {} },
+        paths  => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+                "_mount",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            master_timeout      => "time",
+            storage             => "string",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'searchable_snapshots.stats' => {
+        doc   => "searchable-snapshots-apis",
+        parts => { index => { multi => 1 } },
+        paths => [
+            [ { index => 0 }, "{index}", "_searchable_snapshots", "stats" ],
+            [ {}, "_searchable_snapshots", "stats" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            level       => "enum",
+        },
+    },
+
+    'security.activate_user_profile' => {
+        body   => { required => 1 },
+        doc    => "security-api-activate-user-profile",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "profile", "_activate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.authenticate' => {
+        doc   => "security-api-authenticate",
+        parts => {},
+        paths => [ [ {}, "_security", "_authenticate" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.bulk_update_api_keys' => {
+        body   => { required => 1 },
+        doc    => "security-api-bulk-update-api-keys",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "api_key", "_bulk_update" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.change_password' => {
+        body   => { required => 1 },
+        doc    => "security-api-change-password",
+        method => "PUT",
+        parts  => { username => {} },
+        paths  => [
+            [   { username => 2 }, "_security",
+                "user",            "{username}",
+                "_password",
+            ],
+            [ {}, "_security", "user", "_password" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.clear_api_key_cache' => {
+        doc    => "security-api-clear-api-key-cache",
+        method => "POST",
+        parts  => { ids => { multi => 1 } },
+        paths  => [
+            [ { ids => 2 }, "_security", "api_key", "{ids}", "_clear_cache" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.clear_cached_privileges' => {
+        doc    => "security-api-clear-privilege-cache",
+        method => "POST",
+        parts  => { application => { multi => 1 } },
+        paths  => [
+            [   { application => 2 }, "_security",
+                "privilege",          "{application}",
+                "_clear_cache",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.clear_cached_realms' => {
+        doc    => "security-api-clear-cache",
+        method => "POST",
+        parts  => { realms => { multi => 1 } },
+        paths  => [
+            [   { realms => 2 }, "_security", "realm", "{realms}",
+                "_clear_cache",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            usernames   => "list",
+        },
+    },
+
+    'security.clear_cached_roles' => {
+        doc    => "security-api-clear-role-cache",
+        method => "POST",
+        parts  => { name => { multi => 1 } },
+        paths  => [
+            [ { name => 2 }, "_security", "role", "{name}", "_clear_cache" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.clear_cached_service_tokens' => {
+        doc    => "security-api-clear-service-token-caches",
+        method => "POST",
+        parts  => { name => { multi => 1 }, namespace => {}, service => {} },
+        paths  => [
+            [   { name => 6, namespace => 2, service => 3 }, "_security",
+                "service",                                   "{namespace}",
+                "{service}",                                 "credential",
+                "token",                                     "{name}",
+                "_clear_cache",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.create_api_key' => {
+        body   => { required => 1 },
+        doc    => "security-api-create-api-key",
+        method => "PUT",
+        parts  => {},
+        paths  => [ [ {}, "_security", "api_key" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.create_service_token' => {
+        doc    => "security-api-create-service-token",
+        method => "POST",
+        parts  => { name => {}, namespace => {}, service => {} },
+        paths  => [
+            [   { name => 6, namespace => 2, service => 3 }, "_security",
+                "service",                                   "{namespace}",
+                "{service}",                                 "credential",
+                "token",                                     "{name}",
+            ],
+            [   { namespace => 2, service => 3 }, "_security",
+                "service",                        "{namespace}",
+                "{service}",                      "credential",
+                "token",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.delete_privileges' => {
+        doc    => "security-api-delete-privilege",
+        method => "DELETE",
+        parts  => { application => {}, name => {} },
+        paths  => [
+            [   { application => 2, name => 3 }, "_security",
+                "privilege",                     "{application}",
+                "{name}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.delete_role' => {
+        doc    => "security-api-delete-role",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_security", "role", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.delete_role_mapping' => {
+        doc    => "security-api-delete-role-mapping",
+        method => "DELETE",
+        parts  => { name => {} },
+        paths => [ [ { name => 2 }, "_security", "role_mapping", "{name}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.delete_service_token' => {
+        doc    => "security-api-delete-service-token",
+        method => "DELETE",
+        parts  => { name => {}, namespace => {}, service => {} },
+        paths  => [
+            [   { name => 6, namespace => 2, service => 3 }, "_security",
+                "service",                                   "{namespace}",
+                "{service}",                                 "credential",
+                "token",                                     "{name}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.delete_user' => {
+        doc    => "security-api-delete-user",
+        method => "DELETE",
+        parts  => { username => {} },
+        paths => [ [ { username => 2 }, "_security", "user", "{username}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.disable_user' => {
+        doc    => "security-api-disable-user",
+        method => "PUT",
+        parts  => { username => {} },
+        paths  => [
+            [   { username => 2 }, "_security",
+                "user",            "{username}",
+                "_disable"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.disable_user_profile' => {
+        doc    => "security-api-disable-user-profile",
+        method => "PUT",
+        parts  => { uid => {} },
+        paths  =>
+            [ [ { uid => 2 }, "_security", "profile", "{uid}", "_disable" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.enable_user' => {
+        doc    => "security-api-enable-user",
+        method => "PUT",
+        parts  => { username => {} },
+        paths  => [
+            [   { username => 2 }, "_security",
+                "user",            "{username}",
+                "_enable"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.enable_user_profile' => {
+        doc    => "security-api-enable-user-profile",
+        method => "PUT",
+        parts  => { uid => {} },
+        paths  =>
+            [ [ { uid => 2 }, "_security", "profile", "{uid}", "_enable" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.enroll_kibana' => {
+        doc   => "security-api-kibana-enrollment",
+        parts => {},
+        paths => [ [ {}, "_security", "enroll", "kibana" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.enroll_node' => {
+        doc   => "security-api-node-enrollment",
+        parts => {},
+        paths => [ [ {}, "_security", "enroll", "node" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_api_key' => {
+        doc   => "security-api-get-api-key",
+        parts => {},
+        paths => [ [ {}, "_security", "api_key" ] ],
+        qs    => {
+            error_trace     => "boolean",
+            filter_path     => "list",
+            human           => "boolean",
+            id              => "string",
+            name            => "string",
+            owner           => "boolean",
+            realm_name      => "string",
+            username        => "string",
+            with_limited_by => "boolean",
+        },
+    },
+
+    'security.get_builtin_privileges' => {
+        doc   => "security-api-get-builtin-privileges",
+        parts => {},
+        paths => [ [ {}, "_security", "privilege", "_builtin" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_privileges' => {
+        doc   => "security-api-get-privileges",
+        parts => { application => {}, name => {} },
+        paths => [
+            [   { application => 2, name => 3 }, "_security",
+                "privilege",                     "{application}",
+                "{name}",
+            ],
+            [   { application => 2 }, "_security",
+                "privilege",          "{application}"
+            ],
+            [ {}, "_security", "privilege" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_role' => {
+        doc   => "security-api-get-role",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 2 }, "_security", "role", "{name}" ],
+            [ {}, "_security", "role" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_role_mapping' => {
+        doc   => "security-api-get-role-mapping",
+        parts => { name => { multi => 1 } },
+        paths => [
+            [ { name => 2 }, "_security", "role_mapping", "{name}" ],
+            [ {}, "_security", "role_mapping" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_service_accounts' => {
+        doc   => "security-api-get-service-accounts",
+        parts => { namespace => {}, service => {} },
+        paths => [
+            [   { namespace => 2, service => 3 }, "_security",
+                "service",                        "{namespace}",
+                "{service}",
+            ],
+            [ { namespace => 2 }, "_security", "service", "{namespace}" ],
+            [ {}, "_security", "service" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_service_credentials' => {
+        doc   => "security-api-get-service-credentials",
+        parts => { namespace => {}, service => {} },
+        paths => [
+            [   { namespace => 2, service => 3 }, "_security",
+                "service",                        "{namespace}",
+                "{service}",                      "credential",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_token' => {
+        body   => { required => 1 },
+        doc    => "security-api-get-token",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "oauth2", "token" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_user' => {
+        doc   => "security-api-get-user",
+        parts => { username => { multi => 1 } },
+        paths => [
+            [ { username => 2 }, "_security", "user", "{username}" ],
+            [ {}, "_security", "user" ],
+        ],
+        qs => {
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            with_profile_uid => "boolean",
+        },
+    },
+
+    'security.get_user_privileges' => {
+        doc   => "security-api-get-user-privileges",
+        parts => {},
+        paths => [ [ {}, "_security", "user", "_privileges" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.get_user_profile' => {
+        doc   => "security-api-get-user-profile",
+        parts => { uid => { multi => 1 } },
+        paths => [ [ { uid => 2 }, "_security", "profile", "{uid}" ] ],
+        qs    => {
+            data        => "list",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'security.grant_api_key' => {
+        body   => { required => 1 },
+        doc    => "security-api-grant-api-key",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "api_key", "grant" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.has_privileges' => {
+        body  => { required => 1 },
+        doc   => "security-api-has-privileges",
+        parts => { user => {} },
+        paths => [
+            [   { user => 2 }, "_security",
+                "user",        "{user}",
+                "_has_privileges"
+            ],
+            [ {}, "_security", "user", "_has_privileges" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.has_privileges_user_profile' => {
+        body  => { required => 1 },
+        doc   => "security-api-has-privileges-user-profile",
+        parts => {},
+        paths => [ [ {}, "_security", "profile", "_has_privileges" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.invalidate_api_key' => {
+        body   => { required => 1 },
+        doc    => "security-api-invalidate-api-key",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_security", "api_key" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.invalidate_token' => {
+        body   => { required => 1 },
+        doc    => "security-api-invalidate-token",
+        method => "DELETE",
+        parts  => {},
+        paths  => [ [ {}, "_security", "oauth2", "token" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.oidc_authenticate' => {
+        body   => { required => 1 },
+        doc    => "security-api-oidc-authenticate",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "oidc", "authenticate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.oidc_logout' => {
+        body   => { required => 1 },
+        doc    => "security-api-oidc-logout",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "oidc", "logout" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.oidc_prepare_authentication' => {
+        body   => { required => 1 },
+        doc    => "security-api-oidc-prepare-authentication",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "oidc", "prepare" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.put_privileges' => {
+        body   => { required => 1 },
+        doc    => "security-api-put-privileges",
+        method => "PUT",
+        parts  => {},
+        paths  => [ [ {}, "_security", "privilege" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.put_role' => {
+        body   => { required => 1 },
+        doc    => "security-api-put-role",
+        method => "PUT",
+        parts  => { name => {} },
+        paths  => [ [ { name => 2 }, "_security", "role", "{name}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.put_role_mapping' => {
+        body   => { required => 1 },
+        doc    => "security-api-put-role-mapping",
+        method => "PUT",
+        parts  => { name => {} },
+        paths => [ [ { name => 2 }, "_security", "role_mapping", "{name}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.put_user' => {
+        body   => { required => 1 },
+        doc    => "security-api-put-user",
+        method => "PUT",
+        parts  => { username => {} },
+        paths => [ [ { username => 2 }, "_security", "user", "{username}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            refresh     => "enum",
+        },
+    },
+
+    'security.query_api_keys' => {
+        body  => {},
+        doc   => "security-api-query-api-key",
+        parts => {},
+        paths => [ [ {}, "_security", "_query", "api_key" ] ],
+        qs    => {
+            error_trace     => "boolean",
+            filter_path     => "list",
+            human           => "boolean",
+            with_limited_by => "boolean",
+        },
+    },
+
+    'security.saml_authenticate' => {
+        body   => { required => 1 },
+        doc    => "security-api-saml-authenticate",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "saml", "authenticate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.saml_complete_logout' => {
+        body   => { required => 1 },
+        doc    => "security-api-saml-complete-logout",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "saml", "complete_logout" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.saml_invalidate' => {
+        body   => { required => 1 },
+        doc    => "security-api-saml-invalidate",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "saml", "invalidate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.saml_logout' => {
+        body   => { required => 1 },
+        doc    => "security-api-saml-logout",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "saml", "logout" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.saml_prepare_authentication' => {
+        body   => { required => 1 },
+        doc    => "security-api-saml-prepare-authentication",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_security", "saml", "prepare" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.saml_service_provider_metadata' => {
+        doc   => "security-api-saml-sp-metadata",
+        parts => { realm_name => {} },
+        paths => [
+            [   { realm_name => 3 }, "_security",
+                "saml",              "metadata",
+                "{realm_name}",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.suggest_user_profiles' => {
+        body  => {},
+        doc   => "security-api-suggest-user-profile",
+        parts => {},
+        paths => [ [ {}, "_security", "profile", "_suggest" ] ],
+        qs    => {
+            data        => "list",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'security.update_api_key' => {
+        body   => {},
+        doc    => "security-api-update-api-key",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_security", "api_key", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'security.update_user_profile_data' => {
+        body   => { required => 1 },
+        doc    => "security-api-update-user-profile-data",
+        method => "PUT",
+        parts  => { uid => {} },
+        paths  =>
+            [ [ { uid => 2 }, "_security", "profile", "{uid}", "_data" ] ],
+        qs => {
+            error_trace     => "boolean",
+            filter_path     => "list",
+            human           => "boolean",
+            if_primary_term => "number",
+            if_seq_no       => "number",
+            refresh         => "enum",
+        },
+    },
+
+    'shutdown.delete_node' => {
+        doc    => "",
+        method => "DELETE",
+        parts  => { node_id => {} },
+        paths  => [ [ { node_id => 1 }, "_nodes", "{node_id}", "shutdown" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'shutdown.get_node' => {
+        doc   => "",
+        parts => { node_id => {} },
+        paths => [
+            [ { node_id => 1 }, "_nodes", "{node_id}", "shutdown" ],
+            [ {}, "_nodes", "shutdown" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'shutdown.put_node' => {
+        body   => { required => 1 },
+        doc    => "",
+        method => "PUT",
+        parts  => { node_id => {} },
+        paths  => [ [ { node_id => 1 }, "_nodes", "{node_id}", "shutdown" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.delete_lifecycle' => {
+        doc    => "slm-api-delete-policy",
+        method => "DELETE",
+        parts  => { policy_id => {} },
+        paths  => [ [ { policy_id => 2 }, "_slm", "policy", "{policy_id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.execute_lifecycle' => {
+        doc    => "slm-api-execute-lifecycle",
+        method => "PUT",
+        parts  => { policy_id => {} },
+        paths  => [
+            [   { policy_id => 2 }, "_slm",
+                "policy",           "{policy_id}",
+                "_execute"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.execute_retention' => {
+        doc    => "slm-api-execute-retention",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_slm", "_execute_retention" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.get_lifecycle' => {
+        doc   => "slm-api-get-policy",
+        parts => { policy_id => { multi => 1 } },
+        paths => [
+            [ { policy_id => 2 }, "_slm", "policy", "{policy_id}" ],
+            [ {}, "_slm", "policy" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.get_stats' => {
+        doc   => "slm-api-get-stats",
+        parts => {},
+        paths => [ [ {}, "_slm", "stats" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.get_status' => {
+        doc   => "slm-api-get-status",
+        parts => {},
+        paths => [ [ {}, "_slm", "status" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.put_lifecycle' => {
+        body   => {},
+        doc    => "slm-api-put-policy",
+        method => "PUT",
+        parts  => { policy_id => {} },
+        paths  => [ [ { policy_id => 2 }, "_slm", "policy", "{policy_id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.start' => {
+        doc    => "slm-api-start",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_slm", "start" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'slm.stop' => {
+        doc    => "slm-api-stop",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_slm", "stop" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'snapshot.cleanup_repository' => {
+        doc    => "clean-up-snapshot-repo-api",
+        method => "POST",
+        parts  => { repository => {} },
+        paths  => [
+            [ { repository => 1 }, "_snapshot", "{repository}", "_cleanup" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'snapshot.clone' => {
+        body   => { required => 1 },
+        doc    => "modules-snapshots",
+        method => "PUT",
+        parts  => { repository => {}, snapshot => {}, target_snapshot => {} },
+        paths  => [
+            [   { repository => 1, snapshot => 2, target_snapshot => 4 },
+                "_snapshot",
+                "{repository}",
+                "{snapshot}",
+                "_clone",
+                "{target_snapshot}",
+            ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'snapshot.create' => {
+        body   => {},
+        doc    => "modules-snapshots",
+        method => "PUT",
+        parts  => { repository => {}, snapshot => {} },
+        paths  => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            master_timeout      => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'snapshot.create_repository' => {
+        body   => { required => 1 },
+        doc    => "modules-snapshots",
+        method => "PUT",
+        parts  => { repository => {} },
+        paths  => [ [ { repository => 1 }, "_snapshot", "{repository}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+            verify         => "boolean",
+        },
+    },
+
+    'snapshot.delete' => {
+        doc    => "modules-snapshots",
+        method => "DELETE",
+        parts  => { repository => {}, snapshot => { multi => 1 } },
+        paths  => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+            ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'snapshot.delete_repository' => {
+        doc    => "modules-snapshots",
+        method => "DELETE",
+        parts  => { repository => { multi => 1 } },
+        paths  => [ [ { repository => 1 }, "_snapshot", "{repository}" ] ],
+        qs     => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'snapshot.get' => {
+        doc   => "modules-snapshots",
+        parts => { repository => {}, snapshot => { multi => 1 } },
+        paths => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+            ],
+        ],
+        qs => {
+            after              => "string",
+            error_trace        => "boolean",
+            filter_path        => "list",
+            from_sort_value    => "string",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            include_repository => "boolean",
+            index_details      => "boolean",
+            index_names        => "boolean",
+            master_timeout     => "time",
+            offset             => "integer",
+            order              => "enum",
+            size               => "integer",
+            slm_policy_filter  => "string",
+            sort               => "enum",
+            verbose            => "boolean",
+        },
+    },
+
+    'snapshot.get_repository' => {
+        doc   => "modules-snapshots",
+        parts => { repository => { multi => 1 } },
+        paths => [
+            [ { repository => 1 }, "_snapshot", "{repository}" ],
+            [ {}, "_snapshot" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            local          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+    'snapshot.repository_analyze' => {
+        doc    => "modules-snapshots",
+        method => "POST",
+        parts  => { repository => {} },
+        paths  => [
+            [ { repository => 1 }, "_snapshot", "{repository}", "_analyze" ],
+        ],
+        qs => {
+            blob_count              => "number",
+            concurrency             => "number",
+            detailed                => "boolean",
+            early_read_node_count   => "number",
+            error_trace             => "boolean",
+            filter_path             => "list",
+            human                   => "boolean",
+            max_blob_size           => "string",
+            max_total_data_size     => "string",
+            rare_action_probability => "number",
+            rarely_abort_writes     => "boolean",
+            read_node_count         => "number",
+            seed                    => "number",
+            timeout                 => "time",
+        },
+    },
+
+    'snapshot.restore' => {
+        body   => {},
+        doc    => "modules-snapshots",
+        method => "POST",
+        parts  => { repository => {}, snapshot => {} },
+        paths  => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+                "_restore",
+            ],
+        ],
+        qs => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            master_timeout      => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'snapshot.status' => {
+        doc   => "modules-snapshots",
+        parts => { repository => {}, snapshot => { multi => 1 } },
+        paths => [
+            [   { repository => 1, snapshot => 2 }, "_snapshot",
+                "{repository}",                     "{snapshot}",
+                "_status",
+            ],
+            [ { repository => 1 }, "_snapshot", "{repository}", "_status" ],
+            [ {}, "_snapshot", "_status" ],
+        ],
+        qs => {
+            error_trace        => "boolean",
+            filter_path        => "list",
+            human              => "boolean",
+            ignore_unavailable => "boolean",
+            master_timeout     => "time",
+        },
+    },
+
+    'snapshot.verify_repository' => {
+        doc    => "modules-snapshots",
+        method => "POST",
+        parts  => { repository => {} },
+        paths  => [
+            [ { repository => 1 }, "_snapshot", "{repository}", "_verify" ],
+        ],
+        qs => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+            timeout        => "time",
+        },
+    },
+
+    'sql.clear_cursor' => {
+        body   => { required => 1 },
+        doc    => "clear-sql-cursor-api",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_sql", "close" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'sql.delete_async' => {
+        doc    => "delete-async-sql-search-api",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 3 }, "_sql", "async", "delete", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'sql.get_async' => {
+        doc   => "get-async-sql-search-api",
+        parts => { id => {} },
+        paths => [ [ { id => 2 }, "_sql", "async", "{id}" ] ],
+        qs    => {
+            delimiter                   => "string",
+            error_trace                 => "boolean",
+            filter_path                 => "list",
+            format                      => "string",
+            human                       => "boolean",
+            keep_alive                  => "time",
+            wait_for_completion_timeout => "time",
+        },
+    },
+
+    'sql.get_async_status' => {
+        doc   => "get-async-sql-search-status-api",
+        parts => { id => {} },
+        paths => [ [ { id => 3 }, "_sql", "async", "status", "{id}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'sql.query' => {
+        body   => { required => 1 },
+        doc    => "sql-search-api",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_sql" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            format      => "string",
+            human       => "boolean",
+        },
+    },
+
+    'sql.translate' => {
+        body   => { required => 1 },
+        doc    => "sql-translate-api",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_sql", "translate" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'ssl.certificates' => {
+        doc   => "security-api-ssl",
+        parts => {},
+        paths => [ [ {}, "_ssl", "certificates" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'tasks.cancel' => {
+        doc    => "tasks",
+        method => "POST",
+        parts  => { task_id => {} },
+        paths  => [
+            [ { task_id => 1 }, "_tasks", "{task_id}", "_cancel" ],
+            [ {}, "_tasks", "_cancel" ],
+        ],
+        qs => {
+            actions             => "list",
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            nodes               => "list",
+            parent_task_id      => "string",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'tasks.get' => {
+        doc   => "tasks",
+        parts => { task_id => {} },
+        paths => [ [ { task_id => 1 }, "_tasks", "{task_id}" ] ],
+        qs    => {
+            error_trace         => "boolean",
+            filter_path         => "list",
+            human               => "boolean",
+            timeout             => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'tasks.list' => {
+        doc   => "tasks",
+        parts => {},
+        paths => [ [ {}, "_tasks" ] ],
+        qs    => {
+            actions             => "list",
+            detailed            => "boolean",
+            error_trace         => "boolean",
+            filter_path         => "list",
+            group_by            => "enum",
+            human               => "boolean",
+            nodes               => "list",
+            parent_task_id      => "string",
+            timeout             => "time",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'text_structure.find_structure' => {
+        body   => { required => 1 },
+        doc    => "find-structure",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_text_structure", "find_structure" ] ],
+        qs     => {
+            charset               => "string",
+            column_names          => "list",
+            delimiter             => "string",
+            ecs_compatibility     => "string",
+            error_trace           => "boolean",
+            explain               => "boolean",
+            filter_path           => "list",
+            format                => "enum",
+            grok_pattern          => "string",
+            has_header_row        => "boolean",
+            human                 => "boolean",
+            line_merge_size_limit => "int",
+            lines_to_sample       => "int",
+            quote                 => "string",
+            should_trim_fields    => "boolean",
+            timeout               => "time",
+            timestamp_field       => "string",
+            timestamp_format      => "string",
+        },
+        serialize => "bulk",
+    },
+
+    'transform.delete_transform' => {
+        doc    => "delete-transform",
+        method => "DELETE",
+        parts  => { transform_id => {} },
+        paths  =>
+            [ [ { transform_id => 1 }, "_transform", "{transform_id}" ] ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            force       => "boolean",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'transform.get_transform' => {
+        doc   => "get-transform",
+        parts => { transform_id => {} },
+        paths => [
+            [ { transform_id => 1 }, "_transform", "{transform_id}" ],
+            [ {}, "_transform" ],
+        ],
+        qs => {
+            allow_no_match    => "boolean",
+            error_trace       => "boolean",
+            exclude_generated => "boolean",
+            filter_path       => "list",
+            from              => "int",
+            human             => "boolean",
+            size              => "int",
+        },
+    },
+
+    'transform.get_transform_stats' => {
+        doc   => "get-transform-stats",
+        parts => { transform_id => {} },
+        paths => [
+            [   { transform_id => 1 }, "_transform",
+                "{transform_id}",      "_stats"
+            ],
+        ],
+        qs => {
+            allow_no_match => "boolean",
+            error_trace    => "boolean",
+            filter_path    => "list",
+            from           => "number",
+            human          => "boolean",
+            size           => "number",
+        },
+    },
+
+    'transform.preview_transform' => {
+        body  => {},
+        doc   => "preview-transform",
+        parts => { transform_id => {} },
+        paths => [
+            [   { transform_id => 1 }, "_transform",
+                "{transform_id}",      "_preview",
+            ],
+            [ {}, "_transform", "_preview" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'transform.put_transform' => {
+        body   => { required => 1 },
+        doc    => "put-transform",
+        method => "PUT",
+        parts  => { transform_id => {} },
+        paths  =>
+            [ [ { transform_id => 1 }, "_transform", "{transform_id}" ] ],
+        qs => {
+            defer_validation => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            timeout          => "time",
+        },
+    },
+
+    'transform.reset_transform' => {
+        doc    => "reset-transform",
+        method => "POST",
+        parts  => { transform_id => {} },
+        paths  => [
+            [   { transform_id => 1 }, "_transform",
+                "{transform_id}",      "_reset"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            force       => "boolean",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'transform.start_transform' => {
+        doc    => "start-transform",
+        method => "POST",
+        parts  => { transform_id => {} },
+        paths  => [
+            [   { transform_id => 1 }, "_transform",
+                "{transform_id}",      "_start"
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'transform.stop_transform' => {
+        doc    => "stop-transform",
+        method => "POST",
+        parts  => { transform_id => {} },
+        paths  => [
+            [   { transform_id => 1 }, "_transform", "{transform_id}",
+                "_stop"
+            ],
+        ],
+        qs => {
+            allow_no_match      => "boolean",
+            error_trace         => "boolean",
+            filter_path         => "list",
+            force               => "boolean",
+            human               => "boolean",
+            timeout             => "time",
+            wait_for_checkpoint => "boolean",
+            wait_for_completion => "boolean",
+        },
+    },
+
+    'transform.update_transform' => {
+        body   => { required => 1 },
+        doc    => "update-transform",
+        method => "POST",
+        parts  => { transform_id => { required => 1 } },
+        paths  => [
+            [   { transform_id => 1 }, "_transform",
+                "{transform_id}",      "_update",
+            ],
+        ],
+        qs => {
+            defer_validation => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+            timeout          => "time",
+        },
+    },
+
+    'transform.upgrade_transforms' => {
+        doc    => "upgrade-transforms",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_transform", "_upgrade" ] ],
+        qs     => {
+            dry_run     => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+            timeout     => "time",
+        },
+    },
+
+    'watcher.ack_watch' => {
+        doc    => "watcher-api-ack-watch",
+        method => "PUT",
+        parts  => { action_id => { multi => 1 }, watch_id => {} },
+        paths  => [
+            [   { action_id => 4, watch_id => 2 }, "_watcher",
+                "watch",                           "{watch_id}",
+                "_ack",                            "{action_id}",
+            ],
+            [ { watch_id => 2 }, "_watcher", "watch", "{watch_id}", "_ack" ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.activate_watch' => {
+        doc    => "watcher-api-activate-watch",
+        method => "PUT",
+        parts  => { watch_id => {} },
+        paths  => [
+            [   { watch_id => 2 }, "_watcher",
+                "watch",           "{watch_id}",
+                "_activate",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.deactivate_watch' => {
+        doc    => "watcher-api-deactivate-watch",
+        method => "PUT",
+        parts  => { watch_id => {} },
+        paths  => [
+            [   { watch_id => 2 }, "_watcher",
+                "watch",           "{watch_id}",
+                "_deactivate",
+            ],
+        ],
+        qs => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.delete_watch' => {
+        doc    => "watcher-api-delete-watch",
+        method => "DELETE",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_watcher", "watch", "{id}" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.execute_watch' => {
+        body   => {},
+        doc    => "watcher-api-execute-watch",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [
+            [ { id => 2 }, "_watcher", "watch", "{id}", "_execute" ],
+            [ {}, "_watcher", "watch", "_execute" ],
+        ],
+        qs => {
+            debug       => "boolean",
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean",
+        },
+    },
+
+    'watcher.get_watch' => {
+        doc   => "watcher-api-get-watch",
+        parts => { id => {} },
+        paths => [ [ { id => 2 }, "_watcher", "watch", "{id}" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.put_watch' => {
+        body   => {},
+        doc    => "watcher-api-put-watch",
+        method => "PUT",
+        parts  => { id => {} },
+        paths  => [ [ { id => 2 }, "_watcher", "watch", "{id}" ] ],
+        qs     => {
+            active          => "boolean",
+            error_trace     => "boolean",
+            filter_path     => "list",
+            human           => "boolean",
+            if_primary_term => "number",
+            if_seq_no       => "number",
+            version         => "number",
+        },
+    },
+
+    'watcher.query_watches' => {
+        body  => {},
+        doc   => "watcher-api-query-watches",
+        parts => {},
+        paths => [ [ {}, "_watcher", "_query", "watches" ] ],
+        qs    => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.start' => {
+        doc    => "watcher-api-start",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_watcher", "_start" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'watcher.stats' => {
+        doc   => "watcher-api-stats",
+        parts => { metric => { multi => 1 } },
+        paths => [
+            [ { metric => 2 }, "_watcher", "stats", "{metric}" ],
+            [ {}, "_watcher", "stats" ],
+        ],
+        qs => {
+            emit_stacktraces => "boolean",
+            error_trace      => "boolean",
+            filter_path      => "list",
+            human            => "boolean",
+        },
+    },
+
+    'watcher.stop' => {
+        doc    => "watcher-api-stop",
+        method => "POST",
+        parts  => {},
+        paths  => [ [ {}, "_watcher", "_stop" ] ],
+        qs     => {
+            error_trace => "boolean",
+            filter_path => "list",
+            human       => "boolean"
+        },
+    },
+
+    'xpack.info' => {
+        doc   => "info-api",
+        parts => {},
+        paths => [ [ {}, "_xpack" ] ],
+        qs    => {
+            accept_enterprise => "boolean",
+            categories        => "list",
+            error_trace       => "boolean",
+            filter_path       => "list",
+            human             => "boolean",
+        },
+    },
+
+    'xpack.usage' => {
+        doc   => "usage-api",
+        parts => {},
+        paths => [ [ {}, "_xpack", "usage" ] ],
+        qs    => {
+            error_trace    => "boolean",
+            filter_path    => "list",
+            human          => "boolean",
+            master_timeout => "time",
+        },
+    },
+
+#=== AUTOGEN - END ===
+
+);
+
+__PACKAGE__->_qs_init( \%API );
+1;
+
+__END__
+
+# ABSTRACT: This class contains the spec for the Elasticsearch APIs
+
+=head1 DESCRIPTION
+
+All of the Elasticsearch APIs are defined in this role. The example given below
+is the definition for the L<Search::Elasticsearch::Client::8_0::Direct/index()> method:
+
+    'index' => {
+        body   => { required => 1 },
+        doc    => "docs-index_",
+        method => "POST",
+        parts  => { id => {}, index => {}, type => {} },
+        paths  => [
+            [   { id => 2, index => 0, type => 1 }, "{index}",
+                "{type}",                           "{id}"
+            ],
+            [ { id    => 2, index => 0 }, "{index}", "_doc", "{id}" ],
+            [ { index => 0, type  => 1 }, "{index}", "{type}" ],
+            [ { index => 0 }, "{index}", "_doc" ],
+        ],
+        qs => {
+            error_trace            => "boolean",
+            filter_path            => "list",
+            human                  => "boolean",
+            if_primary_term        => "number",
+            if_seq_no              => "number",
+            op_type                => "enum",
+            pipeline               => "string",
+            refresh                => "enum",
+            require_alias          => "boolean",
+            routing                => "string",
+            timeout                => "time",
+            version                => "number",
+            version_type           => "enum",
+            wait_for_active_shards => "string",
+        },
+    }
+
+These definitions can be used by different L<Search::Elasticsearch::Role::Client>
+implementations to provide distinct user interfaces.
+
+=head1 METHODS
+
+=head2 C<api()>
+
+    $defn = $api->api($name);
+
+The only method in this class is the C<api()> method which takes the name
+of the I<action> and returns its definition.  Actions in the
+C<indices> or C<cluster> namespace use the namespace as a prefix, eg:
+
+    $defn = $e->api('indices.create');
+    $defn = $e->api('cluster.node_stats');
+
+=head1 SEE ALSO
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Role::API>
+
+=item *
+
+L<Search::Elasticsearch::Client::8_0::Direct>
+
+=back
diff --git a/lib/Search/Elasticsearch/Client/8_0/Role/Bulk.pm b/lib/Search/Elasticsearch/Client/8_0/Role/Bulk.pm
new file mode 100644
index 0000000..9b048b1
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Role/Bulk.pm
@@ -0,0 +1,280 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Role::Bulk;
+
+use Moo::Role;
+requires 'add_action', 'flush';
+
+use Search::Elasticsearch::Util qw(parse_params throw);
+use namespace::clean;
+
+has 'es'        => ( is => 'ro', required => 1 );
+has 'max_count' => ( is => 'rw', default  => 1_000 );
+has 'max_size'  => ( is => 'rw', default  => 1_000_000 );
+has 'max_time'  => ( is => 'rw', default  => 0 );
+
+has 'on_success'  => ( is => 'ro', default => 0 );
+has 'on_error'    => ( is => 'lazy' );
+has 'on_conflict' => ( is => 'ro', default => 0 );
+has 'verbose'     => ( is => 'rw' );
+
+has '_buffer' => ( is => 'ro', default => sub { [] } );
+has '_buffer_size'  => ( is => 'rw', default => 0 );
+has '_buffer_count' => ( is => 'rw', default => 0 );
+has '_serializer'   => ( is => 'lazy' );
+has '_bulk_args'    => ( is => 'ro' );
+has '_last_flush' => ( is => 'rw', default => sub {time} );
+has '_metadata_params' => ( is => 'ro' );
+has '_update_params'   => ( is => 'ro' );
+has '_required_params' => ( is => 'ro' );
+
+our %Actions = (
+    'index'  => 1,
+    'create' => 1,
+    'update' => 1,
+    'delete' => 1
+);
+
+#===================================
+sub _build__serializer { shift->es->transport->serializer }
+#===================================
+
+#===================================
+sub _build_on_error {
+#===================================
+    my $self       = shift;
+    my $serializer = $self->_serializer;
+    return sub {
+        my ( $action, $result, $src ) = @_;
+        warn( "Bulk error [$action]: " . $serializer->encode($result) );
+    };
+}
+
+#===================================
+sub BUILDARGS {
+#===================================
+    my ( $class, $params ) = parse_params(@_);
+    my $es = $params->{es} or throw( 'Param', 'Missing required param <es>' );
+    $params->{_metadata_params} = $es->api('bulk.metadata')->{params};
+    $params->{_update_params}   = $es->api('bulk.update')->{params};
+    $params->{_required_params} = $es->api('bulk.required')->{params};
+    my $bulk_spec = $es->api('bulk');
+    my %args;
+    for ( keys %{ $bulk_spec->{qs} }, keys %{ $bulk_spec->{parts} } ) {
+        $args{$_} = delete $params->{$_}
+            if exists $params->{$_};
+    }
+    $params->{_bulk_args} = \%args;
+    return $params;
+}
+
+#===================================
+sub index {
+#===================================
+    shift->add_action( map { ( 'index' => $_ ) } @_ );
+}
+
+#===================================
+sub create {
+#===================================
+    shift->add_action( map { ( 'create' => $_ ) } @_ );
+}
+
+#===================================
+sub delete {
+#===================================
+    shift->add_action( map { ( 'delete' => $_ ) } @_ );
+}
+
+#===================================
+sub update {
+#===================================
+    shift->add_action( map { ( 'update' => $_ ) } @_ );
+}
+
+#===================================
+sub create_docs {
+#===================================
+    my $self = shift;
+    $self->add_action( map { ( 'create' => { source => $_ } ) } @_ );
+}
+
+#===================================
+sub delete_ids {
+#===================================
+    my $self = shift;
+    $self->add_action( map { ( 'delete' => { _id => $_ } ) } @_ );
+}
+
+#===================================
+sub _encode_action {
+#===================================
+    my $self   = shift;
+    my $action = shift || '';
+    my $orig   = shift;
+
+    throw( 'Param', "Unrecognised action <$action>" )
+        unless $Actions{$action};
+
+    throw( 'Param', "Missing <params> for action <$action>" )
+        unless ref($orig) eq 'HASH';
+
+    my %metadata;
+    my $params     = {%$orig};
+    my $serializer = $self->_serializer;
+
+    my $meta_params = $self->_metadata_params;
+    for ( keys %$meta_params ) {
+        next unless exists $params->{$_};
+        $metadata{ $meta_params->{$_} } = delete $params->{$_};
+    }
+
+    for ( @{ $self->_required_params } ) {
+        throw( 'Param', "Missing required param <$_>" )
+            unless $metadata{"_$_"} || $self->_bulk_args->{$_};
+    }
+
+    my $source;
+    if ( $action eq 'update' ) {
+        for ( @{ $self->_update_params } ) {
+            $source->{$_} = delete $params->{$_}
+                if exists $params->{$_};
+        }
+    }
+    elsif ( $action ne 'delete' ) {
+        $source = delete $params->{source}
+            || throw( 'Param',
+            "Missing <source> for action <$action>: "
+                . $serializer->encode($orig) );
+    }
+
+    throw(    "Unknown params <"
+            . ( join ',', sort keys %$params )
+            . "> in <$action>: "
+            . $serializer->encode($orig) )
+        if keys %$params;
+
+    return map { $serializer->encode($_) }
+        grep {$_} ( { $action => \%metadata }, $source );
+}
+
+#===================================
+sub _report {
+#===================================
+    my ( $self, $buffer, $results ) = @_;
+    my $on_success  = $self->on_success;
+    my $on_error    = $self->on_error;
+    my $on_conflict = $self->on_conflict;
+
+    # assume errors if key not present, bwc
+    $results->{errors} = 1 unless exists $results->{errors};
+
+    return
+        unless $on_success
+        || ( $results->{errors} and $on_error || $on_conflict );
+
+    my $serializer = $self->_serializer;
+
+    my $j = 0;
+
+    for my $item ( @{ $results->{items} } ) {
+        my ( $action, $result ) = %$item;
+        my @args = ($action);
+        if ( my $error = $result->{error} ) {
+            if ($on_conflict) {
+                my ( $is_conflict, $version )
+                    = $self->_is_conflict_error($error);
+                if ($is_conflict) {
+                    $on_conflict->( $action, $result, $j, $version );
+                    next;
+                }
+            }
+            $on_error && $on_error->( $action, $result, $j );
+        }
+        else {
+            $on_success && $on_success->( $action, $result, $j );
+        }
+        $j++;
+    }
+}
+
+#===================================
+sub _is_conflict_error {
+#===================================
+    my ( $self, $error ) = @_;
+    my $version;
+    if ( ref($error) eq 'HASH' ) {
+        return 1 if $error->{type} eq 'document_already_exists_exception';
+        return unless $error->{type} eq 'version_conflict_engine_exception';
+        $error->{reason} =~ /version.conflict,.current.(?:version.)?\[(\d+)\]/;
+        return ( 1, $1 );
+    }
+    return unless $error =~ /
+            DocumentAlreadyExistsException
+           |version.conflict,.current.\[(\d+)\]
+           /x;
+    return ( 1, $1 );
+}
+
+#===================================
+sub clear_buffer {
+#===================================
+    my $self = shift;
+    @{ $self->_buffer } = ();
+    $self->_buffer_size(0);
+    $self->_buffer_count(0);
+}
+
+#===================================
+sub _doc_transformer {
+#===================================
+    my ( $self, $params ) = @_;
+
+    my $bulk_args = $self->_bulk_args;
+    my %allowed = map { $_ => 1, "_$_" => 1 }
+        ( @{ $self->_metadata_params }, 'source' );
+    $allowed{fields} = 1;
+
+    delete @allowed{ 'index', '_index' } if $bulk_args->{index};
+    delete @allowed{ 'type',  '_type' }  if $bulk_args->{type};
+
+    my $version_type = $params->{version_type};
+    my $transform    = $params->{transform};
+
+    return sub {
+        my %doc = %{ shift() };
+        for ( keys %doc ) {
+            delete $doc{$_} unless $allowed{$_};
+        }
+
+        if ( my $fields = delete $doc{fields} ) {
+            for (qw(_routing routing _parent parent)) {
+                $doc{$_} = $fields->{$_}
+                    if exists $fields->{$_};
+            }
+        }
+        $doc{_version_type} = $version_type if $version_type;
+
+        return \%doc unless $transform;
+        return $transform->( \%doc );
+    };
+}
+
+1;
+
+# ABSTRACT: Provides common functionality to L<Elasticseach::Client::8_0::Bulk> and L<Search::Elasticsearch::Client::8_0::Async::Bulk>
diff --git a/lib/Search/Elasticsearch/Client/8_0/Role/Scroll.pm b/lib/Search/Elasticsearch/Client/8_0/Role/Scroll.pm
new file mode 100644
index 0000000..c0cabd2
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Role/Scroll.pm
@@ -0,0 +1,63 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Role::Scroll;
+
+use Moo::Role;
+requires 'finish';
+use Search::Elasticsearch::Util qw(parse_params throw);
+use Devel::GlobalDestruction;
+use namespace::clean;
+has 'es' => ( is => 'ro', required => 1 );
+has 'scroll'        => ( is => 'ro' );
+has 'total'         => ( is => 'rwp' );
+has 'max_score'     => ( is => 'rwp' );
+has 'facets'        => ( is => 'rwp' );
+has 'aggregations'  => ( is => 'rwp' );
+has 'suggest'       => ( is => 'rwp' );
+has 'took'          => ( is => 'rwp' );
+has 'total_took'    => ( is => 'rwp' );
+has 'search_params' => ( is => 'ro' );
+has 'is_finished'   => ( is => 'rwp', default => '' );
+has '_pid'          => ( is => 'ro', default => sub {$$} );
+has '_scroll_id'    => ( is => 'rwp', clearer => 1, predicate => 1 );
+
+#===================================
+sub scroll_request {
+#===================================
+    my $self = shift;
+    throw( 'Illegal',
+              'Scroll requests are not fork safe and may only be '
+            . 'refilled by the same process that created the instance.' )
+        if $self->_pid != $$;
+
+    my %args = ( scroll => $self->scroll );
+    $args{body} = { scroll_id => $self->_scroll_id };
+    $self->es->scroll(%args);
+}
+
+#===================================
+sub DEMOLISH {
+#===================================
+    my $self = shift or return;
+    return if in_global_destruction;
+    $self->finish;
+}
+
+1;
+
+# ABSTRACT: Provides common functionality to L<Search::Elasticsearch::Client::8_0::Scroll> and L<Search::Elasticsearch::Client::8_0::Async::Scroll>
diff --git a/lib/Search/Elasticsearch/Client/8_0/Scroll.pm b/lib/Search/Elasticsearch/Client/8_0/Scroll.pm
new file mode 100644
index 0000000..525e52d
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/Scroll.pm
@@ -0,0 +1,368 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::Scroll;
+
+use Moo;
+use Search::Elasticsearch::Util qw(parse_params throw);
+use namespace::clean;
+
+has '_buffer' => ( is => 'ro' );
+
+with 'Search::Elasticsearch::Role::Is_Sync',
+    'Search::Elasticsearch::Client::8_0::Role::Scroll';
+
+#===================================
+sub BUILDARGS {
+#===================================
+    my ( $class, $params ) = parse_params(@_);
+    my $es = delete $params->{es};
+    my $scroll = $params->{scroll} ||= '1m';
+
+    my $results      = $es->search($params);
+
+    my $total = $results->{hits}{total};
+    if (ref $total) {
+        $total = $total->{value}
+    }
+
+    return {
+        es           => $es,
+        scroll       => $scroll,
+        aggregations => $results->{aggregations},
+        facets       => $results->{facets},
+        suggest      => $results->{suggest},
+        took         => $results->{took},
+        total_took   => $results->{took},
+        total        => $total,
+        max_score    => $results->{hits}{max_score},
+        _buffer      => $results->{hits}{hits},
+        $total
+        ? ( _scroll_id => $results->{_scroll_id} )
+        : ( is_finished => 1 )
+    };
+}
+
+#===================================
+sub next {
+#===================================
+    my ( $self, $n ) = @_;
+    $n ||= 1;
+    while ( $self->_has_scroll_id and $self->buffer_size < $n ) {
+        $self->refill_buffer;
+    }
+    my @return = splice( @{ $self->_buffer }, 0, $n );
+    $self->finish if @return < $n;
+    return wantarray ? @return : $return[-1];
+}
+
+#===================================
+sub drain_buffer {
+#===================================
+    my $self = shift;
+    return splice( @{ $self->_buffer } );
+}
+
+#===================================
+sub buffer_size { 0 + @{ shift->_buffer } }
+#===================================
+
+#===================================
+sub refill_buffer {
+#===================================
+    my $self = shift;
+
+    return 0 if $self->is_finished;
+
+    my $buffer    = $self->_buffer;
+    my $scroll_id = $self->_scroll_id
+        || return 0 + @$buffer;
+
+    my $results = $self->scroll_request;
+
+    my $hits = $results->{hits}{hits};
+    $self->_set_total_took( $self->total_took + $results->{took} );
+
+    if ( @$hits == 0 ) {
+        $self->_clear_scroll_id;
+    }
+    else {
+        $self->_set__scroll_id( $results->{_scroll_id} );
+        push @$buffer, @$hits;
+    }
+    $self->finish if @$buffer == 0;
+    return 0 + @$buffer;
+}
+
+#===================================
+sub finish {
+#===================================
+    my $self = shift;
+    return if $self->is_finished || $self->_pid != $$;
+
+    $self->_set_is_finished(1);
+    @{ $self->_buffer } = ();
+
+    my $scroll_id = $self->_scroll_id or return;
+    $self->_clear_scroll_id;
+
+    my %args = ( body => { scroll_id => $scroll_id } );
+    eval { $self->es->clear_scroll(%args) };
+    return 1;
+}
+
+1;
+
+__END__
+
+# ABSTRACT: A helper module for scrolled searches
+
+=head1 SYNOPSIS
+
+    use Search::Elasticsearch;
+
+    my $es     = Search::Elasticsearch->new;
+
+    my $scroll = $es->scroll_helper(
+        index       => 'my_index',
+        body => {
+            query   => {...},
+            size    => 1000,
+            sort    => '_doc'
+        }
+    );
+
+    say "Total hits: ". $scroll->total;
+
+    while (my $doc = $scroll->next) {
+        # do something
+    }
+
+=head1 DESCRIPTION
+
+A I<scrolled search> is a search that allows you to keep pulling results
+until there are no more matching results, much like a cursor in an SQL
+database.
+
+Unlike paginating through results (with the C<from> parameter in
+L<search()|Search::Elasticsearch::Client::8_0::Direct/search()>),
+scrolled searches take a snapshot of the current state of the index. Even
+if you keep adding new documents to the index or updating existing documents,
+a scrolled search will only see the index as it was when the search began.
+
+This module is a helper utility that wraps the functionality of the
+L<search()|Search::Elasticsearch::Client::8_0::Direct/search()> and
+L<scroll()|Search::Elasticsearch::Client::8_0::Direct/scroll()> methods to make
+them easier to use.
+
+This class does L<Search::Elasticsearch::Client::8_0::Role::Scroll> and
+L<Search::Elasticsearch::Role::Is_Sync>.
+
+
+=head1 USE CASES
+
+There are two primary use cases:
+
+=head2 Pulling enough results
+
+Perhaps you want to group your results by some field, and you don't know
+exactly how many results you will need in order to return 10 grouped
+results.  With a scrolled search you can keep pulling more results
+until you have enough.  For instance, you can search emails in a mailing
+list, and return results grouped by C<thread_id>:
+
+    my (%groups,@results);
+
+    my $scroll = $es->scroll_helper(
+        index => 'my_emails',
+        type  => 'email',
+        body  => { query => {... some query ... }}
+    );
+
+    my $doc;
+    while (@results < 10 and $doc = $scroll->next) {
+
+        my $thread = $doc->{_source}{thread_id};
+
+        unless ($groups{$thread}) {
+            $groups{$thread} = [];
+            push @results, $groups{$thread};
+        }
+        push @{$groups{$thread}},$doc;
+
+    }
+
+
+=head2 Extracting all documents
+
+Often you will want to extract all (or a subset of) documents in an index.
+If you want to change your type mappings, you will need to reindex all of your
+data. Or perhaps you want to move a subset of the data in one index into
+a new dedicated index. In these cases, you don't care about sort
+order, you just want to retrieve all documents which match a query, and do
+something with them. For instance, to retrieve all the docs for a particular
+C<client_id>:
+
+    my $scroll = $es->scroll_helper(
+        index       => 'my_index',
+        size        => 1000,
+        body        => {
+            query => {
+                match => {
+                    client_id => 123
+                }
+            },
+            sort => '_doc'
+        }
+    );
+
+    while (my $doc = $scroll->next) {
+        # do something
+    }
+
+Very often the I<something> that you will want to do with these results
+involves bulk-indexing them into a new index. The easiest way to
+do this is to use the built-in L<Search::Elasticsearch::Client::8_0::Direct/reindex()>
+functionality provided by Elasticsearch.
+
+=head1 METHODS
+
+=head2 C<new()>
+
+    use Search::Elasticsearch;
+
+    my $es = Search::Elasticsearch->new(...);
+    my $scroll = $es->scroll_helper(
+        scroll         => '1m',            # optional
+        %search_params
+    );
+
+The L<Search::Elasticsearch::Client::8_0::Direct/scroll_helper()> method loads
+L<Search::Elasticsearch::Client::8_0::Scroll> class and calls L</new()>,
+passing in any arguments.
+
+You can specify a C<scroll> duration (which defaults to C<"1m">).
+Any other parameters are passed directly to L<Search::Elasticsearch::Client::8_0::Direct/search()>.
+
+The C<scroll> duration tells Elasticearch how long it should keep the scroll
+alive.  B<Note>: this duration doesn't need to be long enough to process
+all results, just long enough to process a single B<batch> of results.
+The expiry gets renewed for another C<scroll> period every time new
+a new batch of results is retrieved from the cluster.
+
+By default, the C<scroll_id> is passed as the C<body> to the
+L<scroll|Search::Elasticsearch::Client::8_0::Direct/scroll()> request.
+
+The C<scroll> request uses C<GET> by default.  To use C<POST> instead,
+set L<send_get_body_as|Search::Elasticsearch::Transport/send_get_body_as> to
+C<POST>.
+
+=head2 C<next()>
+
+    $doc  = $scroll->next;
+    @docs = $scroll->next($num);
+
+The C<next()> method returns the next result, or the next C<$num> results
+(pulling more results if required).  If all results have been exhausted,
+it returns an empty list.
+
+=head2 C<drain_buffer()>
+
+    @docs = $scroll->drain_buffer;
+
+The C<drain_buffer()> method returns all of the documents currently in the
+buffer, without fetching any more from the cluster.
+
+=head2 C<refill_buffer()>
+
+    $total = $scroll->refill_buffer;
+
+The C<refill_buffer()> method fetches the next batch of results from the
+cluster, stores them in the buffer, and returns the total number of docs
+currently in the buffer.
+
+=head2 C<buffer_size()>
+
+    $total = $scroll->buffer_size;
+
+The C<buffer_size()> method returns the total number of docs currently in
+the buffer.
+
+=head2 C<finish()>
+
+    $scroll->finish;
+
+The C<finish()> method clears out the buffer, sets L</is_finished()> to C<true>
+and tries to clear the C<scroll_id> on Elasticsearch.  This API is only
+supported since v0.90.6, but the call to C<clear_scroll> is wrapped in an
+C<eval> so the C<finish()> method can be safely called with any version
+of Elasticsearch.
+
+When the C<$scroll> instance goes out of scope, L</finish()> is called
+automatically if required.
+
+=head2 C<is_finished()>
+
+    $bool = $scroll->is_finished;
+
+A flag which returns C<true> if all results have been processed or
+L</finish()> has been called.
+
+=head1 INFO ACCESSORS
+
+The information from the original search is returned via the following
+accessors:
+
+=head2 C<total>
+
+The total number of documents that matched your query.
+
+=head2 C<max_score>
+
+The maximum score of any documents in your query.
+
+=head2 C<aggregations>
+
+Any aggregations that were specified, or C<undef>
+
+=head2 C<facets>
+
+Any facets that were specified, or C<undef>
+
+=head2 C<suggest>
+
+Any suggestions that were specified, or C<undef>
+
+=head2 C<took>
+
+How long the original search took, in milliseconds
+
+=head2 C<took_total>
+
+How long the original search plus all subsequent batches took, in milliseconds.
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Client::8_0::Direct/search()>
+
+=item * L<Search::Elasticsearch::Client::8_0::Direct/scroll()>
+
+=item * L<Search::Elasticsearch::Client::8_0::Direct/reindex()>
+
+=back
diff --git a/lib/Search/Elasticsearch/Client/8_0/TestServer.pm b/lib/Search/Elasticsearch/Client/8_0/TestServer.pm
new file mode 100644
index 0000000..87c68a4
--- /dev/null
+++ b/lib/Search/Elasticsearch/Client/8_0/TestServer.pm
@@ -0,0 +1,47 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Client::8_0::TestServer;
+
+use strict;
+use warnings;
+
+#===================================
+sub command_line {
+#===================================
+    my ( $class, $ts, $pid_file, $dir, $transport, $http ) = @_;
+
+    return (
+        $ts->es_home . '/bin/elasticsearch',
+        '-p',
+        $pid_file->filename,
+        map {"-E$_"} (
+            'path.data=' . $dir,
+            'network.host=127.0.0.1',
+            'cluster.name=es_test',
+            'discovery.zen.ping_timeout=1s',
+            'discovery.zen.ping.unicast.hosts=127.0.0.1:' . $ts->es_port,
+            'transport.tcp.port=' . $transport,
+            'http.port=' . $http,
+            @{ $ts->conf }
+        )
+    );
+}
+
+1
+
+# ABSTRACT: Client-specific backend for Search::Elasticsearch::TestServer
diff --git a/lib/Search/Elasticsearch/Cxn/AEHTTP.pm b/lib/Search/Elasticsearch/Cxn/AEHTTP.pm
new file mode 100644
index 0000000..16ae98e
--- /dev/null
+++ b/lib/Search/Elasticsearch/Cxn/AEHTTP.pm
@@ -0,0 +1,280 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Cxn::AEHTTP;
+
+use AnyEvent::HTTP qw(http_request);
+use Promises qw(deferred);
+use Try::Tiny;
+use Moo;
+
+with 'Search::Elasticsearch::Role::Cxn::Async';
+with 'Search::Elasticsearch::Role::Cxn',
+    'Search::Elasticsearch::Role::Is_Async';
+
+has '_tls_ctx' => ( is => 'lazy' );
+
+use namespace::clean;
+
+#===================================
+sub _build__tls_ctx {
+#===================================
+    my $self = shift;
+    return 'low' unless $self->has_ssl_options;
+    require AnyEvent::TLS;
+    return AnyEvent::TLS->new( %{ $self->ssl_options } );
+}
+
+#===================================
+sub perform_request {
+#===================================
+    my ( $self, $params ) = @_;
+    my $uri     = $self->build_uri($params);
+    my $method  = $params->{method};
+    my %headers = ( %{ $self->default_headers } );
+    my $data    = $params->{data};
+    if ( defined $data ) {
+        $headers{'Content-Type'}     = $params->{mime_type};
+        $headers{'Content-Encoding'} = $params->{encoding}
+            if $params->{encoding};
+    }
+
+    my $deferred = deferred;
+
+    http_request(
+        $method => $uri,
+        headers => \%headers,
+        timeout => $params->{timeout} || $self->request_timeout,
+        body => $data,
+        persistent => 0,
+        session    => $self->_pid,
+        ( %{ $self->handle_args } ),
+        ( $self->is_https ? ( tls_ctx => $self->_tls_ctx ) : () ),
+        sub {
+            my ( $body, $headers ) = @_;
+            try {
+                my ( $code, $response ) = $self->process_response(
+                    $params,                      # request
+                    delete $headers->{Status},    # code
+                    delete $headers->{Reason},    # msg
+                    $body,                        # body
+                    $headers                      # headers
+                );
+                $deferred->resolve( $code, $response );
+            }
+            catch {
+                $deferred->reject($_);
+            }
+
+        }
+    );
+    $deferred->promise;
+}
+
+#===================================
+sub error_from_text {
+#===================================
+    local $_ = $_[2];
+    return
+          /[Tt]imed out/              ? 'Timeout'
+        : /certificate verify failed/ ? 'SSL'
+        : /Invalid argument/          ? 'Cxn'
+        :                               'Request';
+}
+
+1;
+
+# ABSTRACT: An async Cxn implementation which uses AnyEvent::HTTP
+
+=head1 DESCRIPTION
+
+Provides the default async HTTP Cxn class and is based on L<AnyEvent::HTTP>.
+The AEHTTP backend is fast, uses pure Perl, support proxies and https
+and provides persistent connections.
+
+This class does L<Search::Elasticsearch::Role::Cxn>, whose documentation
+provides more information, L<Search::Elasticsearch::Role::Async::Cxn>,
+and L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 CONFIGURATION
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<node|Search::Elasticsearch::Role::Cxn/"node">
+
+=item * L<max_content_length|Search::Elasticsearch::Role::Cxn/"max_content_length">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"gzip">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"deflate">
+
+=item * L<request_timeout|Search::Elasticsearch::Role::Cxn/"request_timeout">
+
+=item * L<ping_timeout|Search::Elasticsearch::Role::Cxn/"ping_timeout">
+
+=item * L<dead_timeout|Search::Elasticsearch::Role::Cxn/"dead_timeout">
+
+=item * L<max_dead_timeout|Search::Elasticsearch::Role::Cxn/"max_dead_timeout">
+
+=item * L<sniff_request_timeout|Search::Elasticsearch::Role::Cxn/"sniff_request_timeout">
+
+=item * L<sniff_timeout|Search::Elasticsearch::Role::Cxn/"sniff_timeout">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"handle_args">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"default_qs_params">
+
+=back
+
+=head1 SSL/TLS
+
+L<Search::Elasticsearch::Cxn::AEHTTP> uses L<AnyEvent::TLS> to support
+HTTPS.  By default, no validation of the remote host is performed.
+
+This behaviour can be changed by passing the C<ssl_options> parameter
+with any options accepted by L<AnyEvent::TLS>. For instance, to check
+that the remote host has a trusted certificate, and to avoid man-in-the-middle
+attacks, you could do the following:
+
+    use Search::Elasticsearch::Async;
+
+    my $es = Search::Elasticsearch::Async->new(
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            verify              => 1,
+            verify_peername     => 'https'
+            ca_file             => '/path/to/cacert.pem'
+        }
+    );
+
+If the remote server cannot be verified, an
+L<Search::Elasticsearch::Error|SSL error> will be thrown.
+
+If you want your client to present its own certificate to the remote
+server, then use:
+
+    use Search::Elasticsearch::Async;
+
+    my $es = Search::Elasticsearch::Async->new(
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            verify              => 1,
+            verify_peername     => 'https'
+            ca_file             => '/path/to/cacert.pem'
+            cert_file           => '/path/to/client.pem',
+        }
+    );
+
+
+=head1 METHODS
+
+=head2 C<perform_request()>
+
+    $self->perform_request({
+        # required
+        method      => 'GET|HEAD|POST|PUT|DELETE',
+        path        => '/path/of/request',
+        qs          => \%query_string_params,
+
+        # optional
+        data        => $body_as_string,
+        mime_type   => 'application/json',
+        timeout     => $timeout
+    })
+    ->then(sub { my ($status,body) = @_; ...})
+
+Sends the request to the associated Elasticsearch node and returns
+a C<$status> code and the decoded response C<$body>, or throws an
+error if the request failed.
+
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<scheme()|Search::Elasticsearch::Role::Cxn/"scheme()">
+
+=item * L<is_https()|Search::Elasticsearch::Role::Cxn/"is_https()">
+
+=item * L<userinfo()|Search::Elasticsearch::Role::Cxn/"userinfo()">
+
+=item * L<default_headers()|Search::Elasticsearch::Role::Cxn/"default_headers()">
+
+=item * L<max_content_length()|Search::Elasticsearch::Role::Cxn/"max_content_length()">
+
+=item * L<build_uri()|Search::Elasticsearch::Role::Cxn/"build_uri()">
+
+=item * L<host()|Search::Elasticsearch::Role::Cxn/"host()">
+
+=item * L<port()|Search::Elasticsearch::Role::Cxn/"port()">
+
+=item * L<uri()|Search::Elasticsearch::Role::Cxn/"uri()">
+
+=item * L<is_dead()|Search::Elasticsearch::Role::Cxn/"is_dead()">
+
+=item * L<is_live()|Search::Elasticsearch::Role::Cxn/"is_live()">
+
+=item * L<next_ping()|Search::Elasticsearch::Role::Cxn/"next_ping()">
+
+=item * L<ping_failures()|Search::Elasticsearch::Role::Cxn/"ping_failures()">
+
+=item * L<mark_dead()|Search::Elasticsearch::Role::Cxn/"mark_dead()">
+
+=item * L<mark_live()|Search::Elasticsearch::Role::Cxn/"mark_live()">
+
+=item * L<force_ping()|Search::Elasticsearch::Role::Cxn/"force_ping()">
+
+=item * L<pings_ok()|Search::Elasticsearch::Role::Cxn/"pings_ok()">
+
+=item * L<sniff()|Search::Elasticsearch::Role::Cxn/"sniff()">
+
+=item * L<process_response()|Search::Elasticsearch::Role::Cxn/"process_response()">
+
+=back
+
+From L<Search::Elasticsearch::Role::Async::Cxn>
+
+=over
+
+=item * L<pings_ok()|Search::Elasticsearch::Role::Async::Cxn/"pings_ok()">
+
+=item * L<sniff()|Search::Elasticsearch::Role::Async::Cxn/"sniff()">
+
+=back
+
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Role::Cxn::Mojo>
+
+=back
+
+
diff --git a/lib/Search/Elasticsearch/Cxn/Factory.pm b/lib/Search/Elasticsearch/Cxn/Factory.pm
new file mode 100644
index 0000000..39c6772
--- /dev/null
+++ b/lib/Search/Elasticsearch/Cxn/Factory.pm
@@ -0,0 +1,68 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Cxn::Factory;
+
+use Moo;
+use Search::Elasticsearch::Util qw(parse_params load_plugin);
+use namespace::clean;
+
+has 'cxn_class'          => ( is => 'ro', required => 1 );
+has '_factory'           => ( is => 'ro', required => 1 );
+has 'default_host'       => ( is => 'ro', default  => 'http://localhost:9200' );
+has 'max_content_length' => ( is => 'rw', default  => 104_857_600 );
+
+#===================================
+sub BUILDARGS {
+#===================================
+    my ( $class, $params ) = parse_params(@_);
+    my %args = (%$params);
+    delete $args{nodes};
+
+    my $cxn_class
+        = load_plugin( 'Search::Elasticsearch::Cxn', delete $args{cxn} );
+    $params->{_factory} = sub {
+        my ( $self, $node ) = @_;
+        $cxn_class->new(
+            %args,
+            node               => $node,
+            max_content_length => $self->max_content_length
+        );
+    };
+    $params->{cxn_args}  = \%args;
+    $params->{cxn_class} = $cxn_class;
+    return $params;
+}
+
+#===================================
+sub new_cxn { shift->_factory->(@_) }
+#===================================
+
+1;
+
+__END__
+
+# ABSTRACT: Used by CxnPools to create new Cxn instances.
+
+=head1 DESCRIPTION
+
+This class is used by the L<Search::Elasticsearch::Role::CxnPool> implementations
+to create new L<Search::Elasticsearch::Role::Cxn>-based instances. It holds on
+to all the configuration options passed to L<Elasticsearch/new()> so
+that new Cxns can use them.
+
+It contains no user serviceable parts.
diff --git a/lib/Search/Elasticsearch/Cxn/HTTPTiny.pm b/lib/Search/Elasticsearch/Cxn/HTTPTiny.pm
new file mode 100644
index 0000000..076d0e6
--- /dev/null
+++ b/lib/Search/Elasticsearch/Cxn/HTTPTiny.pm
@@ -0,0 +1,263 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Cxn::HTTPTiny;
+
+use Moo;
+with 'Search::Elasticsearch::Role::Cxn', 'Search::Elasticsearch::Role::Is_Sync';
+
+use HTTP::Tiny 0.076 ();
+use namespace::clean;
+
+my $Cxn_Error = qr/ Connection.(?:timed.out|re(?:set|fused))
+                       | connect:.timeout
+                       | Host.is.down
+                       | No.route.to.host
+                       | temporarily.unavailable
+                       /x;
+
+#===================================
+sub perform_request {
+#===================================
+    my ( $self, $params ) = @_;
+    my $uri    = $self->build_uri($params);
+    my $method = $params->{method};
+
+    my %args;
+    if ( defined $params->{data} ) {
+        $args{content}                     = $params->{data};
+        $args{headers}{'Content-Type'}     = $params->{mime_type};
+        $args{headers}{'Content-Encoding'} = $params->{encoding}
+            if $params->{encoding};
+    }
+
+    my $handle = $self->handle;
+    $handle->timeout( $params->{timeout} || $self->request_timeout );
+
+    my $response = $handle->request( $method, "$uri", \%args );
+
+    return $self->process_response(
+        $params,                 # request
+        $response->{status},     # code
+        $response->{reason},     # msg
+        $response->{content},    # body
+        $response->{headers}     # headers
+    );
+}
+
+#===================================
+sub error_from_text {
+#===================================
+    local $_ = $_[2];
+    return
+          /[Tt]imed out/             ? 'Timeout'
+        : /Unexpected end of stream/ ? 'ContentLength'
+        : /SSL connection failed/    ? 'SSL'
+        : /$Cxn_Error/               ? 'Cxn'
+        :                              'Request';
+}
+
+#===================================
+sub _build_handle {
+#===================================
+    my $self = shift;
+    my %args = ( default_headers => $self->default_headers );
+    if ( $self->is_https && $self->has_ssl_options ) {
+        $args{SSL_options} = $self->ssl_options;
+        if ( $args{SSL_options}{SSL_verify_mode} ) {
+            $args{verify_ssl} = 1;
+        }
+    }
+
+    return HTTP::Tiny->new( %args, %{ $self->handle_args } );
+}
+
+1;
+
+# ABSTRACT: A Cxn implementation which uses HTTP::Tiny
+
+=head1 DESCRIPTION
+
+Provides the default HTTP Cxn class and is based on L<HTTP::Tiny>.
+The HTTP::Tiny backend is fast, uses pure Perl, support proxies and https
+and provides persistent connections.
+
+This class does L<Search::Elasticsearch::Role::Cxn>, whose documentation
+provides more information, and L<Search::Elasticsearch::Role::Is_Sync>.
+
+=head1 CONFIGURATION
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<node|Search::Elasticsearch::Role::Cxn/"node">
+
+=item * L<max_content_length|Search::Elasticsearch::Role::Cxn/"max_content_length">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"gzip">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"deflate">
+
+=item * L<request_timeout|Search::Elasticsearch::Role::Cxn/"request_timeout">
+
+=item * L<ping_timeout|Search::Elasticsearch::Role::Cxn/"ping_timeout">
+
+=item * L<dead_timeout|Search::Elasticsearch::Role::Cxn/"dead_timeout">
+
+=item * L<max_dead_timeout|Search::Elasticsearch::Role::Cxn/"max_dead_timeout">
+
+=item * L<sniff_request_timeout|Search::Elasticsearch::Role::Cxn/"sniff_request_timeout">
+
+=item * L<sniff_timeout|Search::Elasticsearch::Role::Cxn/"sniff_timeout">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"handle_args">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"default_qs_params">
+
+=back
+
+=head1 SSL/TLS
+
+L<Search::Elasticsearch::Cxn::HTTPTiny> uses L<IO::Socket::SSL> to support
+HTTPS.  By default, no validation of the remote host is performed.
+
+This behaviour can be changed by passing the C<ssl_options> parameter
+with any options accepted by L<IO::Socket::SSL>. For instance, to check
+that the remote host has a trusted certificate, and to avoid man-in-the-middle
+attacks, you could do the following:
+
+    use Search::Elasticsearch;
+    use IO::Socket::SSL;
+
+    my $es = Search::Elasticsearch->new(
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            SSL_verify_mode     => SSL_VERIFY_PEER,
+            SSL_ca_file         => '/path/to/cacert.pem'
+        }
+    );
+
+If the remote server cannot be verified, an
+L<Search::Elasticsearch::Error|SSL error> will be thrown.
+
+If you want your client to present its own certificate to the remote
+server, then use:
+
+    use Search::Elasticsearch;
+    use IO::Socket::SSL;
+
+    my $es = Search::Elasticsearch->new(
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            SSL_verify_mode     => SSL_VERIFY_PEER,
+            SSL_use_cert        => 1,
+            SSL_ca_file         => '/path/to/cacert.pem',
+            SSL_cert_file       => '/path/to/client.pem',
+            SSL_key_file        => '/path/to/client.pem',
+        }
+    );
+
+
+=head1 METHODS
+
+=head2 C<perform_request()>
+
+    ($status,$body) = $self->perform_request({
+        # required
+        method      => 'GET|HEAD|POST|PUT|DELETE',
+        path        => '/path/of/request',
+        qs          => \%query_string_params,
+
+        # optional
+        data        => $body_as_string,
+        mime_type   => 'application/json',
+        timeout     => $timeout
+    });
+
+Sends the request to the associated Elasticsearch node and returns
+a C<$status> code and the decoded response C<$body>, or throws an
+error if the request failed.
+
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<scheme()|Search::Elasticsearch::Role::Cxn/"scheme()">
+
+=item * L<is_https()|Search::Elasticsearch::Role::Cxn/"is_https()">
+
+=item * L<userinfo()|Search::Elasticsearch::Role::Cxn/"userinfo()">
+
+=item * L<default_headers()|Search::Elasticsearch::Role::Cxn/"default_headers()">
+
+=item * L<max_content_length()|Search::Elasticsearch::Role::Cxn/"max_content_length()">
+
+=item * L<build_uri()|Search::Elasticsearch::Role::Cxn/"build_uri()">
+
+=item * L<host()|Search::Elasticsearch::Role::Cxn/"host()">
+
+=item * L<port()|Search::Elasticsearch::Role::Cxn/"port()">
+
+=item * L<uri()|Search::Elasticsearch::Role::Cxn/"uri()">
+
+=item * L<is_dead()|Search::Elasticsearch::Role::Cxn/"is_dead()">
+
+=item * L<is_live()|Search::Elasticsearch::Role::Cxn/"is_live()">
+
+=item * L<next_ping()|Search::Elasticsearch::Role::Cxn/"next_ping()">
+
+=item * L<ping_failures()|Search::Elasticsearch::Role::Cxn/"ping_failures()">
+
+=item * L<mark_dead()|Search::Elasticsearch::Role::Cxn/"mark_dead()">
+
+=item * L<mark_live()|Search::Elasticsearch::Role::Cxn/"mark_live()">
+
+=item * L<force_ping()|Search::Elasticsearch::Role::Cxn/"force_ping()">
+
+=item * L<pings_ok()|Search::Elasticsearch::Role::Cxn/"pings_ok()">
+
+=item * L<sniff()|Search::Elasticsearch::Role::Cxn/"sniff()">
+
+=item * L<process_response()|Search::Elasticsearch::Role::Cxn/"process_response()">
+
+=back
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Role::Cxn>
+
+=item * L<Search::Elasticsearch::Cxn::LWP>
+
+=item * L<Search::Elasticsearch::Cxn::NetCurl>
+
+=back
+
+
diff --git a/lib/Search/Elasticsearch/Cxn/LWP.pm b/lib/Search/Elasticsearch/Cxn/LWP.pm
new file mode 100644
index 0000000..61c68f8
--- /dev/null
+++ b/lib/Search/Elasticsearch/Cxn/LWP.pm
@@ -0,0 +1,274 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Cxn::LWP;
+
+use Moo;
+with 'Search::Elasticsearch::Role::Cxn', 'Search::Elasticsearch::Role::Is_Sync';
+
+use LWP::UserAgent();
+use HTTP::Headers();
+use HTTP::Request();
+
+my $Cxn_Error = qr/
+            Can't.connect
+          | Server.closed.connection
+          | Connection.refused
+            /x;
+
+use namespace::clean;
+
+#===================================
+sub perform_request {
+#===================================
+    my ( $self, $params ) = @_;
+    my $uri    = $self->build_uri($params);
+    my $method = $params->{method};
+
+    my %headers;
+    if ( $params->{data} ) {
+        $headers{'Content-Type'}     = $params->{mime_type};
+        $headers{'Content-Encoding'} = $params->{encoding}
+            if $params->{encoding};
+    }
+    my $request = HTTP::Request->new(
+        $method => $uri,
+        [ %headers, %{ $self->default_headers }, ],
+        $params->{data}
+    );
+    my $ua = $self->handle;
+    my $timeout = $params->{timeout} || $self->request_timeout;
+    if ( $timeout ne $ua->timeout ) {
+        $ua->conn_cache->drop;
+        $ua->timeout($timeout);
+    }
+    my $response = $ua->request($request);
+
+    return $self->process_response(
+        $params,               # request
+        $response->code,       # code
+        $response->message,    # msg
+        $response->content,    # body
+        $response->headers     # headers
+    );
+}
+
+#===================================
+sub error_from_text {
+#===================================
+    local $_ = $_[2];
+
+    return
+          /read timeout/                           ? 'Timeout'
+        : /write failed: Connection reset by peer/ ? 'ContentLength'
+        : /$Cxn_Error/                             ? 'Cxn'
+        :                                            'Request';
+}
+
+#===================================
+sub _build_handle {
+#===================================
+    my $self = shift;
+    my %args = (
+        keep_alive => 1,
+        parse_head => 0
+    );
+    if ( $self->is_https ) {
+        $args{ssl_opts}
+            = $self->has_ssl_options
+            ? $self->ssl_options
+            : { verify_hostname => 0, SSL_verify_mode => 0x00 };
+    }
+    return LWP::UserAgent->new( %args, %{ $self->handle_args } );
+}
+
+1;
+
+# ABSTRACT: A Cxn implementation which uses LWP
+
+=head1 DESCRIPTION
+
+Provides an HTTP Cxn class and based on L<LWP>.
+The LWP backend uses pure Perl and persistent connections.
+
+This class does L<Search::Elasticsearch::Role::Cxn>, whose documentation
+provides more information, and L<Search::Elasticsearch::Role::Is_Sync>.
+
+=head1 CONFIGURATION
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<node|Search::Elasticsearch::Role::Cxn/"node">
+
+=item * L<max_content_length|Search::Elasticsearch::Role::Cxn/"max_content_length">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"gzip">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"deflate">
+
+=item * L<request_timeout|Search::Elasticsearch::Role::Cxn/"request_timeout">
+
+=item * L<ping_timeout|Search::Elasticsearch::Role::Cxn/"ping_timeout">
+
+=item * L<dead_timeout|Search::Elasticsearch::Role::Cxn/"dead_timeout">
+
+=item * L<max_dead_timeout|Search::Elasticsearch::Role::Cxn/"max_dead_timeout">
+
+=item * L<sniff_request_timeout|Search::Elasticsearch::Role::Cxn/"sniff_request_timeout">
+
+=item * L<sniff_timeout|Search::Elasticsearch::Role::Cxn/"sniff_timeout">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"handle_args">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"default_qs_params">
+
+=back
+
+=head1 SSL/TLS
+
+L<Search::Elasticsearch::Cxn::LWP> uses L<IO::Socket::SSL> to support
+HTTPS.  By default, no validation of the remote host is performed.
+
+This behaviour can be changed by passing the C<ssl_options> parameter
+with any options accepted by L<IO::Socket::SSL>. For instance, to check
+that the remote host has a trusted certificate, and to avoid man-in-the-middle
+attacks, you could do the following:
+
+    use Search::Elasticsearch;
+
+    my $es = Search::Elasticsearch->new(
+        cxn   => 'LWP',
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            verify_hostname     => 1,
+            SSL_ca_file         => '/path/to/cacert.pem'
+        }
+    );
+
+If the remote server cannot be verified, an
+L<Search::Elasticsearch::Error|Cxn error> will be thrown - LWP does not
+allow us to detect that the connection error was due to invalid SSL.
+
+If you want your client to present its own certificate to the remote
+server, then use:
+
+    use Search::Elasticsearch;
+
+    my $es = Search::Elasticsearch->new(
+        cxn   => 'LWP',
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            verify_hostname     => 1,
+            SSL_ca_file         => '/path/to/cacert.pem',
+            SSL_use_cert        => 1,
+            SSL_cert_file       => '/path/to/client.pem',
+            SSL_key_file        => '/path/to/client.pem',
+        }
+    );
+
+
+=head1 METHODS
+
+=head2 C<perform_request()>
+
+    ($status,$body) = $self->perform_request({
+        # required
+        method      => 'GET|HEAD|POST|PUT|DELETE',
+        path        => '/path/of/request',
+        qs          => \%query_string_params,
+
+        # optional
+        data        => $body_as_string,
+        mime_type   => 'application/json',
+        timeout     => $timeout
+    });
+
+Sends the request to the associated Elasticsearch node and returns
+a C<$status> code and the decoded response C<$body>, or throws an
+error if the request failed.
+
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<scheme()|Search::Elasticsearch::Role::Cxn/"scheme()">
+
+=item * L<is_https()|Search::Elasticsearch::Role::Cxn/"is_https()">
+
+=item * L<userinfo()|Search::Elasticsearch::Role::Cxn/"userinfo()">
+
+=item * L<default_headers()|Search::Elasticsearch::Role::Cxn/"default_headers()">
+
+=item * L<max_content_length()|Search::Elasticsearch::Role::Cxn/"max_content_length()">
+
+=item * L<build_uri()|Search::Elasticsearch::Role::Cxn/"build_uri()">
+
+=item * L<host()|Search::Elasticsearch::Role::Cxn/"host()">
+
+=item * L<port()|Search::Elasticsearch::Role::Cxn/"port()">
+
+=item * L<uri()|Search::Elasticsearch::Role::Cxn/"uri()">
+
+=item * L<is_dead()|Search::Elasticsearch::Role::Cxn/"is_dead()">
+
+=item * L<is_live()|Search::Elasticsearch::Role::Cxn/"is_live()">
+
+=item * L<next_ping()|Search::Elasticsearch::Role::Cxn/"next_ping()">
+
+=item * L<ping_failures()|Search::Elasticsearch::Role::Cxn/"ping_failures()">
+
+=item * L<mark_dead()|Search::Elasticsearch::Role::Cxn/"mark_dead()">
+
+=item * L<mark_live()|Search::Elasticsearch::Role::Cxn/"mark_live()">
+
+=item * L<force_ping()|Search::Elasticsearch::Role::Cxn/"force_ping()">
+
+=item * L<pings_ok()|Search::Elasticsearch::Role::Cxn/"pings_ok()">
+
+=item * L<sniff()|Search::Elasticsearch::Role::Cxn/"sniff()">
+
+=item * L<process_response()|Search::Elasticsearch::Role::Cxn/"process_response()">
+
+=back
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Role::Cxn>
+
+=item * L<Search::Elasticsearch::Cxn::HTTPTiny>
+
+=item * L<Search::Elasticsearch::Cxn::NetCurl>
+
+=back
+
+
+
diff --git a/lib/Search/Elasticsearch/Cxn/Mojo.pm b/lib/Search/Elasticsearch/Cxn/Mojo.pm
new file mode 100644
index 0000000..23bd25c
--- /dev/null
+++ b/lib/Search/Elasticsearch/Cxn/Mojo.pm
@@ -0,0 +1,292 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Cxn::Mojo;
+
+use Mojo::UserAgent();
+use Promises qw(deferred);
+use Try::Tiny;
+use Moo;
+
+with 'Search::Elasticsearch::Role::Cxn::Async';
+with 'Search::Elasticsearch::Role::Cxn',
+    'Search::Elasticsearch::Role::Is_Async';
+
+has 'connect_timeout' => ( is => 'ro', default => 2 );
+
+use namespace::clean;
+
+#===================================
+sub perform_request {
+#===================================
+    my ( $self, $params ) = @_;
+
+    my $uri     = $self->build_uri($params) . '';
+    my $method  = $params->{method};
+    my %headers = ( %{ $self->default_headers } );
+
+    my @args = ( $method, $uri, \%headers );
+    my $data = $params->{data};
+    if ( defined $data ) {
+        $headers{'Content-Type'}     = $params->{mime_type};
+        $headers{'Content-Encoding'} = $params->{encoding}
+            if $params->{encoding};
+        push @args, $data;
+    }
+
+    my $handle = $self->handle;
+    $handle->connect_timeout( $self->connect_timeout );
+    $handle->request_timeout( $params->{timeout} || $self->request_timeout );
+
+    my $tx = $handle->build_tx(@args);
+
+    my $deferred = deferred;
+    $tx = $handle->start(
+        $tx,
+        sub {
+            my ( $ua, $tx ) = @_;
+            my $res = $tx->res;
+            my $error;
+            if ( $error = $res->error ) {
+                $error = $error->{message}
+                    if ref $error eq 'HASH';
+            }
+
+            my $headers = $res->headers->to_hash;
+            $headers->{ lc($_) } = delete $headers->{$_} for keys %{$headers};
+            try {
+                my ( $code, $response ) = $self->process_response(
+                    $params,    # request
+                    ( $res->code || 500 ),    # status
+                    $error,                   # reason
+                    $res->body,               # content
+                    $headers,                 # headers
+                );
+                $deferred->resolve( $code, $response );
+            }
+            catch {
+                $deferred->reject($_);
+            };
+        }
+    );
+    $deferred->promise;
+}
+
+#===================================
+sub error_from_text {
+#===================================
+    local $_ = $_[2];
+    return
+          /[Tt]imed out/               ? 'Timeout'
+        : /SSL connect attempt failed/ ? 'SSL'
+        : /Invalid argument/           ? 'Cxn'
+        :                                'Request';
+}
+
+#===================================
+sub _build_handle {
+#===================================
+    my $self = shift;
+    my %args = %{ $self->handle_args };
+    if ( $self->is_https && $self->has_ssl_options ) {
+        %args = ( %args, %{ $self->ssl_options } );
+    }
+    return Mojo::UserAgent->new(%args);
+}
+1;
+
+# ABSTRACT: An async Cxn implementation which uses Mojo::UserAgent
+
+=head1 DESCRIPTION
+
+Provides an async HTTP Cxn class based on L<Mojo::UserAgent>.
+The Mojo backend is fast, uses pure Perl, support proxies and https
+and provides persistent connections.
+
+This class does L<Search::Elasticsearch::Role::Cxn>, whose documentation
+provides more information, L<Search::Elasticsearch::Role::Async::Cxn>,
+and L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 CONFIGURATION
+
+=head2 C<connect_timeout>
+
+Unlike most HTTP backends, L<Mojo::UserAgent> accepts a separate C<connect_timeout>
+parameter, which defaults to C<2> seconds but can be reduced in an
+environment with low network latency.
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<node|Search::Elasticsearch::Role::Cxn/"node">
+
+=item * L<max_content_length|Search::Elasticsearch::Role::Cxn/"max_content_length">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"gzip">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"deflate">
+
+=item * L<request_timeout|Search::Elasticsearch::Role::Cxn/"request_timeout">
+
+=item * L<ping_timeout|Search::Elasticsearch::Role::Cxn/"ping_timeout">
+
+=item * L<dead_timeout|Search::Elasticsearch::Role::Cxn/"dead_timeout">
+
+=item * L<max_dead_timeout|Search::Elasticsearch::Role::Cxn/"max_dead_timeout">
+
+=item * L<sniff_request_timeout|Search::Elasticsearch::Role::Cxn/"sniff_request_timeout">
+
+=item * L<sniff_timeout|Search::Elasticsearch::Role::Cxn/"sniff_timeout">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"handle_args">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"default_qs_params">
+
+=back
+
+=head1 SSL/TLS
+
+L<Search::Elasticsearch::Cxn::Mojo> does no validation of the remote host by default.
+
+This behaviour can be changed by passing the C<ssl_options> parameter
+with the C<ca>, C<cert>, and C<key> options. For instance, to check
+that the remote host has a trusted certificate, and to avoid man-in-the-middle
+attacks, you could do the following:
+
+    use Search::Elasticsearch::Async;
+
+    my $es = Search::Elasticsearch::Async->new(
+        cxn   => 'Mojo',
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            ca  => '/path/to/cacert.pem'
+        }
+    );
+
+If the remote server cannot be verified, an
+L<Search::Elasticsearch::Error|SSL error> will be thrown.
+
+If you want your client to present its own certificate to the remote
+server, then use:
+
+    use Search::Elasticsearch::Async;
+
+    my $es = Search::Elasticsearch::Async->new(
+        cxn   => 'Mojo',
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            ca   => '/path/to/cacert.pem'
+            cert => '/path/to/client.pem',
+            key  => '/path/to/client.pem'
+        }
+    );
+
+
+=head1 METHODS
+
+=head2 C<perform_request()>
+
+    $self->perform_request({
+        # required
+        method      => 'GET|HEAD|POST|PUT|DELETE',
+        path        => '/path/of/request',
+        qs          => \%query_string_params,
+
+        # optional
+        data        => $body_as_string,
+        mime_type   => 'application/json',
+        timeout     => $timeout
+    })
+    ->then(sub { my ($status,body) = @_; ...})
+
+Sends the request to the associated Elasticsearch node and returns
+a C<$status> code and the decoded response C<$body>, or throws an
+error if the request failed.
+
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<scheme()|Search::Elasticsearch::Role::Cxn/"scheme()">
+
+=item * L<is_https()|Search::Elasticsearch::Role::Cxn/"is_https()">
+
+=item * L<userinfo()|Search::Elasticsearch::Role::Cxn/"userinfo()">
+
+=item * L<default_headers()|Search::Elasticsearch::Role::Cxn/"default_headers()">
+
+=item * L<max_content_length()|Search::Elasticsearch::Role::Cxn/"max_content_length()">
+
+=item * L<build_uri()|Search::Elasticsearch::Role::Cxn/"build_uri()">
+
+=item * L<host()|Search::Elasticsearch::Role::Cxn/"host()">
+
+=item * L<port()|Search::Elasticsearch::Role::Cxn/"port()">
+
+=item * L<uri()|Search::Elasticsearch::Role::Cxn/"uri()">
+
+=item * L<is_dead()|Search::Elasticsearch::Role::Cxn/"is_dead()">
+
+=item * L<is_live()|Search::Elasticsearch::Role::Cxn/"is_live()">
+
+=item * L<next_ping()|Search::Elasticsearch::Role::Cxn/"next_ping()">
+
+=item * L<ping_failures()|Search::Elasticsearch::Role::Cxn/"ping_failures()">
+
+=item * L<mark_dead()|Search::Elasticsearch::Role::Cxn/"mark_dead()">
+
+=item * L<mark_live()|Search::Elasticsearch::Role::Cxn/"mark_live()">
+
+=item * L<force_ping()|Search::Elasticsearch::Role::Cxn/"force_ping()">
+
+=item * L<pings_ok()|Search::Elasticsearch::Role::Cxn/"pings_ok()">
+
+=item * L<sniff()|Search::Elasticsearch::Role::Cxn/"sniff()">
+
+=item * L<process_response()|Search::Elasticsearch::Role::Cxn/"process_response()">
+
+=back
+
+From L<Search::Elasticsearch::Role::Async::Cxn>
+
+=over
+
+=item * L<pings_ok()|Search::Elasticsearch::Role::Cxn/"pings_ok()">
+
+=item * L<sniff()|Search::Elasticsearch::Role::Cxn/"sniff()">
+
+=back
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Role::Cxn::AEHTTP>
+
+=back
diff --git a/lib/Search/Elasticsearch/Cxn/NetCurl.pm b/lib/Search/Elasticsearch/Cxn/NetCurl.pm
new file mode 100644
index 0000000..30489c9
--- /dev/null
+++ b/lib/Search/Elasticsearch/Cxn/NetCurl.pm
@@ -0,0 +1,383 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Cxn::NetCurl;
+
+use Moo;
+with 'Search::Elasticsearch::Role::Cxn', 'Search::Elasticsearch::Role::Is_Sync';
+
+use Search::Elasticsearch 8.00;
+our $VERSION = '8.00';
+
+use HTTP::Parser::XS qw(HEADERS_AS_HASHREF parse_http_response);
+use Try::Tiny;
+use Net::Curl::Easy qw(
+    CURLOPT_HEADER
+    CURLOPT_VERBOSE
+    CURLOPT_URL
+    CURLOPT_CONNECTTIMEOUT_MS
+    CURLOPT_CUSTOMREQUEST
+    CURLOPT_TIMEOUT_MS
+    CURLOPT_POSTFIELDS
+    CURLOPT_POSTFIELDSIZE
+    CURLOPT_HTTPHEADER
+    CURLOPT_SSL_VERIFYPEER
+    CURLOPT_SSL_VERIFYHOST
+    CURLOPT_WRITEDATA
+    CURLOPT_HEADERDATA
+    CURLINFO_RESPONSE_CODE
+    CURLOPT_TCP_NODELAY
+    CURLOPT_NOBODY
+);
+
+has 'connect_timeout' => ( is => 'ro', default => 2 );
+
+use namespace::clean;
+
+#===================================
+sub perform_request {
+#===================================
+    my ( $self, $params ) = @_;
+    my $uri    = $self->build_uri($params);
+    my $method = $params->{method};
+
+    my $handle = $self->handle;
+    $handle->reset;
+
+    # $handle->setopt( CURLOPT_VERBOSE,     1 );
+
+    $handle->setopt( CURLOPT_HEADER,        0 );
+    $handle->setopt( CURLOPT_TCP_NODELAY,   1 );
+    $handle->setopt( CURLOPT_URL,           $uri );
+    $handle->setopt( CURLOPT_CUSTOMREQUEST, $method );
+    $handle->setopt( CURLOPT_NOBODY,        1 ) if $method eq 'HEAD';
+
+    $handle->setopt( CURLOPT_CONNECTTIMEOUT_MS, $self->connect_timeout * 1000 );
+    $handle->setopt( CURLOPT_TIMEOUT_MS,
+        1000 * ( $params->{timeout} || $self->request_timeout ) );
+
+    my %headers = %{ $self->default_headers };
+
+    my $data = $params->{data};
+    if ( defined $data ) {
+        $headers{'Content-Type'}     = $params->{mime_type};
+        $headers{'Expect'}           = '';
+        $headers{'Content-Encoding'} = $params->{encoding}
+            if $params->{encoding};
+        $handle->setopt( CURLOPT_POSTFIELDS,    $data );
+        $handle->setopt( CURLOPT_POSTFIELDSIZE, length $data );
+    }
+
+    $handle->setopt( CURLOPT_HTTPHEADER,
+        [ map { "$_: " . $headers{$_} } keys %headers ] )
+        if %headers;
+
+    my %opts = %{ $self->handle_args };
+    if ( $self->is_https ) {
+        if ( $self->has_ssl_options ) {
+            %opts = ( %opts, %{ $self->ssl_options } );
+        }
+        else {
+            %opts = (
+                %opts,
+                (   CURLOPT_SSL_VERIFYPEER() => 0,
+                    CURLOPT_SSL_VERIFYHOST() => 0
+                )
+            );
+        }
+    }
+
+    for ( keys %opts ) {
+        $handle->setopt( $_, $opts{$_} );
+    }
+
+    my $content = my $head = '';
+    $handle->setopt( CURLOPT_WRITEDATA,  \$content );
+    $handle->setopt( CURLOPT_HEADERDATA, \$head );
+
+    my ( $code, $msg, $headers );
+
+    try {
+        $handle->perform;
+        ( undef, undef, $code, $msg, $headers )
+            = parse_http_response( $head, HEADERS_AS_HASHREF );
+    }
+    catch {
+        $code = 509;
+        $msg  = ( 0 + $_ ) . ": $_";
+        $msg . ", " . $handle->error
+            if $handle->error;
+        undef $content;
+    };
+
+    return $self->process_response(
+        $params,     # request
+        $code,       # code
+        $msg,        # msg
+        $content,    # body
+        $headers     # headers
+    );
+}
+
+#===================================
+sub error_from_text {
+#===================================
+    local $_ = $_[2];
+    shift;
+    return
+          m/^7:/  ? 'Cxn'
+        : m/^28:/ ? 'Timeout'
+        : m/^51:/ ? 'SSL'
+        : m/^55:/ ? 'ContentLength'
+        :           'Request';
+
+}
+
+#===================================
+sub _build_handle { Net::Curl::Easy->new }
+#===================================
+
+1;
+
+# ABSTRACT: A Cxn implementation which uses libcurl via Net::Curl
+
+=head1 DESCRIPTION
+
+Provides an HTTP Cxn class based on L<Net::Curl>.
+The C<NetCurl> Cxn class is very fast and uses persistent connections but
+requires XS and C<libcurl>.
+
+This class does L<Search::Elasticsearch::Role::Cxn>, whose documentation
+provides more information.
+
+=head1 CONFIGURATION
+
+=head2 C<connect_timeout>
+
+Unlike most HTTP backends, L<Net::Curl> accepts a separate C<connect_timeout>
+parameter, which defaults to C<2> seconds but can be reduced in an
+environment with low network latency.
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<node|Search::Elasticsearch::Role::Cxn/"node">
+
+=item * L<max_content_length|Search::Elasticsearch::Role::Cxn/"max_content_length">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"gzip">
+
+=item * L<deflate|Search::Elasticsearch::Role::Cxn/"deflate">
+
+=item * L<request_timeout|Search::Elasticsearch::Role::Cxn/"request_timeout">
+
+=item * L<ping_timeout|Search::Elasticsearch::Role::Cxn/"ping_timeout">
+
+=item * L<dead_timeout|Search::Elasticsearch::Role::Cxn/"dead_timeout">
+
+=item * L<max_dead_timeout|Search::Elasticsearch::Role::Cxn/"max_dead_timeout">
+
+=item * L<sniff_request_timeout|Search::Elasticsearch::Role::Cxn/"sniff_request_timeout">
+
+=item * L<sniff_timeout|Search::Elasticsearch::Role::Cxn/"sniff_timeout">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"handle_args">
+
+=item * L<handle_args|Search::Elasticsearch::Role::Cxn/"default_qs_params">
+
+=back
+
+=head1 SSL/TLS
+
+L<Search::Elasticsearch::Cxn::NetCurl> does no validation of remote
+hosts by default.
+
+This behaviour can be changed by passing the C<ssl_options> parameter
+with any options accepted by L<Net::Curl> (see L<http://curl.haxx.se/libcurl/c/curl_easy_setopt.html>).
+
+For instance, to check that the remote host has a trusted certificate,
+and to avoid man-in-the-middle attacks, you could do the following:
+
+    use Search::Elasticsearch;
+    use Net::Curl::Easy qw(
+        CURLOPT_CAINFO
+    );
+
+    my $es = Search::Elasticsearch->new(
+        cxn   => 'NetCurl',
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            CURLOPT_CAINFO()  => '/path/to/cacert.pem'
+        }
+    );
+
+If the remote server cannot be verified, an
+L<Search::Elasticsearch::Error|SSL error> will be thrown.
+
+If you want your client to present its own certificate to the remote
+server, then use:
+
+    use Net::Curl::Easy qw(
+        CURLOPT_CAINFO
+        CURLOPT_SSLCERT
+        CURLOPT_SSLKEY
+    );
+
+    my $es = Search::Elasticsearch->new(
+        cxn   => 'NetCurl',
+        nodes => [
+            "https://node1.mydomain.com:9200",
+            "https://node2.mydomain.com:9200",
+        ],
+        ssl_options => {
+            CURLOPT_CAINFO()      => '/path/to/cacert.pem'
+            CURLOPT_SSLCERT()     => '/path/to/client.pem',
+            CURLOPT_SSLKEY()      => '/path/to/client.pem',
+        }
+    );
+
+
+
+=head1 METHODS
+
+=head2 C<perform_request()>
+
+    ($status,$body) = $self->perform_request({
+        # required
+        method      => 'GET|HEAD|POST|PUT|DELETE',
+        path        => '/path/of/request',
+        qs          => \%query_string_params,
+
+        # optional
+        data        => $body_as_string,
+        mime_type   => 'application/json',
+        timeout     => $timeout
+    });
+
+Sends the request to the associated Elasticsearch node and returns
+a C<$status> code and the decoded response C<$body>, or throws an
+error if the request failed.
+
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::Cxn>
+
+=over
+
+=item * L<scheme()|Search::Elasticsearch::Role::Cxn/"scheme()">
+
+=item * L<is_https()|Search::Elasticsearch::Role::Cxn/"is_https()">
+
+=item * L<userinfo()|Search::Elasticsearch::Role::Cxn/"userinfo()">
+
+=item * L<default_headers()|Search::Elasticsearch::Role::Cxn/"default_headers()">
+
+=item * L<max_content_length()|Search::Elasticsearch::Role::Cxn/"max_content_length()">
+
+=item * L<build_uri()|Search::Elasticsearch::Role::Cxn/"build_uri()">
+
+=item * L<host()|Search::Elasticsearch::Role::Cxn/"host()">
+
+=item * L<port()|Search::Elasticsearch::Role::Cxn/"port()">
+
+=item * L<uri()|Search::Elasticsearch::Role::Cxn/"uri()">
+
+=item * L<is_dead()|Search::Elasticsearch::Role::Cxn/"is_dead()">
+
+=item * L<is_live()|Search::Elasticsearch::Role::Cxn/"is_live()">
+
+=item * L<next_ping()|Search::Elasticsearch::Role::Cxn/"next_ping()">
+
+=item * L<ping_failures()|Search::Elasticsearch::Role::Cxn/"ping_failures()">
+
+=item * L<mark_dead()|Search::Elasticsearch::Role::Cxn/"mark_dead()">
+
+=item * L<mark_live()|Search::Elasticsearch::Role::Cxn/"mark_live()">
+
+=item * L<force_ping()|Search::Elasticsearch::Role::Cxn/"force_ping()">
+
+=item * L<pings_ok()|Search::Elasticsearch::Role::Cxn/"pings_ok()">
+
+=item * L<sniff()|Search::Elasticsearch::Role::Cxn/"sniff()">
+
+=item * L<process_response()|Search::Elasticsearch::Role::Cxn/"process_response()">
+
+=back
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Role::Cxn>
+
+=item * L<Search::Elasticsearch::Cxn::LWP>
+
+=item * L<Search::Elasticsearch::Cxn::HTTPTiny>
+
+=back
+
+=head1 BUGS
+
+This is a stable API but this implemenation is new. Watch this space
+for new releases.
+
+If you have any suggestions for improvements, or find any bugs, please report
+them to L<http://github.com/elasticsearch/elasticsearch-perl/issues>.
+I will be notified, and then you'll automatically be notified of progress on
+your bug as I make changes.
+
+=head1 SUPPORT
+
+You can find documentation for this module with the perldoc command.
+
+    perldoc Search::Elasticsearch::Cxn::NetCurl
+
+You can also look for information at:
+
+=over 4
+
+=item * GitHub
+
+L<http://github.com/elasticsearch/elasticsearch-perl>
+
+=item * CPAN Ratings
+
+L<http://cpanratings.perl.org/d/Search::Elasticsearch::Cxn::NetCurl>
+
+
+=item * Search MetaCPAN
+
+L<https://metacpan.org/module/Search::Elasticsearch::Cxn::NetCurl>
+
+=item * IRC
+
+The L<#elasticsearch|irc://irc.freenode.net/elasticsearch> channel on
+C<irc.freenode.net>.
+
+=item * Mailing list
+
+The main L<Elasticsearch mailing list|http://www.elastic.co/community>.
+
+=back
+
diff --git a/lib/Search/Elasticsearch/CxnPool/Async/Sniff.pm b/lib/Search/Elasticsearch/CxnPool/Async/Sniff.pm
new file mode 100644
index 0000000..bb21298
--- /dev/null
+++ b/lib/Search/Elasticsearch/CxnPool/Async/Sniff.pm
@@ -0,0 +1,308 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::CxnPool::Async::Sniff;
+
+use Moo;
+with 'Search::Elasticsearch::Role::CxnPool::Sniff',
+    'Search::Elasticsearch::Role::Is_Async';
+
+use Scalar::Util qw(weaken);
+use Promises qw(deferred);
+use Search::Elasticsearch::Util qw(new_error);
+
+use namespace::clean;
+has 'concurrent_sniff' => ( is => 'rw', default => 4 );
+has '_current_sniff'   => ( is => 'rw', clearer => '_clear_sniff' );
+
+#===================================
+sub next_cxn {
+#===================================
+    my ( $self, $no_sniff ) = @_;
+
+    return $self->sniff->then( sub { $self->next_cxn('no_sniff') } )
+        if $self->next_sniff <= time() && !$no_sniff;
+
+    my $cxns  = $self->cxns;
+    my $total = @$cxns;
+    my $cxn;
+
+    while ( 0 < $total-- ) {
+        $cxn = $cxns->[ $self->next_cxn_num ];
+        last if $cxn->is_live;
+        undef $cxn;
+    }
+
+    my $deferred = deferred;
+
+    if ($cxn) {
+        $deferred->resolve($cxn);
+    }
+    else {
+        $deferred->reject(
+            new_error(
+                "NoNodes",
+                "No nodes are available: [" . $self->cxns_seeds_str . ']'
+            )
+        );
+    }
+    return $deferred->promise;
+}
+
+#===================================
+sub sniff {
+#===================================
+    my $self = shift;
+
+    my $promise;
+    if ( $promise = $self->_current_sniff ) {
+        return $promise;
+    }
+
+    my $deferred   = deferred;
+    my $cxns       = $self->cxns;
+    my $total      = @$cxns;
+    my $done       = 0;
+    my $current    = 0;
+    my $done_seeds = 0;
+    $promise = $self->_current_sniff( $deferred->promise );
+
+    my ( @all, @skipped );
+
+    while ( 0 < $total-- ) {
+        my $cxn = $cxns->[ $self->next_cxn_num ];
+        if ( $cxn->is_dead ) {
+            push @skipped, $cxn;
+        }
+        else {
+            push @all, $cxn;
+        }
+    }
+
+    push @all, @skipped;
+    unless (@all) {
+        @all = $self->_seeds_as_cxns;
+        $done_seeds++;
+    }
+
+    my ( $weak_check_sniff, $cxn );
+    my $check_sniff = sub {
+
+        return if $done;
+        my ( $cxn, $nodes ) = @_;
+        if ( $nodes && $self->parse_sniff($nodes) ) {
+            $done++;
+            $self->_clear_sniff;
+            return $deferred->resolve();
+        }
+
+        unless ( @all || $done_seeds++ ) {
+            $self->logger->info("No live nodes available. Trying seed nodes.");
+            @all = $self->_seeds_as_cxns;
+        }
+
+        if ( my $cxn = shift @all ) {
+            return $cxn->sniff->done($weak_check_sniff);
+        }
+        if ( --$current == 0 ) {
+            $self->_clear_sniff;
+            $deferred->resolve();
+        }
+    };
+    weaken( $weak_check_sniff = $check_sniff );
+
+    for ( 1 .. $self->concurrent_sniff ) {
+        my $cxn = shift(@all) || last;
+        $current++;
+        $cxn->sniff->done($check_sniff);
+    }
+
+    return $promise;
+}
+
+#===================================
+sub _seeds_as_cxns {
+#===================================
+    my $self    = shift;
+    my $factory = $self->cxn_factory;
+    return map { $factory->new_cxn($_) } @{ $self->seed_nodes };
+}
+
+1;
+
+# ABSTRACT: An async CxnPool for connecting to a local cluster with a dynamic node list
+
+=head1 SYNOPSIS
+
+
+    $e = Search::Elasticsearch::Async->new(
+        cxn_pool => 'Async::Sniff',
+        nodes    => [
+            'search1:9200',
+            'search2:9200'
+        ],
+    );
+
+=head1 DESCRIPTION
+
+The L<Async::Sniff|Search::Elasticsearch::CxnPool::Async::Sniff> connection
+pool should be used when you B<do> have direct access to the Elasticsearch
+cluster, eg when your web servers and Elasticsearch servers are on the same
+network. The nodes that you specify are used to I<discover> the cluster,
+which is then I<sniffed> to find the current list of live nodes that the
+cluster knows about.
+
+This sniff process is repeated regularly, or whenever a node fails,
+to update the list of healthy nodes.  So if you add more nodes to your
+cluster, they will be auto-discovered during a sniff.
+
+If all sniffed nodes fail, then it falls back to sniffing the original
+I<seed> nodes that you specified in C<new()>.
+
+For L<HTTP Cxn classes|Search::Elasticsearch::Role::Cxn>, this module
+will also dynamically detect the C<max_content_length> which the nodes
+in the cluster will accept.
+
+This class does L<Search::Elasticsearch::Role::CxnPool::Sniff> and
+L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 CONFIGURATION
+
+=head2 C<nodes>
+
+The list of nodes to use to discover the cluster.  Can accept a single node,
+multiple nodes, and defaults to C<localhost:9200> if no C<nodes> are
+specified. See L<Search::Elasticsearch::Role::Cxn/node> for details of the node
+specification.
+
+=head2 C<concurrent_sniff>
+
+By default, this module will issue up to 4 concurrent sniff requests in parallel,
+depending on how many nodes are known.  The first successful response is used
+to set the new list of live nodes.  Set C<concurrent_sniff> to change the
+maximum number of concurrent sniff requests.
+
+=head2 See also
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/request_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/sniff_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/sniff_request_timeout>
+
+=back
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::CxnPool::Sniff>
+
+=over
+
+=item * L<sniff_interval|Search::Elasticsearch::Role::CxnPool::Sniff/"sniff_interval">
+
+=item * L<sniff_max_content_length|Search::Elasticsearch::Role::CxnPool::Sniff/"sniff_max_content_length">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<randomize_cxns|Search::Elasticsearch::Role::CxnPool/"randomize_cxns">
+
+=back
+
+=head1 METHODS
+
+=head2 C<next_cxn()>
+
+    $cxn_pool->next_cxn
+             -> then( sub { my $cxn = shift })
+
+Returns the next available live node (in round robin fashion), or
+throws a C<NoNodes> error if no nodes can be sniffed from the cluster.
+
+=head2 C<sniff()>
+
+    $cxn_pool->sniff->then(
+        sub { "ok"     },
+        sub { "not_ok" }
+    );
+
+Sniffs the cluster and returns a promise which is resolved on success, or
+rejected on failure.
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::CxnPool::Sniff>
+
+=over
+
+=item * L<schedule_check()|Search::Elasticsearch::Role::CxnPool::Sniff/"schedule_check()">
+
+=item * L<parse_sniff()|Search::Elasticsearch::Role::CxnPool::Sniff/"parse_sniff()">
+
+=item * L<should_accept_node()|Search::Elasticsearch::Role::CxnPool::Sniff/"should_accept_node()">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<cxn_factory()|Search::Elasticsearch::Role::CxnPool/"cxn_factory()">
+
+=item * L<logger()|Search::Elasticsearch::Role::CxnPool/"logger()">
+
+=item * L<serializer()|Search::Elasticsearch::Role::CxnPool/"serializer()">
+
+=item * L<current_cxn_num()|Search::Elasticsearch::Role::CxnPool/"current_cxn_num()">
+
+=item * L<cxns()|Search::Elasticsearch::Role::CxnPool/"cxns()">
+
+=item * L<seed_nodes()|Search::Elasticsearch::Role::CxnPool/"seed_nodes()">
+
+=item * L<next_cxn_num()|Search::Elasticsearch::Role::CxnPool/"next_cxn_num()">
+
+=item * L<set_cxns()|Search::Elasticsearch::Role::CxnPool/"set_cxns()">
+
+=item * L<request_ok()|Search::Elasticsearch::Role::CxnPool/"request_ok()">
+
+=item * L<request_failed()|Search::Elasticsearch::Role::CxnPool/"request_failed()">
+
+=item * L<should_retry()|Search::Elasticsearch::Role::CxnPool/"should_retry()">
+
+=item * L<should_mark_dead()|Search::Elasticsearch::Role::CxnPool/"should_mark_dead()">
+
+=item * L<cxns_str()|Search::Elasticsearch::Role::CxnPool/"cxns_str()">
+
+=item * L<cxns_seeds_str()|Search::Elasticsearch::Role::CxnPool/"cxns_seeds_str()">
+
+=item * L<retries()|Search::Elasticsearch::Role::CxnPool/"retries()">
+
+=item * L<reset_retries()|Search::Elasticsearch::Role::CxnPool/"reset_retries()">
+
+=back
+
diff --git a/lib/Search/Elasticsearch/CxnPool/Async/Static.pm b/lib/Search/Elasticsearch/CxnPool/Async/Static.pm
new file mode 100644
index 0000000..873c2ad
--- /dev/null
+++ b/lib/Search/Elasticsearch/CxnPool/Async/Static.pm
@@ -0,0 +1,213 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::CxnPool::Async::Static;
+
+use Moo;
+with 'Search::Elasticsearch::Role::CxnPool::Static',
+    'Search::Elasticsearch::Role::Is_Async';
+
+use Search::Elasticsearch::Util qw(new_error);
+use Scalar::Util qw(weaken);
+use Promises qw(deferred);
+use namespace::clean;
+
+#===================================
+sub next_cxn {
+#===================================
+    my ($self) = @_;
+
+    my $cxns     = $self->cxns;
+    my $now      = time();
+    my $deferred = deferred;
+
+    my ( %seen, @skipped, $weak_find_cxn );
+
+    my $find_cxn = sub {
+        my $total = @$cxns;
+        my $found;
+
+        if ( $total > keys %seen ) {
+
+            # we haven't seen all cxns yet
+            while ( $total-- ) {
+                my $cxn = $cxns->[ $self->next_cxn_num ];
+                next if $seen{$cxn}++;
+
+                return $deferred->resolve($cxn)
+                    if $cxn->is_live;
+
+                if ( $cxn->next_ping <= time() ) {
+                    $found = $cxn;
+                    last;
+                }
+
+                push @skipped, $cxn;
+            }
+        }
+
+        if ( $found ||= shift @skipped ) {
+            return $found->pings_ok->then(
+                sub { $deferred->resolve($found) },    # success
+                $weak_find_cxn                         # resolve
+            );
+        }
+
+        $_->force_ping for @$cxns;
+
+        return $deferred->reject(
+            new_error(
+                "NoNodes", "No nodes are available: [" . $self->cxns_str . ']'
+            )
+        );
+
+    };
+    weaken( $weak_find_cxn = $find_cxn );
+
+    $find_cxn->();
+    $deferred->promise;
+}
+
+1;
+
+# ABSTRACT: An async CxnPool for connecting to a remote cluster with a static list of nodes.
+
+=head1 SYNOPSIS
+
+
+    $e = Search::Elasticsearch::Async->new(
+        cxn_pool => 'Async::Static'     # default
+        nodes    => [
+            'search1:9200',
+            'search2:9200'
+        ],
+    );
+
+=head1 DESCRIPTION
+
+The L<Async::Static|Search::Elasticsearch::CxnPool::Async::Static> connection
+pool, which is the default, should be used when you don't have direct access
+to the Elasticsearch cluster, eg when you are accessing the cluster through a
+proxy.  It round-robins through the nodes that you specified, and pings each
+node before it is used for  the first time, to ensure that it is responding.
+
+If any node fails, then all nodes are pinged before the next request to
+ensure that they are still alive and responding.  Failed nodes will be
+pinged regularly to check if they have recovered.
+
+This class does L<Search::Elasticsearch::Role::CxnPool::Async::Static> and
+L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 CONFIGURATION
+
+=head2 C<nodes>
+
+The list of nodes to use to serve requests.  Can accept a single node,
+multiple nodes, and defaults to C<localhost:9200> if no C<nodes> are
+specified. See L<Search::Elasticsearch::Role::Cxn/node> for details of the node
+specification.
+
+=head2 See also
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/request_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/ping_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/dead_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/max_dead_timeout>
+
+=back
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<randomize_cxns|Search::Elasticsearch::Role::CxnPool/"randomize_cxns">
+
+=back
+
+
+=head1 METHODS
+
+=head2 C<next_cxn()>
+
+    $cxn_pool->next_cxn
+             ->then( sub { my $cxn = shift });
+
+Returns the next available live node (in round robin fashion), or
+throws a C<NoNodes> error if no nodes respond to ping requests.
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::CxnPool::Static>
+
+=over
+
+=item * L<schedule_check()|Search::Elasticsearch::Role::CxnPool::Static/"schedule_check()">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<cxn_factory()|Search::Elasticsearch::Role::CxnPool/"cxn_factory()">
+
+=item * L<logger()|Search::Elasticsearch::Role::CxnPool/"logger()">
+
+=item * L<serializer()|Search::Elasticsearch::Role::CxnPool/"serializer()">
+
+=item * L<current_cxn_num()|Search::Elasticsearch::Role::CxnPool/"current_cxn_num()">
+
+=item * L<cxns()|Search::Elasticsearch::Role::CxnPool/"cxns()">
+
+=item * L<seed_nodes()|Search::Elasticsearch::Role::CxnPool/"seed_nodes()">
+
+=item * L<next_cxn_num()|Search::Elasticsearch::Role::CxnPool/"next_cxn_num()">
+
+=item * L<set_cxns()|Search::Elasticsearch::Role::CxnPool/"set_cxns()">
+
+=item * L<request_ok()|Search::Elasticsearch::Role::CxnPool/"request_ok()">
+
+=item * L<request_failed()|Search::Elasticsearch::Role::CxnPool/"request_failed()">
+
+=item * L<should_retry()|Search::Elasticsearch::Role::CxnPool/"should_retry()">
+
+=item * L<should_mark_dead()|Search::Elasticsearch::Role::CxnPool/"should_mark_dead()">
+
+=item * L<cxns_str()|Search::Elasticsearch::Role::CxnPool/"cxns_str()">
+
+=item * L<cxns_seeds_str()|Search::Elasticsearch::Role::CxnPool/"cxns_seeds_str()">
+
+=item * L<retries()|Search::Elasticsearch::Role::CxnPool/"retries()">
+
+=item * L<reset_retries()|Search::Elasticsearch::Role::CxnPool/"reset_retries()">
+
+=back
diff --git a/lib/Search/Elasticsearch/CxnPool/Async/Static/NoPing.pm b/lib/Search/Elasticsearch/CxnPool/Async/Static/NoPing.pm
new file mode 100644
index 0000000..c1f5ef2
--- /dev/null
+++ b/lib/Search/Elasticsearch/CxnPool/Async/Static/NoPing.pm
@@ -0,0 +1,189 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::CxnPool::Async::Static::NoPing;
+
+use Moo;
+with 'Search::Elasticsearch::Role::CxnPool::Static::NoPing',
+    'Search::Elasticsearch::Role::Is_Async';
+
+use Promises qw(deferred);
+use Try::Tiny;
+use namespace::clean;
+
+#===================================
+around 'next_cxn' => sub {
+#===================================
+    my ( $orig, $self ) = @_;
+
+    my $deferred = deferred;
+    try {
+        my $cxn = $orig->($self);
+        $deferred->resolve($cxn);
+    }
+    catch {
+        $deferred->reject($_);
+    };
+
+    $deferred->promise;
+
+};
+
+1;
+
+# ABSTRACT: An async CxnPool for connecting to a remote cluster without the ability to ping.
+
+=head1 SYNOPSIS
+
+    $e = Search::Elasticsearch::Async->new(
+        cxn_pool => 'Async::Static::NoPing'
+        nodes    => [
+            'search1:9200',
+            'search2:9200'
+        ],
+    );
+
+=head1 DESCRIPTION
+
+The L<Async::Static::NoPing|Search::Elasticsearch::CxnPool::Async::Static::NoPing>
+connection pool (like the L<Async::Static|Search::Elasticsearch::CxnPool::Async::Static>
+pool) should be used when your access to the cluster is limited.  However, the
+C<Async::Static> pool needs to be able to ping nodes in the cluster, with a
+C<HEAD /> request.  If you can't ping your nodes, then you should use the
+C<Async::Static::NoPing> connection pool instead.
+
+Because the cluster cannot be pinged, this CxnPool cannot use a short
+ping request to determine whether nodes are live or not - it just has to
+send requests to the nodes to determine whether they are alive or not.
+
+Most of the time, a dead node will cause the request to fail quickly.
+However, in situations where node failure takes time (eg malfunctioning
+routers or firewalls), a failure may not be reported until the request
+itself times out (see L<Search::Elasticsearch::Cxn/request_timeout>).
+
+Failed nodes will be retried regularly to check if they have recovered.
+
+This class does L<Search::Elasticsearch::Role::CxnPool::Static::NoPing> and
+L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 CONFIGURATION
+
+=head2 C<nodes>
+
+The list of nodes to use to serve requests.  Can accept a single node,
+multiple nodes, and defaults to C<localhost:9200> if no C<nodes> are
+specified. See L<Search::Elasticsearch::Role::Cxn/node> for details of the node
+specification.
+
+=head2 See also
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/request_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/dead_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/max_dead_timeout>
+
+=back
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::CxnPool::Static::NoPing>
+
+=over
+
+=item * L<max_retries|Search::Elasticsearch::Role::CxnPool::Static::NoPing/"max_retries">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<randomize_cxns|Search::Elasticsearch::Role::CxnPool/"randomize_cxns">
+
+=back
+
+=head1 METHODS
+
+=head2 C<next_cxn()>
+
+    $cxn_pool->next_cxn->then( sub { my $cxn = shift });
+
+Returns the next available node  in round robin fashion - either a live node
+which has previously responded successfully, or a previously failed
+node which should be retried. If all nodes are dead, it will throw
+a C<NoNodes> error.
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::CxnPool::Static::NoPing>
+
+=over
+
+=item * L<should_mark_dead()|Search::Elasticsearch::Role::CxnPool::Static::NoPing/"should_mark_dead()">
+
+=item * L<schedule_check()|Search::Elasticsearch::Role::CxnPool::Static::NoPing/"schedule_check()">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<cxn_factory()|Search::Elasticsearch::Role::CxnPool/"cxn_factory()">
+
+=item * L<logger()|Search::Elasticsearch::Role::CxnPool/"logger()">
+
+=item * L<serializer()|Search::Elasticsearch::Role::CxnPool/"serializer()">
+
+=item * L<current_cxn_num()|Search::Elasticsearch::Role::CxnPool/"current_cxn_num()">
+
+=item * L<cxns()|Search::Elasticsearch::Role::CxnPool/"cxns()">
+
+=item * L<seed_nodes()|Search::Elasticsearch::Role::CxnPool/"seed_nodes()">
+
+=item * L<next_cxn_num()|Search::Elasticsearch::Role::CxnPool/"next_cxn_num()">
+
+=item * L<set_cxns()|Search::Elasticsearch::Role::CxnPool/"set_cxns()">
+
+=item * L<request_ok()|Search::Elasticsearch::Role::CxnPool/"request_ok()">
+
+=item * L<request_failed()|Search::Elasticsearch::Role::CxnPool/"request_failed()">
+
+=item * L<should_retry()|Search::Elasticsearch::Role::CxnPool/"should_retry()">
+
+=item * L<should_mark_dead()|Search::Elasticsearch::Role::CxnPool/"should_mark_dead()">
+
+=item * L<cxns_str()|Search::Elasticsearch::Role::CxnPool/"cxns_str()">
+
+=item * L<cxns_seeds_str()|Search::Elasticsearch::Role::CxnPool/"cxns_seeds_str()">
+
+=item * L<retries()|Search::Elasticsearch::Role::CxnPool/"retries()">
+
+=item * L<reset_retries()|Search::Elasticsearch::Role::CxnPool/"reset_retries()">
+
+=back
+
+
diff --git a/lib/Search/Elasticsearch/CxnPool/Sniff.pm b/lib/Search/Elasticsearch/CxnPool/Sniff.pm
new file mode 100644
index 0000000..d760536
--- /dev/null
+++ b/lib/Search/Elasticsearch/CxnPool/Sniff.pm
@@ -0,0 +1,243 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::CxnPool::Sniff;
+
+use Moo;
+with 'Search::Elasticsearch::Role::CxnPool::Sniff',
+    'Search::Elasticsearch::Role::Is_Sync';
+
+use Search::Elasticsearch::Util qw(throw);
+use namespace::clean;
+
+#===================================
+sub next_cxn {
+#===================================
+    my ($self) = @_;
+
+    $self->sniff if $self->next_sniff <= time();
+
+    my $cxns  = $self->cxns;
+    my $total = @$cxns;
+
+    while ( 0 < $total-- ) {
+        my $cxn = $cxns->[ $self->next_cxn_num ];
+        return $cxn if $cxn->is_live;
+    }
+
+    throw( "NoNodes",
+        "No nodes are available: [" . $self->cxns_seeds_str . ']' );
+}
+
+#===================================
+sub sniff {
+#===================================
+    my $self = shift;
+
+    my $cxns  = $self->cxns;
+    my $total = @$cxns;
+    my @skipped;
+
+    while ( 0 < $total-- ) {
+        my $cxn = $cxns->[ $self->next_cxn_num ];
+        if ( $cxn->is_dead ) {
+            push @skipped, $cxn;
+        }
+        else {
+            $self->sniff_cxn($cxn) and return;
+            $cxn->mark_dead;
+        }
+    }
+
+    for my $cxn (@skipped) {
+        $self->sniff_cxn($cxn) and return;
+    }
+
+    $self->logger->info("No live nodes available. Trying seed nodes.");
+    for my $seed ( @{ $self->seed_nodes } ) {
+        my $cxn = $self->cxn_factory->new_cxn($seed);
+        $self->sniff_cxn($cxn) and return;
+    }
+
+}
+
+#===================================
+sub sniff_cxn {
+#===================================
+    my ( $self, $cxn ) = @_;
+    return $self->parse_sniff( $cxn->sniff );
+}
+
+1;
+
+# ABSTRACT: A CxnPool for connecting to a local cluster with a dynamic node list
+
+=head1 SYNOPSIS
+
+
+    $e = Search::Elasticsearch->new(
+        cxn_pool => 'Sniff',
+        nodes    => [
+            'search1:9200',
+            'search2:9200'
+        ],
+    );
+
+=head1 DESCRIPTION
+
+The L<Sniff|Search::Elasticsearch::CxnPool::Sniff> connection pool should be used
+when you B<do> have direct access to the Elasticsearch cluster, eg when
+your web servers and Elasticsearch servers are on the same network.
+The nodes that you specify are used to I<discover> the cluster, which is
+then I<sniffed> to find the current list of live nodes that the cluster
+knows about.
+
+This sniff process is repeated regularly, or whenever a node fails,
+to update the list of healthy nodes.  So if you add more nodes to your
+cluster, they will be auto-discovered during a sniff.
+
+If all sniffed nodes fail, then it falls back to sniffing the original
+I<seed> nodes that you specified in C<new()>.
+
+For L<HTTP Cxn classes|Search::Elasticsearch::Role::Cxn>, this module
+will also dynamically detect the C<max_content_length> which the nodes
+in the cluster will accept.
+
+This class does L<Search::Elasticsearch::Role::CxnPool::Sniff> and
+L<Search::Elasticsearch::Role::Is_Sync>.
+
+=head1 CONFIGURATION
+
+=head2 C<nodes>
+
+The list of nodes to use to discover the cluster.  Can accept a single node,
+multiple nodes, and defaults to C<localhost:9200> if no C<nodes> are
+specified. See L<Search::Elasticsearch::Role::Cxn/node> for details of the node
+specification.
+
+=head2 See also
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/request_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/sniff_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/sniff_request_timeout>
+
+=back
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::CxnPool::Sniff>
+
+=over
+
+=item * L<sniff_interval|Search::Elasticsearch::Role::CxnPool::Sniff/"sniff_interval">
+
+=item * L<sniff_max_content_length|Search::Elasticsearch::Role::CxnPool::Sniff/"sniff_max_content_length">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<randomize_cxns|Search::Elasticsearch::Role::CxnPool/"randomize_cxns">
+
+=back
+
+=head1 METHODS
+
+=head2 C<next_cxn()>
+
+    $cxn = $cxn_pool->next_cxn
+
+Returns the next available live node (in round robin fashion), or
+throws a C<NoNodes> error if no nodes can be sniffed from the cluster.
+
+=head2 C<schedule_check()>
+
+    $cxn_pool->schedule_check
+
+Forces a sniff before the next Cxn is returned, to updated the list of healthy
+nodes in the cluster.
+
+=head2 C<sniff()>
+
+    $bool = $cxn_pool->sniff
+
+Sniffs the cluster and returns C<true> if the sniff was successful.
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::CxnPool::Sniff>
+
+=over
+
+=item * L<schedule_check()|Search::Elasticsearch::Role::CxnPool::Sniff/"schedule_check()">
+
+=item * L<parse_sniff()|Search::Elasticsearch::Role::CxnPool::Sniff/"parse_sniff()">
+
+=item * L<should_accept_node()|Search::Elasticsearch::Role::CxnPool::Sniff/"should_accept_node()">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<cxn_factory()|Search::Elasticsearch::Role::CxnPool/"cxn_factory()">
+
+=item * L<logger()|Search::Elasticsearch::Role::CxnPool/"logger()">
+
+=item * L<serializer()|Search::Elasticsearch::Role::CxnPool/"serializer()">
+
+=item * L<current_cxn_num()|Search::Elasticsearch::Role::CxnPool/"current_cxn_num()">
+
+=item * L<cxns()|Search::Elasticsearch::Role::CxnPool/"cxns()">
+
+=item * L<seed_nodes()|Search::Elasticsearch::Role::CxnPool/"seed_nodes()">
+
+=item * L<next_cxn_num()|Search::Elasticsearch::Role::CxnPool/"next_cxn_num()">
+
+=item * L<set_cxns()|Search::Elasticsearch::Role::CxnPool/"set_cxns()">
+
+=item * L<request_ok()|Search::Elasticsearch::Role::CxnPool/"request_ok()">
+
+=item * L<request_failed()|Search::Elasticsearch::Role::CxnPool/"request_failed()">
+
+=item * L<should_retry()|Search::Elasticsearch::Role::CxnPool/"should_retry()">
+
+=item * L<should_mark_dead()|Search::Elasticsearch::Role::CxnPool/"should_mark_dead()">
+
+=item * L<cxns_str()|Search::Elasticsearch::Role::CxnPool/"cxns_str()">
+
+=item * L<cxns_seeds_str()|Search::Elasticsearch::Role::CxnPool/"cxns_seeds_str()">
+
+=item * L<retries()|Search::Elasticsearch::Role::CxnPool/"retries()">
+
+=item * L<reset_retries()|Search::Elasticsearch::Role::CxnPool/"reset_retries()">
+
+=back
+
diff --git a/lib/Search/Elasticsearch/CxnPool/Static.pm b/lib/Search/Elasticsearch/CxnPool/Static.pm
new file mode 100644
index 0000000..f4d6feb
--- /dev/null
+++ b/lib/Search/Elasticsearch/CxnPool/Static.pm
@@ -0,0 +1,187 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::CxnPool::Static;
+
+use Moo;
+with 'Search::Elasticsearch::Role::CxnPool::Static',
+    'Search::Elasticsearch::Role::Is_Sync';
+use Search::Elasticsearch::Util qw(throw);
+use namespace::clean;
+
+#===================================
+sub next_cxn {
+#===================================
+    my ($self) = @_;
+
+    my $cxns  = $self->cxns;
+    my $total = @$cxns;
+
+    my $now = time();
+    my @skipped;
+
+    while ( $total-- ) {
+        my $cxn = $cxns->[ $self->next_cxn_num ];
+        return $cxn if $cxn->is_live;
+
+        if ( $cxn->next_ping < $now ) {
+            return $cxn if $cxn->pings_ok;
+        }
+        else {
+            push @skipped, $cxn;
+        }
+    }
+
+    for my $cxn (@skipped) {
+        return $cxn if $cxn->pings_ok;
+    }
+
+    $_->force_ping for @$cxns;
+
+    throw( "NoNodes", "No nodes are available: [" . $self->cxns_str . ']' );
+}
+
+1;
+
+__END__
+
+# ABSTRACT: A CxnPool for connecting to a remote cluster with a static list of nodes.
+
+=head1 SYNOPSIS
+
+
+    $e = Search::Elasticsearch->new(
+        cxn_pool => 'Static'     # default
+        nodes    => [
+            'search1:9200',
+            'search2:9200'
+        ],
+    );
+
+=head1 DESCRIPTION
+
+The L<Static|Search::Elasticsearch::CxnPool::Static> connection pool, which is the
+default, should be used when you don't have direct access to the Elasticsearch
+cluster, eg when you are accessing the cluster through a proxy.  It
+round-robins through the nodes that you specified, and pings each node
+before it is used for  the first time, to ensure that it is responding.
+
+If any node fails, then all nodes are pinged before the next request to
+ensure that they are still alive and responding.  Failed nodes will be
+pinged regularly to check if they have recovered.
+
+This class does L<Search::Elasticsearch::Role::CxnPool::Static> and
+L<Search::Elasticsearch::Role::Is_Sync>.
+
+=head1 CONFIGURATION
+
+=head2 C<nodes>
+
+The list of nodes to use to serve requests.  Can accept a single node,
+multiple nodes, and defaults to C<localhost:9200> if no C<nodes> are
+specified. See L<Search::Elasticsearch::Role::Cxn/node> for details of the node
+specification.
+
+=head2 See also
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/request_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/ping_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/dead_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/max_dead_timeout>
+
+=back
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<randomize_cxns|Search::Elasticsearch::Role::CxnPool/"randomize_cxns">
+
+=back
+
+
+=head1 METHODS
+
+=head2 C<next_cxn()>
+
+    $cxn = $cxn_pool->next_cxn
+
+Returns the next available live node (in round robin fashion), or
+throws a C<NoNodes> error if no nodes respond to ping requests.
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::CxnPool::Static>
+
+=over
+
+=item * L<schedule_check()|Search::Elasticsearch::Role::CxnPool::Static/"schedule_check()">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<cxn_factory()|Search::Elasticsearch::Role::CxnPool/"cxn_factory()">
+
+=item * L<logger()|Search::Elasticsearch::Role::CxnPool/"logger()">
+
+=item * L<serializer()|Search::Elasticsearch::Role::CxnPool/"serializer()">
+
+=item * L<current_cxn_num()|Search::Elasticsearch::Role::CxnPool/"current_cxn_num()">
+
+=item * L<cxns()|Search::Elasticsearch::Role::CxnPool/"cxns()">
+
+=item * L<seed_nodes()|Search::Elasticsearch::Role::CxnPool/"seed_nodes()">
+
+=item * L<next_cxn_num()|Search::Elasticsearch::Role::CxnPool/"next_cxn_num()">
+
+=item * L<set_cxns()|Search::Elasticsearch::Role::CxnPool/"set_cxns()">
+
+=item * L<request_ok()|Search::Elasticsearch::Role::CxnPool/"request_ok()">
+
+=item * L<request_failed()|Search::Elasticsearch::Role::CxnPool/"request_failed()">
+
+=item * L<should_retry()|Search::Elasticsearch::Role::CxnPool/"should_retry()">
+
+=item * L<should_mark_dead()|Search::Elasticsearch::Role::CxnPool/"should_mark_dead()">
+
+=item * L<cxns_str()|Search::Elasticsearch::Role::CxnPool/"cxns_str()">
+
+=item * L<cxns_seeds_str()|Search::Elasticsearch::Role::CxnPool/"cxns_seeds_str()">
+
+=item * L<retries()|Search::Elasticsearch::Role::CxnPool/"retries()">
+
+=item * L<reset_retries()|Search::Elasticsearch::Role::CxnPool/"reset_retries()">
+
+=back
diff --git a/lib/Search/Elasticsearch/CxnPool/Static/NoPing.pm b/lib/Search/Elasticsearch/CxnPool/Static/NoPing.pm
new file mode 100644
index 0000000..b854415
--- /dev/null
+++ b/lib/Search/Elasticsearch/CxnPool/Static/NoPing.pm
@@ -0,0 +1,171 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::CxnPool::Static::NoPing;
+
+use Moo;
+with 'Search::Elasticsearch::Role::CxnPool::Static::NoPing',
+    'Search::Elasticsearch::Role::Is_Sync';
+use Search::Elasticsearch::Util qw(throw);
+use namespace::clean;
+
+1;
+
+__END__
+
+# ABSTRACT: A CxnPool for connecting to a remote cluster without the ability to ping.
+
+=head1 SYNOPSIS
+
+    $e = Search::Elasticsearch->new(
+        cxn_pool => 'Static::NoPing'
+        nodes    => [
+            'search1:9200',
+            'search2:9200'
+        ],
+    );
+
+=head1 DESCRIPTION
+
+The L<Static::NoPing|Search::Elasticsearch::CxnPool::Static::NoPing> connection
+pool (like the L<Static|Search::Elasticsearch::CxnPool::Static> pool) should be used
+when your access to the cluster is limited.  However, the C<Static> pool needs
+to be able to ping nodes in the cluster, with a C<HEAD /> request.  If you
+can't ping your nodes, then you should use the C<Static::NoPing>
+connection pool instead.
+
+Because the cluster cannot be pinged, this CxnPool cannot use a short
+ping request to determine whether nodes are live or not - it just has to
+send requests to the nodes to determine whether they are alive or not.
+
+Most of the time, a dead node will cause the request to fail quickly.
+However, in situations where node failure takes time (eg malfunctioning
+routers or firewalls), a failure may not be reported until the request
+itself times out (see L<Search::Elasticsearch::Cxn/request_timeout>).
+
+Failed nodes will be retried regularly to check if they have recovered.
+
+This class does L<Search::Elasticsearch::Role::CxnPool::Static::NoPing> and
+L<Search::Elasticsearch::Role::Is_Sync>.
+
+=head1 CONFIGURATION
+
+=head2 C<nodes>
+
+The list of nodes to use to serve requests.  Can accept a single node,
+multiple nodes, and defaults to C<localhost:9200> if no C<nodes> are
+specified. See L<Search::Elasticsearch::Role::Cxn/node> for details of the node
+specification.
+
+=head2 See also
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/request_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/dead_timeout>
+
+=item *
+
+L<Search::Elasticsearch::Role::Cxn/max_dead_timeout>
+
+=back
+
+=head2 Inherited configuration
+
+From L<Search::Elasticsearch::Role::CxnPool::Static::NoPing>
+
+=over
+
+=item * L<max_retries|Search::Elasticsearch::Role::CxnPool::Static::NoPing/"max_retries">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<randomize_cxns|Search::Elasticsearch::Role::CxnPool/"randomize_cxns">
+
+=back
+
+=head1 METHODS
+
+=head2 C<next_cxn()>
+
+    $cxn = $cxn_pool->next_cxn
+
+Returns the next available node  in round robin fashion - either a live node
+which has previously responded successfully, or a previously failed
+node which should be retried. If all nodes are dead, it will throw
+a C<NoNodes> error.
+
+=head2 Inherited methods
+
+From L<Search::Elasticsearch::Role::CxnPool::Static::NoPing>
+
+=over
+
+=item * L<should_mark_dead()|Search::Elasticsearch::Role::CxnPool::Static::NoPing/"should_mark_dead()">
+
+=item * L<schedule_check()|Search::Elasticsearch::Role::CxnPool::Static::NoPing/"schedule_check()">
+
+=back
+
+From L<Search::Elasticsearch::Role::CxnPool>
+
+=over
+
+=item * L<cxn_factory()|Search::Elasticsearch::Role::CxnPool/"cxn_factory()">
+
+=item * L<logger()|Search::Elasticsearch::Role::CxnPool/"logger()">
+
+=item * L<serializer()|Search::Elasticsearch::Role::CxnPool/"serializer()">
+
+=item * L<current_cxn_num()|Search::Elasticsearch::Role::CxnPool/"current_cxn_num()">
+
+=item * L<cxns()|Search::Elasticsearch::Role::CxnPool/"cxns()">
+
+=item * L<seed_nodes()|Search::Elasticsearch::Role::CxnPool/"seed_nodes()">
+
+=item * L<next_cxn_num()|Search::Elasticsearch::Role::CxnPool/"next_cxn_num()">
+
+=item * L<set_cxns()|Search::Elasticsearch::Role::CxnPool/"set_cxns()">
+
+=item * L<request_ok()|Search::Elasticsearch::Role::CxnPool/"request_ok()">
+
+=item * L<request_failed()|Search::Elasticsearch::Role::CxnPool/"request_failed()">
+
+=item * L<should_retry()|Search::Elasticsearch::Role::CxnPool/"should_retry()">
+
+=item * L<should_mark_dead()|Search::Elasticsearch::Role::CxnPool/"should_mark_dead()">
+
+=item * L<cxns_str()|Search::Elasticsearch::Role::CxnPool/"cxns_str()">
+
+=item * L<cxns_seeds_str()|Search::Elasticsearch::Role::CxnPool/"cxns_seeds_str()">
+
+=item * L<retries()|Search::Elasticsearch::Role::CxnPool/"retries()">
+
+=item * L<reset_retries()|Search::Elasticsearch::Role::CxnPool/"reset_retries()">
+
+=back
+
+
diff --git a/lib/Search/Elasticsearch/Error.pm b/lib/Search/Elasticsearch/Error.pm
new file mode 100644
index 0000000..885da1d
--- /dev/null
+++ b/lib/Search/Elasticsearch/Error.pm
@@ -0,0 +1,315 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Error;
+
+our $DEBUG = 0;
+
+@Search::Elasticsearch::Error::Internal::ISA     = __PACKAGE__;
+@Search::Elasticsearch::Error::Param::ISA        = __PACKAGE__;
+@Search::Elasticsearch::Error::NoNodes::ISA      = __PACKAGE__;
+@Search::Elasticsearch::Error::ProductCheck::ISA = __PACKAGE__;
+@Search::Elasticsearch::Error::Unauthorized::ISA = __PACKAGE__;
+@Search::Elasticsearch::Error::Forbidden::ISA    = __PACKAGE__;
+@Search::Elasticsearch::Error::Illegal::ISA      = __PACKAGE__;
+@Search::Elasticsearch::Error::Request::ISA      = __PACKAGE__;
+@Search::Elasticsearch::Error::Timeout::ISA      = __PACKAGE__;
+@Search::Elasticsearch::Error::Cxn::ISA          = __PACKAGE__;
+@Search::Elasticsearch::Error::Serializer::ISA   = __PACKAGE__;
+
+@Search::Elasticsearch::Error::Conflict::ISA
+    = ( 'Search::Elasticsearch::Error::Request', __PACKAGE__ );
+
+@Search::Elasticsearch::Error::Missing::ISA
+    = ( 'Search::Elasticsearch::Error::Request', __PACKAGE__ );
+
+@Search::Elasticsearch::Error::RequestTimeout::ISA
+    = ( 'Search::Elasticsearch::Error::Request', __PACKAGE__ );
+
+@Search::Elasticsearch::Error::ContentLength::ISA
+    = ( __PACKAGE__, 'Search::Elasticsearch::Error::Request' );
+
+@Search::Elasticsearch::Error::SSL::ISA
+    = ( __PACKAGE__, 'Search::Elasticsearch::Error::Cxn' );
+
+@Search::Elasticsearch::Error::BadGateway::ISA
+    = ( 'Search::Elasticsearch::Error::Cxn', __PACKAGE__ );
+
+@Search::Elasticsearch::Error::Unavailable::ISA
+    = ( 'Search::Elasticsearch::Error::Cxn', __PACKAGE__ );
+
+@Search::Elasticsearch::Error::GatewayTimeout::ISA
+    = ( 'Search::Elasticsearch::Error::Cxn', __PACKAGE__ );
+
+use overload (
+    '""'  => '_stringify',
+    'cmp' => '_compare',
+);
+
+use Data::Dumper();
+
+#===================================
+sub new {
+#===================================
+    my ( $class, $type, $msg, $vars, $caller ) = @_;
+    return $type if ref $type;
+    $caller ||= 0;
+
+    my $error_class = 'Search::Elasticsearch::Error::' . $type;
+    $msg = 'Unknown error' unless defined $msg;
+
+    local $DEBUG = 2 if $type eq 'Internal';
+
+    my $stack = $class->_stack;
+
+    my $self = bless {
+        type  => $type,
+        text  => $msg,
+        vars  => $vars,
+        stack => $stack,
+    }, $error_class;
+
+    return $self;
+}
+
+#===================================
+sub is {
+#===================================
+    my $self = shift;
+    for (@_) {
+        return 1 if $self->isa("Search::Elasticsearch::Error::$_");
+    }
+    return 0;
+}
+
+#===================================
+sub _stringify {
+#===================================
+    my $self = shift;
+    local $Data::Dumper::Terse  = 1;
+    local $Data::Dumper::Indent = !!$DEBUG;
+
+    unless ( $self->{msg} ) {
+        my $stack  = $self->{stack};
+        my $caller = $stack->[0];
+        $self->{msg} = sprintf( "[%s] ** %s, called from sub %s at %s line %d.",
+            $self->{type}, $self->{text}, @{$caller}[ 3, 1, 2 ] );
+
+        if ( $self->{vars} ) {
+            $self->{msg} .= sprintf( " With vars: %s\n",
+                Data::Dumper::Dumper $self->{vars} );
+        }
+
+        if ( @$stack > 1 ) {
+            $self->{msg}
+                .= sprintf( "Stacktrace:\n%s\n", $self->stacktrace($stack) );
+        }
+    }
+    return $self->{msg};
+
+}
+
+#===================================
+sub _compare {
+#===================================
+    my ( $self, $other, $swap ) = @_;
+    $self .= '';
+    ( $self, $other ) = ( $other, $self ) if $swap;
+    return $self cmp $other;
+}
+
+#===================================
+sub _stack {
+#===================================
+    my $self = shift;
+    my $caller = shift() || 2;
+
+    my @stack;
+    while ( my @caller = caller( ++$caller ) ) {
+        next if $caller[0] eq 'Try::Tiny';
+
+        if ( $caller[3] =~ /^(.+)::__ANON__\[(.+):(\d+)\]$/ ) {
+            @caller = ( $1, $2, $3, '(ANON)' );
+        }
+        elsif ( $caller[1] =~ /^\(eval \d+\)/ ) {
+            $caller[3] = "modified(" . $caller[3] . ")";
+        }
+
+        next
+            if $caller[0] =~ /^Search::Elasticsearch/
+            and ( $DEBUG < 2 or $caller[3] eq 'Try::Tiny::try' );
+        push @stack, [ @caller[ 0, 1, 2, 3 ] ];
+        last unless $DEBUG > 1;
+    }
+    return \@stack;
+}
+
+#===================================
+sub stacktrace {
+#===================================
+    my $self = shift;
+    my $stack = shift || $self->_stack();
+
+    my $o = sprintf "%s\n%-4s %-50s %-5s %s\n%s\n",
+        '-' x 80, '#', 'Package', 'Line', 'Sub-routine', '-' x 80;
+
+    my $i = 1;
+    for (@$stack) {
+        $o .= sprintf "%-4d %-50s %4d  %s\n", $i++, @{$_}[ 0, 2, 3 ];
+    }
+
+    return $o .= ( '-' x 80 ) . "\n";
+}
+
+#===================================
+sub TO_JSON {
+#===================================
+    my $self = shift;
+    return $self->_stringify;
+}
+1;
+
+# ABSTRACT: Errors thrown by Search::Elasticsearch
+
+=head1 DESCRIPTION
+
+Errors thrown by Search::Elasticsearch are error objects, which can include
+a stack trace and information to help debug problems. An error object
+consists of the following:
+
+    {
+        type  => $type,              # eg Missing
+        text  => 'Error message',
+        vars  => {...},              # vars which may help to explain the error
+        stack => [...],              # a stack trace
+    }
+
+The C<$Search::Elasticsearch::Error::DEBUG> variable can be set to C<1> or C<2>
+to increase the verbosity of errors.
+
+Error objects stringify to a human readable error message when used in text
+context (for example: C<print 'Oh no! '.$error>).  They also support the C<TO_JSON>
+method to support conversion to JSON when L<JSON/convert_blessed> is enabled.
+
+=head1 ERROR CLASSES
+
+The following error classes are defined:
+
+=over
+
+=item * C<Search::Elasticsearch::Error::Param>
+
+A bad parameter has been passed to a method.
+
+=item * C<Search::Elasticsearch::Error::Request>
+
+There was some generic error performing your request in Elasticsearch.
+This error is triggered by HTTP status codes C<400> and C<500>. This class
+has the following sub-classes:
+
+=over
+
+=item * C<Search::Elasticsearch::Error::Unauthorized>
+
+Invalid (or no) username/password provided as C<userinfo> for a password
+protected service. These errors are triggered by the C<401> HTTP status code.
+
+=item * C<Search::Elasticsearch::Error::Missing>
+
+A resource that you requested was not found.  These errors are triggered
+by the C<404> HTTP status code.
+
+=item * C<Elastisearch::Error::Conflict>
+
+Your request could not be performed because of some conflict.  For instance,
+if you try to delete a document with a particular version number, and the
+document has already changed, it will throw a C<Conflict> error.  If it can,
+it will include the C<current_version> in the error vars. This error
+is triggered by the C<409> HTTP status code.
+
+=item * C<Search::Elasticsearch::Error::ContentLength>
+
+The request body was longer than the
+L<max_content_length|Search::Elasticsearch::Role::Cxn/max_content_length>.
+
+=item * C<Search::Elasticsearch::Error::RequestTimeout>
+
+The request took longer than the specified C<timeout>.  Currently only
+applies to the
+L<cluster_health|Search::Elasticsearch::Client::6_0::Direct::Cluster/cluster_health()>
+request.
+
+=back
+
+=item * C<Search::Elasticsearch::Error::Timeout>
+
+The request timed out.
+
+=item * C<Search::Elasticsearch::Error::Cxn>
+
+There was an error connecting to a node in the cluster.  This error
+indicates node failure and will be retried on another node.
+This error has the following sub-classes:
+
+=over
+
+=item * C<Search::Elasticsearch::Error::Unavailable>
+
+The current node is unable to handle your request at the moment. Your
+request will be retried on another node.  This error is triggered by
+the C<503> HTTP status code.
+
+=item * C<Search::Elasticsearch::Error::BadGateway>
+
+A proxy between the client and Elasticsearch is unable to connect to Elasticsearch.
+This error is triggered by the C<502> HTTP status code.
+
+=item * C<Search::Elasticsearch::Error::GatewayTimeout>
+
+A proxy between the client and Elasticsearch is unable to connect to Elasticsearch
+within its own timeout. This error is triggered by the C<504> HTTP status code.
+
+=item * C<Search::Elasticsearch::Error::SSL>
+
+There was a problem validating the SSL certificate.  Not all
+backends support this error type.
+
+=back
+
+=item * C<Search::Elasticsearch::Error::Forbidden>
+
+Either the cluster was unable to process the request because it is currently
+blocking, eg there are not enough master nodes to form a cluster, or
+because the authenticated user is trying to perform an unauthorized
+action. This error is triggered by the C<403> HTTP status code.
+
+=item * C<Search::Elasticsearch::Error::Illegal>
+
+You have attempted to perform an illegal operation.
+For instance, you attempted to use a Scroll helper in a different process
+after forking.
+
+=item * C<Search::Elasticsearch::Error::Serializer>
+
+There was an error serializing a variable or deserializing a string.
+
+=item * C<Elasticsarch::Error::Internal>
+
+An internal error occurred - please report this as a bug in
+this module.
+
+=back
diff --git a/lib/Search/Elasticsearch/Logger/LogAny.pm b/lib/Search/Elasticsearch/Logger/LogAny.pm
new file mode 100644
index 0000000..f9b6aeb
--- /dev/null
+++ b/lib/Search/Elasticsearch/Logger/LogAny.pm
@@ -0,0 +1,137 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Logger::LogAny;
+
+use Moo;
+with 'Search::Elasticsearch::Role::Logger';
+use Search::Elasticsearch::Util qw(parse_params to_list);
+use namespace::clean;
+
+use Log::Any 1.02 ();
+use Log::Any::Adapter();
+
+#===================================
+sub _build_log_handle {
+#===================================
+    my $self = shift;
+    if ( my @args = to_list( $self->log_to ) ) {
+        Log::Any::Adapter->set( { category => $self->log_as }, @args );
+    }
+    Log::Any->get_logger( category => $self->log_as );
+}
+
+#===================================
+sub _build_trace_handle {
+#===================================
+    my $self = shift;
+    if ( my @args = to_list( $self->trace_to ) ) {
+        Log::Any::Adapter->set( { category => $self->trace_as }, @args );
+    }
+    Log::Any->get_logger( category => $self->trace_as );
+}
+
+#===================================
+sub _build_deprecate_handle {
+#===================================
+    my $self = shift;
+    if ( my @args = to_list( $self->deprecate_to ) ) {
+        Log::Any::Adapter->set( { category => $self->deprecate_as }, @args );
+    }
+    Log::Any->get_logger(
+        default_adapter => 'Stderr',
+        category        => $self->deprecate_as
+    );
+}
+
+1;
+
+# ABSTRACT: A Log::Any-based Logger implementation
+
+=head1 DESCRIPTION
+
+L<Search::Elasticsearch::Logger::LogAny> provides event logging and the tracing
+of request/response conversations with Elasticsearch nodes via the
+L<Log::Any> module.
+
+I<Logging> refers to log events, such as node failures, pings, sniffs, etc,
+and should be enabled for monitoring purposes.
+
+I<Tracing> refers to the actual HTTP requests and responses sent
+to Elasticsearch nodes.  Tracing can be enabled for debugging purposes,
+or for generating a pretty-printed C<curl> script which can be used for
+reporting problems.
+
+I<Deprecations> refers to deprecation warnings returned by Elasticsearch
+5.x and above. Deprecations are logged to STDERR by default.
+
+=head1 CONFIGURATION
+
+Logging and tracing can be enabled using L<Log::Any::Adapter>, or by
+passing options to L<Search::Elasticsearch/new()>.
+
+=head2 USING LOG::ANY::ADAPTER
+
+Send all logging and tracing to C<STDERR>:
+
+    use Log::Any::Adapter qw(Stderr);
+    use Search::Elasticsearch;
+    my $e = Search::Elasticsearch->new;
+
+Send logging and deprecations to a file, and tracing to Stderr:
+
+    use Log::Any::Adapter();
+    Log::Any::Adapter->set(
+        { category => 'elasticsearch.event' },
+        'File',
+        '/path/to/file.log'
+    );
+    Log::Any::Adapter->set(
+        { category => 'elasticsearch.trace' },
+        'Stderr'
+    );
+    Log::Any::Adapter->set(
+        { category => 'elasticsearch.deprecation' },
+        'File',
+        '/path/to/deprecations.log'
+    );
+
+    use Search::Elasticsearch;
+    my $e = Search::Elasticsearch->new;
+
+=head2 USING C<log_to>, C<trace_to> AND C<deprecate_to>
+
+Send all logging and tracing to C<STDERR>:
+
+    use Search::Elasticsearch;
+    my $e = Search::Elasticsearch->new(
+        log_to   => 'Stderr',
+        trace_to => 'Stderr',
+        deprecate_to => 'Stderr'  # default
+    );
+
+Send logging and deprecations to a file, and tracing to Stderr:
+
+    use Search::Elasticsearch;
+    my $e = Search::Elasticsearch->new(
+        log_to       => ['File', '/path/to/file.log'],
+        trace_to     => 'Stderr',
+        deprecate_to => ['File', '/path/to/deprecations.log'],
+    );
+
+See L<Log::Any::Adapter> for more.
+
diff --git a/lib/Search/Elasticsearch/Role/API.pm b/lib/Search/Elasticsearch/Role/API.pm
new file mode 100644
index 0000000..d659de1
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/API.pm
@@ -0,0 +1,124 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::API;
+
+use Moo::Role;
+requires 'api_version';
+requires 'api';
+
+use Scalar::Util qw(looks_like_number);
+use Search::Elasticsearch::Util qw(throw);
+use namespace::clean;
+
+our %Handler = (
+    string  => \&_string,
+    time    => \&_string,
+    date    => \&_string,
+    list    => \&_list,
+    boolean => \&_bool,
+    enum    => \&_list,
+    number  => \&_num,
+    int     => \&_num,
+    integer => \&_num,
+    float   => \&_num,
+    double  => \&_num,
+    "number|string" => \&_numOrString,
+    "boolean|long"  => \&_booleanOrLong
+);
+
+#===================================
+sub _bool {
+#===================================
+    my $val = _detect_bool(@_);
+    return ( $val && $val ne 'false' ) ? 'true' : 'false';
+}
+
+#===================================
+sub _detect_bool {
+#===================================
+    my $val = shift;
+    return '' unless defined $val;
+    if ( ref $val eq 'SCALAR' ) {
+        return 'false' if $$val eq 0;
+        return 'true'  if $$val eq 1;
+    }
+    elsif ( UNIVERSAL::isa( $val, "JSON::PP::Boolean" ) ) {
+        return "$val" ? 'true' : 'false';
+    }
+    return "$val";
+}
+
+#===================================
+sub _list {
+#===================================
+    return join ",", map { _detect_bool($_) }    #
+        ref $_[0] eq 'ARRAY' ? @{ $_[0] } : $_[0];
+}
+
+#===================================
+sub _num {
+#===================================
+    return 0 + $_[0];
+}
+
+#===================================
+sub _string {
+#===================================
+    return "$_[0]";
+}
+
+#===================================
+sub _numOrString {
+#===================================
+    if (looks_like_number($_[0])) {
+        return _num($_[0]);
+    }
+    return _string($_[0]);
+}
+
+#===================================
+sub _booleanOrLong {
+#===================================
+    if (looks_like_number($_[0])) {
+        return _num($_[0]);
+    }
+    my $val = _detect_bool(@_);
+    return ( $val && $val ne 'false' ) ? 'true' : 'false';
+}
+
+#===================================
+sub _qs_init {
+#===================================
+    my $class = shift;
+    my $API   = shift;
+    for my $spec ( keys %$API ) {
+        my $qs = $API->{$spec}{qs};
+        for my $param ( keys %$qs ) {
+            my $handler = $Handler{ $qs->{$param} }
+                or throw( "Internal",
+                      "Unknown type <"
+                    . $qs->{$param}
+                    . "> for param <$param> in API <$spec>" );
+            $qs->{$param} = $handler;
+        }
+    }
+}
+
+1;
+
+# ABSTRACT: Provides common functionality for API implementations
diff --git a/lib/Search/Elasticsearch/Role/Client.pm b/lib/Search/Elasticsearch/Role/Client.pm
new file mode 100644
index 0000000..4eb7b68
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Client.pm
@@ -0,0 +1,41 @@
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Role::Client;
+
+use Moo::Role;
+use namespace::clean;
+
+requires 'parse_request';
+
+has 'transport' => ( is => 'ro', required => 1 );
+has 'logger'    => ( is => 'ro', required => 1 );
+
+#===================================
+sub perform_request {
+#===================================
+    my $self    = shift;
+    my $request = $self->parse_request(@_);
+    return $self->transport->perform_request($request);
+}
+
+1;
+
+__END__
+
+# ABSTRACT: Provides common functionality for Client implementations
+
+=head1 DESCRIPTION
+
+This role provides a common C<perform_request()> method for Client
+implementations.
+
+=head1 METHODS
+
+=head2 C<perform_request()>
+
+This method takes whatever arguments it is passed and passes them directly to
+a C<parse_request()> method (which should be provided by Client implementations).
+The C<parse_request()> method should return a request suitable for passing
+to L<Search::Elasticsearch::Transport/perform_request()>.
diff --git a/lib/Search/Elasticsearch/Role/Client/Direct.pm b/lib/Search/Elasticsearch/Role/Client/Direct.pm
new file mode 100644
index 0000000..141e5c6
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Client/Direct.pm
@@ -0,0 +1,212 @@
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Role::Client::Direct;
+
+use Moo::Role;
+with 'Search::Elasticsearch::Role::Client';
+use Search::Elasticsearch::Util qw(load_plugin is_compat throw);
+
+use Try::Tiny;
+use Package::Stash 0.34 ();
+use Any::URI::Escape qw(uri_escape);
+use namespace::clean;
+
+#===================================
+sub parse_request {
+#===================================
+    my $self   = shift;
+    my $defn   = shift || {};
+    my $params = { ref $_[0] ? %{ shift() } : @_ };
+
+    my $request;
+    try {
+        $request = {
+            ignore    => delete $params->{ignore} || [],
+            method    => $defn->{method}          || 'GET',
+            serialize => $defn->{serialize}       || 'std',
+            path => $self->_parse_path( $defn,         $params ),
+            body => $self->_parse_body( $defn->{body}, $params ),
+            qs   => $self->_parse_qs( $defn->{qs},     $params ),
+        };
+    }
+    catch {
+        chomp $_;
+        my $name = $defn->{name} || '<unknown method>';
+        $self->logger->throw_error( 'Param', "$_ in ($name) request. " );
+    };
+    return $request;
+}
+
+#===================================
+sub _parse_path {
+#===================================
+    my ( $self, $defn, $params ) = @_;
+    return delete $params->{path}
+        if $params->{path};
+    my $paths = $defn->{paths};
+    my $parts = $defn->{parts};
+
+    my %args;
+    keys %$parts;
+    no warnings 'uninitialized';
+    while ( my ( $key, $req ) = each %$parts ) {
+        my $val = delete $params->{$key};
+        if ( ref $val eq 'ARRAY' ) {
+            die "Param ($key) must contain a single value\n"
+                if @$val > 1 and not $req->{multi};
+            $val = join ",", @$val;
+        }
+        if ( !length $val ) {
+            die "Missing required param ($key)\n"
+                if $req->{required};
+            next;
+        }
+        utf8::encode($val);
+        $args{$key} = uri_escape($val);
+    }
+PATH: for my $path (@$paths) {
+        my @keys = keys %{ $path->[0] };
+        next PATH unless @keys == keys %args;
+        for (@keys) {
+            next PATH unless exists $args{$_};
+        }
+        my ( $pos, @parts ) = @$path;
+        for ( keys %$pos ) {
+            $parts[ $pos->{$_} ] = $args{$_};
+        }
+        return join "/", '', @parts;
+    }
+
+    throw(
+        'Internal',
+        "Couldn't determine path",
+        { params => $params, defn => $defn }
+    );
+}
+
+#===================================
+sub _parse_body {
+#===================================
+    my ( $self, $defn, $params ) = @_;
+    if ( defined $defn ) {
+        die("Missing required param (body)\n")
+            if $defn->{required} && !$params->{body};
+        return delete $params->{body};
+    }
+    die("Unknown param (body)\n") if $params->{body};
+    return undef;
+}
+
+#===================================
+sub _parse_qs {
+#===================================
+    my ( $self, $handlers, $params ) = @_;
+    die "No (qs) defined\n" unless $handlers;
+    my %qs;
+
+    if ( my $raw = delete $params->{params} ) {
+        die("Arg (params) shoud be a hashref\n")
+            unless ref $raw eq 'HASH';
+        %qs = %$raw;
+    }
+
+    for my $key ( keys %$params ) {
+        my $handler = $handlers->{$key}
+            or die("Unknown param ($key)\n");
+        $qs{$key} = $handler->( delete $params->{$key} );
+    }
+    return \%qs;
+}
+
+#===================================
+sub _install_api {
+#===================================
+    my ( $class, $group ) = @_;
+    my $defns = $class->api;
+    my $stash = Package::Stash->new($class);
+
+    my $group_qr = $group ? qr/$group\./ : qr//;
+    for my $action ( keys %$defns ) {
+        my ($name) = ( $action =~ /^$group_qr([^.]+)$/ )
+            or next;
+        next if $stash->has_symbol( '&' . $name );
+
+        my %defn = ( name => $name, %{ $defns->{$action} } );
+        $stash->add_symbol(
+            '&' . $name => sub {
+                shift->perform_request( \%defn, @_ );
+            }
+        );
+    }
+}
+
+#===================================
+sub _build_namespace {
+#===================================
+    my ( $self, $ns ) = @_;
+    my $class = load_plugin( $self->_namespace, [$ns] );
+    return $class->new(
+        {   transport => $self->transport,
+            logger    => $self->logger
+        }
+    );
+}
+
+#===================================
+sub _build_helper {
+#===================================
+    my ( $self, $name, $sub_class ) = @_;
+    my $class = load_plugin( 'Search::Elasticsearch', $sub_class );
+    is_compat( $name . '_helper_class', $self->transport, $class );
+    return $class;
+}
+
+1;
+
+# ABSTRACT: Request parsing for Direct clients
+
+=head1 DESCRIPTION
+
+This role provides the single C<parse_request()> method for classes
+which need to parse an API definition from L<Search::Elasticsearch::Role::API>
+and convert it into a request which can be passed to
+L<Search::Elasticsearch::Transport/perform_request()>.
+
+=head1 METHODS
+
+=head2 C<perform_request()>
+
+    $request = $client->parse_request(\%defn,\%params);
+
+The C<%defn> is a definition returned by L<Search::Elasticsearch::Role::API/api()>
+with an extra key C<name> which should be the name of the method that
+was called on the client.  For instance if the user calls C<< $client->search >>,
+then the C<name> should be C<"search">.
+
+C<parse_request()> will turn the parameters that have been passed in into
+a C<path> (via L<Search::Elasticsearch::Util::API::Path/path_init()>), a query-string
+hash (via L<Search::Elasticsearch::Util::API::QS/qs_init>) and will through a
+C<body> value directly.
+
+B<NOTE:> If a C<path> key is specified in the C<%params> then it will be used
+directly, instead of trying to build path from the path template.  Similarly,
+if a C<params> key is specified in the C<%params>, then it will be used
+as a basis for the query string hash.  For instance:
+
+    $client->perform_request(
+        {
+            method => 'GET',
+            name   => 'new_method'
+        },
+        {
+            path   => '/new/method',
+            params => { foo => 'bar' },
+            body   => \%body
+        }
+    );
+
+This makes it easy to add support for custom plugins or new functionality
+not yet supported by the released client.
+
diff --git a/lib/Search/Elasticsearch/Role/Cxn.pm b/lib/Search/Elasticsearch/Role/Cxn.pm
new file mode 100644
index 0000000..706b9de
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Cxn.pm
@@ -0,0 +1,842 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::Cxn;
+
+our $PRODUCT_CHECK_HEADER = 'x-elastic-product';
+our $PRODUCT_CHECK_VALUE = 'Elasticsearch';
+
+use Moo::Role;
+use Search::Elasticsearch::Util qw(parse_params throw to_list);
+use List::Util qw(min);
+use Try::Tiny;
+use URI();
+use IO::Compress::Deflate();
+use IO::Uncompress::Inflate();
+use IO::Compress::Gzip();
+use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
+use Search::Elasticsearch::Util qw(to_list);
+use namespace::clean;
+use Net::IP;
+
+requires qw(perform_request error_from_text handle);
+
+has 'host'                  => ( is => 'ro', required => 1 );
+has 'port'                  => ( is => 'ro', required => 1 );
+has 'uri'                   => ( is => 'ro', required => 1 );
+has 'request_timeout'       => ( is => 'ro', default  => 30 );
+has 'ping_timeout'          => ( is => 'ro', default  => 2 );
+has 'sniff_timeout'         => ( is => 'ro', default  => 1 );
+has 'sniff_request_timeout' => ( is => 'ro', default  => 2 );
+has 'next_ping'             => ( is => 'rw', default  => 0 );
+has 'ping_failures'         => ( is => 'rw', default  => 0 );
+has 'dead_timeout'          => ( is => 'ro', default  => 60 );
+has 'max_dead_timeout'      => ( is => 'ro', default  => 3600 );
+has 'serializer'            => ( is => 'ro', required => 1 );
+has 'logger'                => ( is => 'ro', required => 1 );
+has 'handle_args'           => ( is => 'ro', default  => sub { {} } );
+has 'default_qs_params'     => ( is => 'ro', default  => sub { {} } );
+has 'scheme'             => ( is => 'ro' );
+has 'is_https'           => ( is => 'ro' );
+has 'userinfo'           => ( is => 'ro' );
+has 'max_content_length' => ( is => 'ro' );
+has 'default_headers'    => ( is => 'ro' );
+has 'deflate'            => ( is => 'ro' );
+has 'gzip'               => ( is => 'ro' );
+has 'ssl_options'        => ( is => 'ro', predicate => 'has_ssl_options' );
+has 'handle'             => ( is => 'lazy', clearer => 1 );
+has '_pid'               => ( is => 'rw', default => $$ );
+
+my %Code_To_Error = (
+    400 => 'Request',
+    401 => 'Unauthorized',
+    403 => 'Forbidden',
+    404 => 'Missing',
+    408 => 'RequestTimeout',
+    409 => 'Conflict',
+    413 => 'ContentLength',
+    502 => 'BadGateway',
+    503 => 'Unavailable',
+    504 => 'GatewayTimeout'
+);
+
+#===================================
+sub stringify { shift->uri . '' }
+#===================================
+
+#===================================
+sub get_user_agent {
+#===================================
+    return sprintf("elasticsearch-perl/%s (%s; perl %s)", $Search::Elasticsearch::VERSION, $^O, $]);
+}
+
+#===================================
+sub get_meta_header {
+#===================================
+    return sprintf("es=%s,pl=%s", $Search::Elasticsearch::VERSION, $^V);
+}
+
+
+#===================================
+sub BUILDARGS {
+#===================================
+    my ( $class, $params ) = parse_params(@_);
+
+    my $node = $params->{node}
+        || { host => 'localhost', port => '9200' };
+
+    unless ( ref $node eq 'HASH' ) {
+        $node = "[$node]" if Net::IP::ip_is_ipv6($node);
+        unless ( $node =~ m{^http(s)?://} ) {
+            $node = ( $params->{use_https} ? 'https://' : 'http://' ) . $node;
+        }
+        if ( $params->{port} && $node !~ m{//[^/\[]+:\d+} ) {
+            $node =~ s{(//[^/]+)}{$1:$params->{port}};
+        }
+        my $uri = URI->new($node);
+        $node = {
+            scheme   => $uri->scheme,
+            host     => $uri->host,
+            port     => $uri->port,
+            path     => $uri->path,
+            userinfo => $uri->userinfo
+        };
+    }
+
+    my $host = $node->{host} || 'localhost';
+    my $userinfo = $node->{userinfo} || $params->{userinfo} || '';
+    my $scheme
+        = $node->{scheme} || ( $params->{use_https} ? 'https' : 'http' );
+    my $port
+        = $node->{port}
+        || $params->{port}
+        || ( $scheme eq 'http' ? 80 : 443 );
+    my $path = $node->{path} || $params->{path_prefix} || '';
+    $path =~ s{^/?}{/}g;
+    $path =~ s{/+$}{};
+
+    my %default_headers = %{ $params->{default_headers} || {} };
+
+    if ($userinfo) {
+        require MIME::Base64;
+        my $auth = MIME::Base64::encode_base64( $userinfo, "" );
+        chomp $auth;
+        $default_headers{Authorization} = "Basic $auth";
+    }
+
+    if ( $params->{gzip} ) {
+        $default_headers{'Accept-Encoding'} = "gzip";
+    }
+
+    elsif ( $params->{deflate} ) {
+        $default_headers{'Accept-Encoding'} = "deflate";
+    }
+
+    $default_headers{'User-Agent'} = $class->get_user_agent();
+
+    # Add Elastic meta header
+    $default_headers{'x-elastic-client-meta'} = $class->get_meta_header();
+
+    # Compatibility header
+    if (defined $ENV{ELASTIC_CLIENT_APIVERSIONING} &&
+        (lc($ENV{ELASTIC_CLIENT_APIVERSIONING}) eq 'true' || $ENV{ELASTIC_CLIENT_APIVERSIONING} eq '1')) {
+            $default_headers{'Accept'} = 'application/vnd.elasticsearch+json;compatible-with=7';
+            $default_headers{'Content-Type'} = 'application/vnd.elasticsearch+json; compatible-with=7';
+    }
+
+    if (defined $params->{elastic_cloud_api_key} && defined $params->{token_api}) {
+        throw( 'Request',
+            "You cannot set elastic_cloud_api_key and token_api together" );
+    }
+
+    # Elastic cloud API key
+    if (defined $params->{elastic_cloud_api_key}) {
+        $default_headers{'Authorization'} = sprintf("ApiKey %s", $params->{elastic_cloud_api_key});
+    }
+
+    # Elasticsearch token API (https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-token.html)
+    if (defined $params->{token_api}) {
+        $default_headers{'Authorization'} = sprintf("Bearer %s", $params->{token_api});
+    }
+
+    # Elasticsearch
+    $params->{scheme}   = $scheme;
+    $params->{is_https} = $scheme eq 'https';
+    $params->{host}     = $host;
+    $params->{port}     = $port;
+    $params->{path}     = $path;
+    $params->{userinfo} = $userinfo;
+    $host = "[$host]" if Net::IP::ip_is_ipv6($host);
+    $params->{uri}             = URI->new("$scheme://$host:$port$path");
+    $params->{default_headers} = \%default_headers;
+
+    return $params;
+}
+
+#===================================
+before 'handle' => sub {
+#===================================
+    my $self = shift;
+    if ( $$ != $self->_pid ) {
+        $self->clear_handle;
+        $self->_pid($$);
+    }
+};
+
+#===================================
+sub is_live { !shift->next_ping }
+sub is_dead { !!shift->next_ping }
+#===================================
+
+#===================================
+sub mark_live {
+#===================================
+    my $self = shift;
+    $self->ping_failures(0);
+    $self->next_ping(0);
+}
+
+#===================================
+sub mark_dead {
+#===================================
+    my $self  = shift;
+    my $fails = $self->ping_failures;
+    $self->ping_failures( $fails + 1 );
+
+    my $timeout
+        = min( $self->dead_timeout * 2**$fails, $self->max_dead_timeout );
+    my $next = $self->next_ping( time() + $timeout );
+
+    $self->logger->infof( 'Marking [%s] as dead. Next ping at: %s',
+        $self->stringify, scalar localtime($next) );
+
+}
+
+#===================================
+sub force_ping {
+#===================================
+    my $self = shift;
+    $self->ping_failures(0);
+    $self->next_ping(-1);
+}
+
+#===================================
+sub pings_ok {
+#===================================
+    my $self = shift;
+    $self->logger->infof( 'Pinging [%s]', $self->stringify );
+    return try {
+        $self->perform_request(
+            {   method  => 'HEAD',
+                path    => '/',
+                timeout => $self->ping_timeout,
+            }
+        );
+        $self->logger->infof( 'Marking [%s] as live', $self->stringify );
+        $self->mark_live;
+        1;
+    }
+    catch {
+        $self->logger->debug("$_");
+        $self->mark_dead;
+        0;
+    };
+}
+
+#===================================
+sub sniff {
+#===================================
+    my $self = shift;
+    $self->logger->infof( 'Sniffing [%s]', $self->stringify );
+    return try {
+        $self->perform_request(
+            {   method  => 'GET',
+                path    => '/_nodes/http',
+                qs      => { timeout => $self->sniff_timeout . 's' },
+                timeout => $self->sniff_request_timeout,
+            }
+        )->{nodes};
+    }
+    catch {
+        $self->logger->debug($_);
+        return;
+    };
+}
+
+#===================================
+sub build_uri {
+#===================================
+    my ( $self, $params ) = @_;
+    my $uri = $self->uri->clone;
+    $uri->path( $uri->path . $params->{path} );
+    my %qs = ( %{ $self->default_qs_params }, %{ $params->{qs} || {} } );
+    $uri->query_form( \%qs );
+    return $uri;
+}
+
+#===================================
+before 'perform_request' => sub {
+#===================================
+    my ( $self, $params ) = @_;
+    return unless defined $params->{data};
+
+    $self->_compress_body($params);
+
+    my $max = $self->max_content_length
+        or return;
+
+    return if length( $params->{data} ) < $max;
+
+    $self->logger->throw_error( 'ContentLength',
+        "Body is longer than max_content_length ($max)",
+    );
+};
+
+#===================================
+sub _compress_body {
+#===================================
+    my ( $self, $params ) = @_;
+    my $output;
+    if ( $self->gzip ) {
+        IO::Compress::Gzip::gzip( \( $params->{data} ), \$output )
+            or throw( 'Request',
+            "Couldn't gzip request: $IO::Compress::Gzip::GzipError" );
+        $params->{data}     = $output;
+        $params->{encoding} = 'gzip';
+    }
+    elsif ( $self->deflate ) {
+        IO::Compress::Deflate::deflate( \( $params->{data} ), \$output )
+            or throw( 'Request',
+            "Couldn't deflate request: $IO::Compress::Deflate::DeflateError" );
+        $params->{data}     = $output;
+        $params->{encoding} = 'deflate';
+    }
+}
+
+#===================================
+sub _decompress_body {
+#===================================
+    my ( $self, $body_ref, $headers ) = @_;
+    if ( my $encoding = $headers->{'content-encoding'} ) {
+        my $output;
+        if ( $encoding eq 'gzip' ) {
+            IO::Uncompress::Gunzip::gunzip( $body_ref, \$output )
+                or throw(
+                'Request',
+                "Couldn't gunzip response: $IO::Uncompress::Gunzip::GunzipError"
+                );
+        }
+        elsif ( $encoding eq 'deflate' ) {
+            IO::Uncompress::Inflate::inflate( $body_ref, \$output,
+                Transparent => 0 )
+                or throw(
+                'Request',
+                "Couldn't inflate response: $IO::Uncompress::Inflate::InflateError"
+                );
+        }
+        else {
+            throw( 'Request', "Unknown content-encoding: $encoding" );
+        }
+        ${$body_ref} = $output;
+    }
+}
+
+#===================================
+sub process_response {
+#===================================
+    my ( $self, $params, $code, $msg, $body, $headers ) = @_;
+
+    # Product check
+    if ( $code >= 200 and $code < 300 ) {
+        my $product = $headers->{$PRODUCT_CHECK_HEADER} // '';
+        if ($product ne $PRODUCT_CHECK_VALUE) {
+            throw( "ProductCheck", "The client noticed that the server is not Elasticsearch and we do not support this unknown product" );
+        }
+    }
+
+    $self->_decompress_body( \$body, $headers );
+
+    my ($mime_type) = split /\s*;\s*/, ( $headers->{'content-type'} || '' );
+
+    my $is_encoded = $mime_type && $mime_type ne 'text/plain';
+
+    # Deprecation warnings
+    if ( my $warnings = $headers->{warning} ) {
+        my $warning_string = _parse_warnings($warnings);
+        my %temp           = (%$params);
+        delete $temp{data};
+        $self->logger->deprecation( $warning_string, \%temp );
+    }
+
+    # Request is successful
+    if ( $code >= 200 and $code <= 209 ) {
+        if ( defined $body and length $body ) {
+            $body = $self->serializer->decode($body)
+                if $is_encoded;
+            return $code, $body;
+        }
+        return ( $code, 1 ) if $params->{method} eq 'HEAD';
+        return ( $code, '' );
+    }
+
+    # Check if the error should be ignored
+    my @ignore = to_list( $params->{ignore} );
+    push @ignore, 404 if $params->{method} eq 'HEAD';
+    return ($code) if grep { $_ eq $code } @ignore;
+
+    # Determine error type
+    my $error_type = $Code_To_Error{$code};
+    unless ($error_type) {
+        if ( defined $body and length $body ) {
+            $msg  = $body;
+            $body = undef;
+        }
+        $error_type = $self->error_from_text( $code, $msg );
+    }
+
+    delete $params->{data} if $params->{body};
+    my %error_args = ( status_code => $code, request => $params );
+
+    # Extract error message from the body, if present
+
+    if ( $body = $self->serializer->decode($body) ) {
+        $error_args{body} = $body;
+        $msg = $self->_munge_elasticsearch_exception($body) || $msg;
+
+        $error_args{current_version} = $1
+            if $error_type eq 'Conflict'
+            and $msg =~ /: version conflict, current (?:version )?\[(\d+)\]/;
+    }
+    $msg ||= $error_type;
+
+    chomp $msg;
+    throw( $error_type, "[" . $self->stringify . "]-[$code] $msg",
+        \%error_args );
+}
+
+#===================================
+sub _parse_warnings {
+#===================================
+    my @warnings = ref $_[0] eq 'ARRAY' ? @{ shift() } : shift();
+    my @str;
+    for (@warnings) {
+        if ( $_ =~ /^\d+\s+\S+\s+"((?:\\"|[^"])+)"/ ) {
+            my $msg = $1;
+            $msg =~ s/\\"/"/g, push @str, $msg;
+        }
+        else {
+            push @str, $_;
+        }
+    }
+    return join "; ", @str;
+}
+
+#===================================
+sub _munge_elasticsearch_exception {
+#===================================
+    my ( $self, $body ) = @_;
+    return $body unless ref $body eq 'HASH';
+    my $error = $body->{error} || return;
+    return $error unless ref $error eq 'HASH';
+
+    my $root_causes = $error->{root_cause} || [];
+    unless (@$root_causes) {
+        my $msg = "[" . $error->{type} . "] " if $error->{type};
+        $msg .= $error->{reason} if $error->{reason};
+        return $msg;
+    }
+
+    my $json = $self->serializer;
+    my @msgs;
+    for (@$root_causes) {
+        my %cause = (%$_);
+        my $msg
+            = "[" . ( delete $cause{type} ) . "] " . ( delete $cause{reason} );
+        if ( keys %cause ) {
+            $msg .= ", with: " . $json->encode( \%cause );
+        }
+        push @msgs, $msg;
+    }
+    return ( join ", ", @msgs );
+}
+
+1;
+
+# ABSTRACT: Provides common functionality to HTTP Cxn implementations
+
+=head1 DESCRIPTION
+
+L<Search::Elasticsearch::Role::Cxn> provides common functionality to Cxn
+implementations. Cxn instances are created by a
+L<Search::Elasticsearch::Role::CxnPool> implementation, using the
+L<Search::Elasticsearch::Cxn::Factory> class.
+
+=head1 CONFIGURATION
+
+The configuration options are as follows:
+
+=head2 C<node>
+
+A single C<node> is passed to C<new()> by the L<Search::Elasticsearch::Cxn::Factory>
+class.  It can either be a URI or a hash containing each part.  For instance:
+
+    node => 'localhost';                    # equiv of 'http://localhost:80'
+    node => 'localhost:9200';               # equiv of 'http://localhost:9200'
+    node => 'http://localhost:9200';
+
+    node => 'https://localhost';            # equiv of 'https://localhost:443'
+    node => 'localhost/path';               # equiv of 'http://localhost:80/path'
+
+
+    node => 'http://user:pass@localhost';   # equiv of 'http://localhost:80'
+                                            # with userinfo => 'user:pass'
+
+Alternatively, a C<node> can be specified as a hash:
+
+    {
+        scheme      => 'http',
+        host        => 'search.domain.com',
+        port        => '9200',
+        path        => '/path',
+        userinfo    => 'user:pass'
+    }
+
+Similarly, default values can be specified with C<port>, C<path_prefix>,
+C<userinfo> and C<use_https>:
+
+    $e = Search::Elasticsearch->new(
+        port        => 9201,
+        path_prefix => '/path',
+        userinfo    => 'user:pass',
+        use_https   => 1,
+        nodes       => [ 'search1', 'search2' ]
+    )
+
+=head2 C<ssl_options>
+
+By default, all backends that support HTTPS disable verification of
+the host they are connecting to.  Use C<ssl_options> to configure
+the type of verification that you would like the client to perform,
+or to configure the client to present its own certificate.
+
+The values accepted by C<ssl_options> depend on the C<Cxn> class.  See the
+documentation for the C<Cxn> class that you are using.
+
+=head2 C<max_content_length>
+
+By default, Elasticsearch nodes accept a maximum post body of 100MB or
+C<104_857_600> bytes. This client enforces that limit.  The limit can
+be customised with the C<max_content_length> parameter (specified in bytes).
+
+If you're using the L<Search::Elasticsearch::CxnPool::Sniff> module, then the
+C<max_content_length> will be automatically retrieved from the live cluster,
+unless you specify a custom C<max_content_length>:
+
+    # max_content_length retrieved from cluster
+    $e = Search::Elasticsearch->new(
+        cxn_pool => 'Sniff'
+    );
+
+    # max_content_length fixed at 10,000 bytes
+    $e = Search::Elasticsearch->new(
+        cxn_pool           => 'Sniff',
+        max_content_length => 10_000
+    );
+
+=head2 C<gzip>
+
+Enable Gzip compression of requests to and responses from Elasticsearch
+as follows:
+
+    $e = Search::Elasticsearch->new(
+        gzip => 1
+    );
+
+=head2 C<deflate>
+
+Enable Inflate/Deflate compression of requests to and responses from Elasticsearch
+as follows:
+
+    $e = Search::Elasticsearch->new(
+        deflate => 1
+    );
+
+
+B<IMPORTANT:> The L</request_timeout>, L</ping_timeout>, L</sniff_timeout>,
+and L</sniff_request_timeout> parameters default to values that allow
+this module to function with low powered hardware and slow networks.
+When you use Elasticsearch in production, you will probably want to reduce
+these timeout parameters to values that suit your environment.
+
+The configuration parameters are as follows:
+
+=head2 C<request_timeout>
+
+    $e = Search::Elasticsearch->new(
+        request_timeout => 30
+    );
+
+How long a normal request (ie not a ping or sniff request) should wait
+before throwing a C<Timeout> error.  Defaults to C<30> seconds.
+
+B<Note:> In production, no CRUD or search request should take 30 seconds to run,
+although admin tasks like C<upgrade()>, C<optimize()>, or snapshot C<create()>
+may take much longer. A more reasonable value for production would be
+C<10> seconds or lower.
+
+=head2 C<ping_timeout>
+
+    $e = Search::Elasticsearch->new(
+        ping_timeout => 2
+    );
+
+How long a ping request should wait before throwing a C<Timeout> error.
+Defaults to C<2> seconds. The L<Search::Elasticsearch::CxnPool::Static> module
+pings nodes on first use, after any failure, and periodically to ensure
+that nodes are healthy. The C<ping_timeout> should be long enough to allow
+nodes respond in time, but not so long that sick nodes cause delays.
+A reasonable value for use in production on reasonable hardware
+would be C<0.3>-C<1> seconds.
+
+=head2 C<dead_timeout>
+
+    $e = Search::Elasticsearch->new(
+        dead_timeout => 60
+    );
+
+How long a Cxn should be considered to be I<dead> (not used to serve requests),
+before it is retried.  The default is C<60> seconds.  This value is increased
+by powers of 2 for each time a request fails.  In other words, the delay
+after each failure is as follows:
+
+    Failure     Delay
+    1           60 * 1  = 60 seconds
+    2           60 * 2  = 120 seconds
+    3           60 * 4  = 240 seconds
+    4           60 * 8  = 480 seconds
+    5           60 * 16 = 960 seconds
+
+=head2 C<max_dead_timeout>
+
+    $e = Search::Elasticsearch->new(
+        max_dead_timeout => 3600
+    );
+
+The maximum delay that should be applied to a failed node. If the
+L</dead_timeout> calculation results in a delay greater than
+C<max_dead_timeout> (default C<3,600> seconds) then the C<max_dead_timeout>
+is used instead.  In other words, dead nodes will be retried at least once
+every hour by default.
+
+=head2 C<sniff_request_timeout>
+
+    $e = Search::Elasticsearch->new(
+        sniff_request_timeout => 2
+    );
+
+How long a sniff request should wait before throwing a C<Timeout> error.
+Defaults to C<2> seconds. A reasonable value for production would be
+C<0.5>-C<2> seconds.
+
+=head2 C<sniff_timeout>
+
+    $e = Search::Elasticsearch->new(
+        sniff_timeout => 1
+    );
+
+How long the node being sniffed should wait for responses from other nodes
+before responding to the client.  Defaults to C<1> second. A reasonable
+value in production would be C<0.3>-C<1> seconds.
+
+B<Note:> The C<sniff_timeout> is distinct from the L</sniff_request_timeout>.
+For example, let's say you have a cluster with 5 nodes, 2 of which are
+unhealthy (taking a long time to respond):
+
+=over
+
+=item *
+
+If you sniff an unhealthy node, the request will throw a C<Timeout> error
+after C<sniff_request_timeout> seconds.
+
+=item *
+
+If you sniff a healthy node, it will gather responses from the other nodes,
+and give up after C<sniff_timeout> seconds, returning just the information it
+has managed to gather from the healthy nodes.
+
+=back
+
+B<NOTE:> The C<sniff_request_timeout> must be longer than the C<sniff_timeout>
+to ensure that you get information about healthy nodes from the cluster.
+
+=head2 C<handle_args>
+
+Any default arguments which should be passed when creating a new instance of
+the class which handles the network transport, eg L<HTTP::Tiny>.
+
+=head2 C<default_qs_params>
+
+    $e = Search::Elasticsearch->new(
+        default_qs_params => {
+            session_key => 'my_session_key'
+        }
+    );
+
+Any values passed to C<default_qs_params> will be added to the query string
+of every request. Also see L<Search::Elasticsearch::Role::Cxn::HTTP/default_headers()>.
+
+
+=head1 METHODS
+
+None of the methods listed below are useful to the user. They are
+documented for those who are writing alternative implementations only.
+
+=head2 C<scheme()>
+
+    $scheme = $cxn->scheme;
+
+Returns the scheme of the connection, ie C<http> or C<https>.
+
+=head2 C<is_https()>
+
+    $bool = $cxn->is_https;
+
+Returns C<true> or C<false> depending on whether the C</scheme()> is C<https>
+or not.
+
+=head2 C<userinfo()>
+
+    $userinfo = $cxn->userinfo
+
+Returns the username and password of the cxn, if any, eg C<"user:pass">.
+If C<userinfo> is provided, then a Basic Authorization header is added
+to each request.
+
+=head2 C<default_headers()>
+
+    $headers = $cxn->default_headers
+
+The default headers that are passed with each request.  This includes
+the C<Accept-Encoding> header if C</deflate> is true, and the C<Authorization>
+header if C</userinfo> has a value.
+Also see L<Search::Elasticsearch::Role::Cxn/default_qs_params>.
+
+=head2 C<max_content_length()>
+
+    $int = $cxn->max_content_length;
+
+Returns the maximum length in bytes that the HTTP body can have.
+
+=head2 C<build_uri()>
+
+    $uri = $cxn->build_uri({ path => '/_search', qs => { size => 10 }});
+
+Returns the HTTP URI to use for a particular request, combining the passed
+in C<path> parameter with any defined C<path_prefix>, and adding the
+query-string parameters.
+
+=head1 METHODS
+
+None of the methods listed below are useful to the user. They are
+documented for those who are writing alternative implementations only.
+
+=head2 C<host()>
+
+    $host = $cxn->host;
+
+The value of the C<host> parameter, eg C<search.domain.com>.
+
+=head2 C<port()>
+
+    $port = $cxn->port;
+
+The value of the C<port> parameter, eg C<9200>.
+
+=head2 C<uri()>
+
+    $uri = $cxn->uri;
+
+A L<URI> object representing the node, eg C<https://search.domain.com:9200/path>.
+
+=head2 C<is_dead()>
+
+    $bool = $cxn->is_dead
+
+Is the current node marked as I<dead>.
+
+=head2 C<is_live()>
+
+    $bool = $cxn->is_live
+
+Is the current node marked as I<live>.
+
+=head2 C<next_ping()>
+
+    $time = $cxn->next_ping($time)
+
+Get/set the time for the next scheduled ping.  If zero, no ping is scheduled
+and the cxn is considered to be alive.  If -1, a ping is scheduled before
+the next use.
+
+=head2 C<ping_failures()>
+
+    $num = $cxn->ping_failures($num)
+
+The number of times that a cxn has been marked as dead.
+
+=head2 C<mark_dead()>
+
+    $cxn->mark_dead
+
+Mark the cxn as I<dead>, set L</next_ping()> and increment L</ping_failures()>.
+
+=head2 C<mark_live()>
+
+Mark the cxn as I<live>, set L</next_ping()> and L</ping_failures()> to zero.
+
+=head2 C<force_ping()>
+
+Set L</next_ping()> to -1 (ie before next use) and L</ping_failures()> to zero.
+
+=head2 C<pings_ok()>
+
+    $bool = $cxn->pings_ok
+
+Try to ping the node and call L</mark_live()> or L</mark_dead()> depending on
+the success or failure of the ping.
+
+=head2 C<sniff()>
+
+    $response = $cxn->sniff;
+
+Send a sniff request to the node and return the response.
+
+=head2 C<process_response()>
+
+    ($code,$result) = $cxn->process_response($params, $code, $msg, $body );
+
+Processes the response received from an Elasticsearch node and either
+returns the HTTP status code and the response body (deserialized from JSON)
+or throws an error of the appropriate type.
+
+The C<$params> are the original params passed to
+L<Search::Elasticsearch::Transport/perform_request()>, the C<$code> is the HTTP
+status code, the C<$msg> is the error message returned by the backend
+library and the C<$body> is the HTTP response body returned by
+Elasticsearch.
+
diff --git a/lib/Search/Elasticsearch/Role/Cxn/Async.pm b/lib/Search/Elasticsearch/Role/Cxn/Async.pm
new file mode 100644
index 0000000..8d40bad
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Cxn/Async.pm
@@ -0,0 +1,106 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::Cxn::Async;
+
+use Moo::Role;
+
+use Search::Elasticsearch::Util qw(new_error);
+use namespace::clean;
+
+#===================================
+sub pings_ok {
+#===================================
+    my $self = shift;
+    $self->logger->infof( 'Pinging [%s]', $self->stringify );
+
+    $self->perform_request(
+        {   method  => 'HEAD',
+            path    => '/',
+            timeout => $self->ping_timeout,
+        }
+        )->then(
+        sub {
+            $self->logger->infof( 'Marking [%s] as live', $self->stringify );
+            $self->mark_live;
+        },
+        sub {
+            $self->logger->debug(@_);
+            $self->mark_dead;
+            die(@_);
+        }
+        );
+}
+
+#===================================
+sub sniff {
+#===================================
+    my $self = shift;
+    $self->logger->infof( 'Sniffing [%s]', $self->stringify );
+    $self->perform_request(
+        {   method  => 'GET',
+            path    => '/_nodes/http',
+            qs      => { timeout => $self->sniff_timeout . 's' },
+            timeout => $self->sniff_request_timeout,
+        }
+        )->then(
+        sub { ( $self, $_[1]->{nodes} ) },
+        sub {
+            $self->mark_dead;
+            $self->logger->debug(@_);
+            ($self);
+        }
+        );
+}
+1;
+
+# ABSTRACT: Provides common functionality to async Cxn implementations
+
+=head1 DESCRIPTION
+
+L<Search::Elasticsearch::Role::Cxn::Async> provides common functionality to the
+async Cxn implementations. Cxn instances are created by a
+L<Search::Elasticsearch::Role::CxnPool> implementation,
+using the L<Search::Elasticsearch::Cxn::Factory> class.
+
+=head1 CONFIGURATION
+
+See L<Search::Elasticsearch::Role::Cxn> for configuration options.
+
+=head1 METHODS
+
+None of the methods listed below are useful to the user. They are
+documented for those who are writing alternative implementations only.
+
+
+=head2 C<pings_ok()>
+
+    $promise = $cxn->pings_ok
+
+Try to ping the node and call L</mark_live()> or L</mark_dead()> depending on
+the success or failure of the ping.
+
+=head2 C<sniff()>
+
+    $cxn->sniff
+        ->then(
+            sub { my ($cxn,$nodes) = @_; ... },
+            sub { my $cxn = shift; ... }
+          )
+
+Send a sniff request to the node and return the response.
+
diff --git a/lib/Search/Elasticsearch/Role/CxnPool.pm b/lib/Search/Elasticsearch/Role/CxnPool.pm
new file mode 100644
index 0000000..0f43b90
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/CxnPool.pm
@@ -0,0 +1,290 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::CxnPool;
+
+use Moo::Role;
+use Search::Elasticsearch::Util qw(parse_params);
+use List::Util qw(shuffle);
+use IO::Select();
+use Time::HiRes qw(time sleep);
+use Search::Elasticsearch::Util qw(to_list);
+use namespace::clean;
+
+requires qw(next_cxn schedule_check);
+
+has 'cxn_factory'     => ( is => 'ro',  required => 1 );
+has 'logger'          => ( is => 'ro',  required => 1 );
+has 'serializer'      => ( is => 'ro',  required => 1 );
+has 'current_cxn_num' => ( is => 'rwp', default  => 0 );
+has 'cxns'            => ( is => 'rwp', default  => sub { [] } );
+has 'seed_nodes'      => ( is => 'ro',  required => 1 );
+has 'retries'         => ( is => 'rw',  default  => 0 );
+has 'randomize_cxns'  => ( is => 'ro',  default  => 1 );
+
+#===================================
+around BUILDARGS => sub {
+#===================================
+    my $orig   = shift;
+    my $params = $orig->(@_);
+    my @seed   = grep {$_} to_list( delete $params->{nodes} || ('') );
+
+    @seed = $params->{cxn_factory}->default_host
+        unless @seed;
+    $params->{seed_nodes} = \@seed;
+    return $params;
+};
+
+#===================================
+sub next_cxn_num {
+#===================================
+    my $self = shift;
+    my $cxns = $self->cxns;
+    return unless @$cxns;
+    my $current = $self->current_cxn_num;
+    $self->_set_current_cxn_num( ( $current + 1 ) % @$cxns );
+    return $current;
+}
+
+#===================================
+sub set_cxns {
+#===================================
+    my $self    = shift;
+    my $factory = $self->cxn_factory;
+    my @cxns    = map { $factory->new_cxn($_) } @_;
+    @cxns = shuffle @cxns if $self->randomize_cxns;
+    $self->_set_cxns( \@cxns );
+    $self->_set_current_cxn_num(0);
+
+    $self->logger->infof( "Current cxns: %s",
+        [ map { $_->stringify } @cxns ] );
+
+    return;
+}
+
+#===================================
+sub request_ok {
+#===================================
+    my ( $self, $cxn ) = @_;
+    $cxn->mark_live;
+    $self->reset_retries;
+}
+
+#===================================
+sub request_failed {
+#===================================
+    my ( $self, $cxn, $error ) = @_;
+
+    if ( $error->is( 'Cxn', 'Timeout' ) ) {
+        $cxn->mark_dead if $self->should_mark_dead($error);
+        $self->schedule_check;
+
+        if ( $self->should_retry($error) ) {
+            my $retries = $self->retries( $self->retries + 1 );
+            return 1 if $retries < $self->_max_retries;
+        }
+    }
+    else {
+        $cxn->mark_live if $cxn;
+    }
+    $self->reset_retries;
+    return 0;
+}
+
+#===================================
+sub should_retry {
+#===================================
+    my ( $self, $error ) = @_;
+    return $error->is('Cxn');
+}
+
+#===================================
+sub should_mark_dead {
+#===================================
+    my ( $self, $error ) = @_;
+    return $error->is('Cxn');
+}
+
+#===================================
+sub cxns_str {
+#===================================
+    my $self = shift;
+    join ", ", map { $_->stringify } @{ $self->cxns };
+}
+
+#===================================
+sub cxns_seeds_str {
+#===================================
+    my $self = shift;
+    join ", ", ( map { $_->stringify } @{ $self->cxns } ),
+        @{ $self->seed_nodes };
+}
+
+#===================================
+sub reset_retries { shift->retries(0) }
+sub _max_retries  {2}
+#===================================
+
+1;
+
+__END__
+
+#ABSTRACT: Provides common functionality to the CxnPool implementations
+
+
+=head1 DESCRIPTION
+
+See the CxnPool implementations:
+
+=over
+
+=item *
+
+L<Search::Elasticsearch::CxnPool::Static>
+
+=item *
+
+L<Search::Elasticsearch::CxnPool::Sniff>
+
+=item *
+
+L<Search::Elasticsearch::CxnPool::Static::NoPing>
+
+=back
+
+=head1 CONFIGURATION
+
+These configuration options should not be set by the user but are
+documented here for completeness.
+
+=head2 C<randomize_cxns>
+
+By default, the order of cxns passed to L</set_cxns()> is randomized
+before they are stored.  Set C<randomize_cxns> to a false value to
+disable.
+
+=head1 METHODS
+
+=head2 C<cxn_factory()>
+
+    $factory = $cxn_pool->cxn_factory
+
+Returns the L<Search::Elasticsearch::Cxn::Factory> object for creating a new
+C<$cxn> instance.
+
+=head2 C<logger()>
+
+    $logger = $cxn_pool->logger
+
+Returns the L<Search::Elasticsearch::Role::Logger>-based object, which
+defaults to L<Search::Elasticsearch::Logger::LogAny>.
+
+=head2 C<serializer()>
+
+    $serializer = $cxn_pool->serializer
+
+Returns the L<Search::Elasticsearch::Role::Serializer>-based object,
+which defaults to L<Search::Elasticsearch::Serializer::JSON>.
+
+=head2 C<current_cxn_num()>
+
+    $num = $cxn_pool->current_cxn_num
+
+Returns the current cxn number, which is an offset into
+the array of cxns set by L</set_cxns()>.
+
+=head2 C<cxns()>
+
+    \@cxns = $cxn_pool->cxns;
+
+Returns the current list of L<Search::Elasticsearch::Role::Cxn>-based
+cxn objects as set by L</set_cxns()>.
+
+=head2 C<seed_nodes()>
+
+    \@seed_nodes = $cxn_pool->seed_nodes
+
+Returns the list of C<nodes> originally specified when calling
+L<Search::Elasticsearch/new()>.
+
+=head2 C<next_cxn_num()>
+
+    $num = $cxn_pool->next_cxn_num;
+
+Returns the number of the next connection, in round-robin fashion.  Updates
+the L</current_cxn_num()>.
+
+=head2 C<set_cxns()>
+
+    $cxn_pool->set_cxns(@nodes);
+
+Takes a list of nodes, converts them into L<Search::Elasticsearch::Role::Cxn>-based
+objects and makes them accessible via L</cxns()>.
+
+=head2 C<request_ok()>
+
+    $cxn_pool->request_ok($cxn);
+
+Called when a request by the specified C<$cxn> object has completed successfully.
+Marks the C<$cxn> as live.
+
+=head2 C<request_failed()>
+
+    $should_retry = $cxn_pool->request_failed($cxn,$error);
+
+Called when a request by the specified C<$cxn> object has failed. Returns
+C<1> if the request should be retried or C<0> if it shouldn't.
+
+=head2 C<should_retry()>
+
+    $bool = $cxn_pool->should_retry($error);
+
+Examines the error to decide whether the request should be retried or not.
+By default, only L<Search::Elasticsearch::Error/Search::Elasticsearch::Error::Cxn> errors
+are retried.
+
+=head2 C<should_mark_dead()>
+
+    $bool = $cxn_pool->should_mark_dead($error);
+
+Examines the error to decide whether the C<$cxn> should be marked as dead or not.
+By default, only L<Search::Elasticsearch::Error/Search::Elasticsearch::Error::Cxn> errors
+cause a C<$cxn> to be marked as dead.
+
+=head2 C<cxns_str()>
+
+    $str = $cxn_pool->cxns_str
+
+Returns all L</cxns()> as a string for logging purposes.
+
+=head2 C<cxns_seeds_str()>
+
+    $str = $cxn_pool->cxns_seeeds_str
+
+Returns all L</cxns()> and L</seed_nodes()> as a string for logging purposes.
+
+=head2 C<retries()>
+
+    $retries = $cxn_pool->retries
+
+The number of times the current request has been retried.
+
+=head2 C<reset_retries()>
+
+    $cxn_pool->reset_retries;
+
+Called at the start of a new request to reset the retries count.
diff --git a/lib/Search/Elasticsearch/Role/CxnPool/Sniff.pm b/lib/Search/Elasticsearch/Role/CxnPool/Sniff.pm
new file mode 100644
index 0000000..c68c483
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/CxnPool/Sniff.pm
@@ -0,0 +1,170 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::CxnPool::Sniff;
+
+use Moo::Role;
+with 'Search::Elasticsearch::Role::CxnPool';
+requires 'next_cxn', 'sniff';
+use namespace::clean;
+
+use Search::Elasticsearch::Util qw(parse_params);
+use List::Util qw(min);
+use Try::Tiny;
+
+has 'sniff_interval' => ( is => 'ro', default => 300 );
+has 'next_sniff'     => ( is => 'rw', default => 0 );
+has 'sniff_max_content_length' => ( is => 'ro' );
+
+#===================================
+sub BUILDARGS {
+#===================================
+    my ( $class, $params ) = parse_params(@_);
+    $params->{sniff_max_content_length} = !$params->{max_content_length}
+        unless defined $params->{sniff_max_content_length};
+    return $params;
+}
+
+#===================================
+sub schedule_check {
+#===================================
+    my $self = shift;
+    $self->logger->info("Require sniff before next request");
+    $self->next_sniff(-1);
+}
+
+#===================================
+sub parse_sniff {
+#===================================
+    my $self = shift;
+    my $nodes = shift or return;
+    my @live_nodes;
+    my $max       = 0;
+    my $sniff_max = $self->sniff_max_content_length;
+
+    for my $node_id ( keys %$nodes ) {
+        my $data = $nodes->{$node_id};
+
+        my $addr = $data->{http}{publish_address} || $data->{http_address};
+        my $host = $self->_extract_host($addr)
+            or next;
+
+        $host = $self->should_accept_node( $host, $node_id, $data )
+            or next;
+
+        push @live_nodes, $host;
+        next unless $sniff_max and $data->{http};
+
+        my $node_max = $data->{http}{max_content_length_in_bytes} || 0;
+        $max
+            = $node_max == 0 ? $max
+            : $max == 0      ? $node_max
+            :                  min( $node_max, $max );
+    }
+
+    return unless @live_nodes;
+
+    $self->cxn_factory->max_content_length($max)
+        if $sniff_max and $max;
+
+    $self->set_cxns(@live_nodes);
+    my $next = $self->next_sniff( time() + $self->sniff_interval );
+    $self->logger->infof( "Next sniff at: %s", scalar localtime($next) );
+
+    return 1;
+}
+
+#===================================
+sub _extract_host {
+#===================================
+    my $self = shift;
+    my $host = shift || return;
+    $host =~ s{^inet\[(.+)\]$}{$1};
+    $host =~ s{^[^/]*/}{};
+    return $host;
+}
+
+#===================================
+sub should_accept_node { return $_[1] }
+#===================================
+
+1;
+
+__END__
+
+# ABSTRACT: A CxnPool role for connecting to a local cluster with a dynamic node list
+
+=head1 CONFIGURATION
+
+=head2 C<sniff_interval>
+
+How often should we perform a sniff in order to detect whether new nodes
+have been added to the cluster.  Defaults to `300` seconds.
+
+=head2 C<sniff_max_content_length>
+
+Whether we should set the
+L<max_content_length|Search::Elasticsearch::Role::Cxn/max_content_length>
+dynamically while sniffing. Defaults to true unless a fixed
+C<max_content_length> was specified.
+
+=head1 METHODS
+
+=head2 C<schedule_check()>
+
+    $cxn_pool->schedule_check
+
+Schedules a sniff before the next request is processed.
+
+=head2 C<parse_sniff()>
+
+    $bool = $cxn_pool->parse_sniff(\%nodes);
+
+Parses the response from a sniff request and extracts the hostname/ip
+of all listed nodes, filtered through L</should_accept_node()>. If any live
+nodes are found, they are passed to L<Search::Elasticsearch::Role::CxnPool/set_cxns()>.
+The L<max_content_length|Search::Elasticsearch::Role::Cxn/max_content_length>
+is also detected if L</sniff_max_content_length> is true.
+
+=head2 C<should_accept_node()>
+
+    $host = $cxn_pool->should_accept_node($host,$node_id,\%node_data)
+
+This method serves as a hook which can be overridden by the user.  When
+a sniff is performed, this method is called with the C<host>
+(eg C<192.168.5.100:9200>), the C<node_id> (the ID assigned to the node
+by Elasticsearch) and the C<node_data> which contains the information
+about the node that Elasticsearch has returned, eg:
+
+    {
+        "transport_address" => "inet[192.168.5.100/192.168.5.100:9300]",
+        "http" : {
+           "publish_address"    => "inet[/192.168.5.100:9200]",
+           "max_content_length" => "100mb",
+           "bound_address"      => "inet[/0:0:0:0:0:0:0:0:9200]",
+           "max_content_length_in_bytes" : 104857600
+        },
+        "version"       => "0.90.4",
+        "name"          => "Silver Sable",
+        "hostname"      => "search1.domain.com",
+        "http_address"  => "inet[/192.168.5.100:9200]"
+    }
+
+If the node should be I<accepted> (ie used to serve data), then it should
+return the C<host> value to use.  By default, nodes are always
+accepted.
+
diff --git a/lib/Search/Elasticsearch/Role/CxnPool/Static.pm b/lib/Search/Elasticsearch/Role/CxnPool/Static.pm
new file mode 100644
index 0000000..de50a34
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/CxnPool/Static.pm
@@ -0,0 +1,61 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::CxnPool::Static;
+
+use Moo::Role;
+with 'Search::Elasticsearch::Role::CxnPool';
+requires 'next_cxn';
+
+use namespace::clean;
+
+#===================================
+sub BUILD {
+#===================================
+    my $self = shift;
+    $self->set_cxns( @{ $self->seed_nodes } );
+    $self->schedule_check;
+}
+
+#===================================
+sub schedule_check {
+#===================================
+    my ($self) = @_;
+    $self->logger->info("Forcing ping before next use on all live cxns");
+    for my $cxn ( @{ $self->cxns } ) {
+        next if $cxn->is_dead;
+        $self->logger->infof( "Ping [%s] before next request",
+            $cxn->stringify );
+        $cxn->force_ping;
+    }
+}
+
+1;
+
+__END__
+
+# ABSTRACT: A CxnPool role for connecting to a remote cluster with a static list of nodes.
+
+=head1 METHODS
+
+=head2 C<schedule_check()>
+
+    $cxn_pool->schedule_check
+
+Forces a ping on each cxn in L<cxns()|Search::Elasticsearch::Role::CxnPool/cxns()>
+before the next time that cxn is used for a request.
+
diff --git a/lib/Search/Elasticsearch/Role/CxnPool/Static/NoPing.pm b/lib/Search/Elasticsearch/Role/CxnPool/Static/NoPing.pm
new file mode 100644
index 0000000..9589bb0
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/CxnPool/Static/NoPing.pm
@@ -0,0 +1,85 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::CxnPool::Static::NoPing;
+
+use Moo::Role;
+with 'Search::Elasticsearch::Role::CxnPool';
+
+use namespace::clean;
+
+has 'max_retries' => ( is => 'lazy' );
+has '_dead_cxns' => ( is => 'ro', default => sub { [] } );
+
+#===================================
+sub next_cxn {
+#===================================
+    my $self = shift;
+
+    my $cxns  = $self->cxns;
+    my $total = @$cxns;
+    my $dead  = $self->_dead_cxns;
+
+    while ( $total-- ) {
+        my $cxn = $cxns->[ $self->next_cxn_num ];
+        return $cxn
+            if $cxn->is_live
+            || $cxn->next_ping < time();
+        push @$dead, $cxn unless grep { $_ eq $cxn } @$dead;
+    }
+
+    if ( @$dead and $self->retries <= $self->max_retries ) {
+        $_->force_ping for @$dead;
+        return shift @$dead;
+    }
+    throw( "NoNodes", "No nodes are available: [" . $self->cxns_str . ']' );
+}
+
+#===================================
+sub _build_max_retries { @{ shift->cxns } - 1 }
+sub _max_retries       { shift->max_retries + 1 }
+#===================================
+
+#===================================
+sub BUILD {
+#===================================
+    my $self = shift;
+    $self->set_cxns( @{ $self->seed_nodes } );
+}
+
+#===================================
+sub should_mark_dead {
+#===================================
+    my ( $self, $error ) = @_;
+    return $error->is( 'Cxn', 'Timeout' );
+}
+
+#===================================
+after 'reset_retries' => sub {
+#===================================
+    my $self = shift;
+    @{ $self->_dead_cxns } = ();
+
+};
+
+#===================================
+sub schedule_check { }
+#===================================
+
+1;
+
+# ABSTRACT: A CxnPool for connecting to a remote cluster without the ability to ping.
diff --git a/lib/Search/Elasticsearch/Role/Is_Async.pm b/lib/Search/Elasticsearch/Role/Is_Async.pm
new file mode 100644
index 0000000..e074e28
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Is_Async.pm
@@ -0,0 +1,25 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::Is_Async;
+
+use Moo::Role;
+use namespace::clean;
+
+1;
+
+# ABSTRACT: A role to mark classes which should be used with other async classes
diff --git a/lib/Search/Elasticsearch/Role/Is_Sync.pm b/lib/Search/Elasticsearch/Role/Is_Sync.pm
new file mode 100644
index 0000000..53f376e
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Is_Sync.pm
@@ -0,0 +1,25 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::Is_Sync;
+
+use Moo::Role;
+use namespace::clean;
+
+1;
+
+# ABSTRACT: A role to mark classes which should be used with other sync classes
diff --git a/lib/Search/Elasticsearch/Role/Logger.pm b/lib/Search/Elasticsearch/Role/Logger.pm
new file mode 100644
index 0000000..67956f7
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Logger.pm
@@ -0,0 +1,263 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::Logger;
+
+use Moo::Role;
+
+use URI();
+use Try::Tiny;
+use Search::Elasticsearch::Util qw(new_error);
+use namespace::clean;
+
+has 'serializer'   => ( is => 'ro', required => 1 );
+has 'log_as'       => ( is => 'ro', default  => 'elasticsearch.event' );
+has 'trace_as'     => ( is => 'ro', default  => 'elasticsearch.trace' );
+has 'deprecate_as' => ( is => 'ro', default  => 'elasticsearch.deprecation' );
+has 'log_to'       => ( is => 'ro' );
+has 'trace_to'     => ( is => 'ro' );
+has 'deprecate_to' => ( is => 'ro' );
+
+has 'trace_handle' => (
+    is      => 'lazy',
+    handles => [qw( trace tracef is_trace)]
+);
+
+has 'log_handle' => (
+    is      => 'lazy',
+    handles => [ qw(
+            debug       debugf      is_debug
+            info        infof       is_info
+            warning     warningf    is_warning
+            error       errorf      is_error
+            critical    criticalf   is_critical
+            )
+    ]
+);
+
+has 'deprecate_handle' => ( is => 'lazy' );
+
+#===================================
+sub throw_error {
+#===================================
+    my ( $self, $type, $msg, $vars ) = @_;
+    my $error = new_error( $type, $msg, $vars );
+    $self->error($error);
+    die $error;
+}
+
+#===================================
+sub throw_critical {
+#===================================
+    my ( $self, $type, $msg, $vars ) = @_;
+    my $error = new_error( $type, $msg, $vars );
+    $self->critical($error);
+    die $error;
+}
+
+#===================================
+sub trace_request {
+#===================================
+    my ( $self, $cxn, $params ) = @_;
+    return unless $self->is_trace;
+
+    my $uri = URI->new( 'http://localhost:9200' . $params->{path} );
+    my %qs = ( %{ $params->{qs} }, pretty => "true" );
+    $uri->query_form( [ map { $_, $qs{$_} } sort keys %qs ] );
+
+    my $body
+        = $params->{serialize} eq 'std'
+        ? $self->serializer->encode_pretty( $params->{body} )
+        : $params->{data};
+
+    my $content_type = '';
+    if ( defined $body ) {
+        $body =~ s/'/\\u0027/g;
+        $body         = " -d '\n$body'\n";
+        $content_type = '-H "Content-type: ' . $params->{mime_type} . '" ';
+    }
+    else { $body = "\n" }
+
+    my $msg = sprintf(
+        "# Request to: %s\n"           #
+            . "curl %s-X%s '%s'%s",    #
+        $cxn->stringify,
+        $content_type,
+        $params->{method},
+        $uri,
+        $body
+    );
+
+    $self->trace($msg);
+}
+
+#===================================
+sub trace_response {
+#===================================
+    my ( $self, $cxn, $code, $response, $took ) = @_;
+    return unless $self->is_trace;
+
+    my $body = $self->serializer->encode_pretty($response) || "\n";
+    $body =~ s/^/# /mg;
+
+    my $msg = sprintf(
+        "# Response: %s, Took: %d ms\n%s",    #
+        $code, $took * 1000, $body
+    );
+
+    $self->trace($msg);
+}
+
+#===================================
+sub trace_error {
+#===================================
+    my ( $self, $cxn, $error ) = @_;
+    return unless $self->is_trace;
+
+    my $body
+        = $self->serializer->encode_pretty( $error->{vars}{body} || "\n" );
+    $body =~ s/^/# /mg;
+
+    my $msg
+        = sprintf( "# ERROR: %s %s\n%s", ref($error), $error->{text}, $body );
+
+    $self->trace($msg);
+}
+
+#===================================
+sub trace_comment {
+#===================================
+    my ( $self, $comment ) = @_;
+    return unless $self->is_trace;
+    $comment =~ s/^/# *** /mg;
+    chomp $comment;
+    $self->trace("$comment\n");
+}
+
+#===================================
+sub deprecation {
+#===================================
+    my $self = shift;
+
+    $self->deprecate_handle->warnf( "[DEPRECATION] %s - In request: %s", @_ );
+}
+1;
+
+# ABSTRACT: Provides common functionality to Logger implementations
+
+=head1 DESCRIPTION
+
+This role provides common functionality to Logger implementations, to enable
+the logging of events and the tracing of request-response conversations
+with Elasticsearch nodes.
+
+See L<Search::Elasticsearch::Logger::LogAny> for the default implementation.
+
+=head1 CONFIGURATION
+
+=head2 C<log_to>
+
+Parameters passed to C<log_to> are used by L<Search::Elasticsearch::Role::Logger>
+implementations to setup the L</log_handle()>.  See
+L<Search::Elasticsearch::Logger::LogAny> for details.
+
+=head2 C<log_as>
+
+By default, events emitted by L</debug()>, L</info()>, L</warning()>,
+L</error()> and L</critical()> are logged to the L</log_handle()> under the
+category C<"elasticsearch.event">, which can be configured with C<log_as>.
+
+=head2 C<trace_to>
+
+Parameters passed to C<trace_to> are used by L<Search::Elasticsearch::Role::Logger>
+implementations to setup the L</trace_handle()>. See
+L<Search::Elasticsearch::Logger::LogAny> for details.
+
+=head2 C<trace_as>
+
+By default, trace output emitted by L</trace_request()>, L</trace_response()>,
+L</trace_error()> and L</trace_comment()> are logged under the category
+C<elasticsearch.trace>, which can be configured with C<trace_as>.
+
+=head2 C<deprecate_to>
+
+Parameters passed to C<deprecate_to> are used by L<Search::Elasticsearch::Role::Logger>
+implementations to setup the L</deprecate_handle()>.  See
+L<Search::Elasticsearch::Logger::LogAny> for details.
+
+=head2 C<deprecate_as>
+
+By default, events emitted by L</deprecation()> are logged to the
+L</deprecate_handle()> under the
+category C<"elasticsearch.deprecation">, which can be configured with C<deprecate_as>.
+
+
+=head1 METHODS
+
+=head2 C<log_handle()>
+
+Returns an object which can handle the methods:
+C<debug()>, C<debugf()>, C<is_debug()>, C<info()>, C<infof()>, C<is_info()>,
+C<warning()>, C<warningf()>, C<is_warning()>, C<error()>, C<errorf()>,
+C<is_error()>, C<critical()>, C<criticalf()> and  C<is_critical()>.
+
+=head2 C<trace_handle()>
+
+Returns an object which can handle the methods:
+C<trace()>, C<tracef()> and C<is_trace()>.
+
+=head2 C<deprecate_handle()>
+
+Returns an object which can handle the C<warnf()> method.
+
+=head2 C<trace_request()>
+
+    $logger->trace_request($cxn,\%request);
+
+Accepts a Cxn object and request parameters and logs them if tracing is
+enabled.
+
+=head2 C<trace_response()>
+
+    $logger->trace_response($cxn,$code,$response,$took);
+
+Logs a successful HTTP response, where C<$code> is the HTTP status code,
+C<$response> is the HTTP body and C<$took> is the time the request
+took in seconds
+
+=head2 C<trace_error()>
+
+    $logger->trace_error($cxn,$error);
+
+Logs a failed HTTP response, where C<$error> is an L<Search::Elasticsearch::Error>
+object.
+
+=head2 C<trace_comment()>
+
+    $logger->trace_comment($comment);
+
+Used to insert debugging comments into trace output.
+
+=head2 C<deprecation()>
+
+    $logger->deprecation($warning,$request)
+
+Issues a deprecation warning to the deprecation logger.
+
+
+
+
diff --git a/lib/Search/Elasticsearch/Role/Serializer.pm b/lib/Search/Elasticsearch/Role/Serializer.pm
new file mode 100644
index 0000000..1b2965f
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Serializer.pm
@@ -0,0 +1,58 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::Serializer;
+
+use Moo::Role;
+
+requires qw(encode decode encode_pretty encode_bulk mime_type);
+
+1;
+
+# ABSTRACT: An interface for Serializer modules
+
+=head1 DESCRIPTION
+
+There is no code in this module. It defines an interface for
+Serializer implementations, and requires the following methods:
+
+=over
+
+=item *
+
+C<encode()>
+
+=item *
+
+C<encode_pretty()>
+
+=item *
+
+C<encode_bulk()>
+
+=item *
+
+C<decode()>
+
+=item *
+
+C<mime_type()>
+
+=back
+
+
+See L<Search::Elasticsearch::Serializer::JSON> for more.
diff --git a/lib/Search/Elasticsearch/Role/Serializer/JSON.pm b/lib/Search/Elasticsearch/Role/Serializer/JSON.pm
new file mode 100644
index 0000000..769d119
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Serializer/JSON.pm
@@ -0,0 +1,158 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Role::Serializer::JSON;
+
+use Moo::Role;
+requires 'JSON';
+
+use Search::Elasticsearch::Util qw(throw);
+use Try::Tiny;
+use Encode qw(encode_utf8 decode_utf8 is_utf8);
+use namespace::clean;
+
+has 'mime_type' => ( is => 'ro', default => 'application/json' );
+
+with 'Search::Elasticsearch::Role::Serializer';
+
+#===================================
+sub encode {
+#===================================
+    my ( $self, $var ) = @_;
+    unless ( ref $var ) {
+        return is_utf8($var)
+            ? encode_utf8($var)
+            : $var;
+    }
+    return try { $self->JSON->encode($var) }
+    catch { throw( "Serializer", $_, { var => $var } ) };
+}
+
+#===================================
+sub encode_bulk {
+#===================================
+    my ( $self, $var ) = @_;
+    unless ( ref $var ) {
+        return is_utf8($var)
+            ? encode_utf8($var)
+            : $var;
+    }
+
+    my $json = '';
+    throw( "Param", "Var must be an array ref" )
+        unless ref $var eq 'ARRAY';
+    return try {
+        for (@$var) {
+            $json .= ( ref($_) ? $self->JSON->encode($_) : $_ ) . "\n";
+        }
+        return $json;
+    }
+    catch { throw( "Serializer", $_, { var => $var } ) };
+}
+
+#===================================
+sub encode_pretty {
+#===================================
+    my ( $self, $var ) = @_;
+    $self->JSON->pretty(1);
+
+    my $json;
+    try {
+        $json = $self->encode($var);
+    }
+    catch {
+        die "$_";
+    }
+    finally {
+        $self->JSON->pretty(0);
+    };
+
+    return $json;
+}
+
+#===================================
+sub decode {
+#===================================
+    my ( $self, $json ) = @_;
+
+    return unless defined $json;
+
+    return is_utf8($json) ? $json : decode_utf8($json)
+        unless substr( $json, 0, 1 ) =~ /^[\[{]/;
+
+    return try {
+        $self->JSON->decode($json);
+    }
+    catch {
+        throw( "Serializer", $_, { json => $json } );
+    };
+}
+
+#===================================
+sub _set_canonical {
+#===================================
+    shift()->JSON->canonical(1);
+}
+
+1;
+
+__END__
+
+# ABSTRACT: A Serializer role for JSON modules
+
+=head1 DESCRIPTION
+
+This role encodes Perl data structures into JSON strings, and
+decodes JSON strings into Perl data structures.
+
+=head1 METHODS
+
+=head2 C<encode()>
+
+    $bytes = $serializer->encode($ref);
+    $bytes = $serializer->encode($str);
+
+The L</encode()> method converts array and hash refs into their JSON
+equivalents.  If a string is passed in, it is returned as the UTF8 encoded
+version of itself.  The empty string and C<undef> are returned as is.
+
+=head2 C<encode_pretty()>
+
+    $bytes = $serializer->encode_pretty($ref);
+    $bytes = $serializer->encode_pretty($str);
+
+Works exactly as L</encode()> but the JSON output is pretty-printed.
+
+=head2 C<encode_bulk()>
+
+    $bytes = $serializer->encode_bulk([\%hash,\%hash,...]);
+    $bytes = $serializer->encode_bulk([$str,$str,...]);
+
+The L</encode_bulk()> method expects an array ref of hashes or strings.
+Each hash or string is processed by L</encode()> then joined together
+by newline characters, with a final newline character appended to the end.
+This is the special JSON format used for bulk requests.
+
+=head2 C<decode()>
+
+    $var = $serializer->decode($json_bytes);
+    $str = $serializer->decode($bytes);
+
+If the passed in value looks like JSON (ie starts with a C<{> or C<[>
+character), then it is decoded from JSON, otherwise it is returned as
+the UTF8 decoded version of itself. The empty string and C<undef> are
+returned as is.
diff --git a/lib/Search/Elasticsearch/Role/Transport.pm b/lib/Search/Elasticsearch/Role/Transport.pm
new file mode 100644
index 0000000..be3b844
--- /dev/null
+++ b/lib/Search/Elasticsearch/Role/Transport.pm
@@ -0,0 +1,65 @@
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Role::Transport;
+
+use Moo::Role;
+
+requires qw(perform_request);
+
+use Try::Tiny;
+use Search::Elasticsearch::Util qw(parse_params is_compat);
+use namespace::clean;
+
+has 'serializer'       => ( is => 'ro', required => 1 );
+has 'logger'           => ( is => 'ro', required => 1 );
+has 'send_get_body_as' => ( is => 'ro', default  => 'GET' );
+has 'cxn_pool'         => ( is => 'ro', required => 1 );
+
+#===================================
+sub BUILD {
+#===================================
+    my $self = shift;
+    my $pool = $self->cxn_pool;
+    is_compat( 'cxn_pool', $self, $pool );
+    is_compat( 'cxn',      $self, $pool->cxn_factory->cxn_class );
+    return $self;
+}
+
+#===================================
+sub tidy_request {
+#===================================
+    my ( $self, $params ) = parse_params(@_);
+    $params->{method} ||= 'GET';
+    $params->{path}   ||= '/';
+    $params->{qs}     ||= {};
+    $params->{ignore} ||= [];
+    my $body = $params->{body};
+    return $params unless defined $body;
+
+    $params->{serialize} ||= 'std';
+    $params->{data}
+        = $params->{serialize} eq 'std'
+        ? $self->serializer->encode($body)
+        : $self->serializer->encode_bulk($body);
+
+    if ( $params->{method} eq 'GET' ) {
+        my $send_as = $self->send_get_body_as;
+        if ( $send_as eq 'POST' ) {
+            $params->{method} = 'POST';
+        }
+        elsif ( $send_as eq 'source' ) {
+            $params->{qs}{source} = delete $params->{data};
+            delete $params->{body};
+        }
+    }
+
+    $params->{mime_type} ||= $self->serializer->mime_type;
+    return $params;
+
+}
+
+1;
+
+#ABSTRACT: Transport role providing interface between the client class and the Elasticsearch cluster
diff --git a/lib/Search/Elasticsearch/Serializer/JSON.pm b/lib/Search/Elasticsearch/Serializer/JSON.pm
new file mode 100644
index 0000000..35f129e
--- /dev/null
+++ b/lib/Search/Elasticsearch/Serializer/JSON.pm
@@ -0,0 +1,70 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Serializer::JSON;
+
+use Moo;
+use JSON::MaybeXS 1.002002 ();
+
+has 'JSON' => ( is => 'ro', default => sub { JSON::MaybeXS->new->utf8(1) } );
+
+with 'Search::Elasticsearch::Role::Serializer::JSON';
+use namespace::clean;
+
+1;
+
+# ABSTRACT: The default JSON Serializer, using JSON::MaybeXS
+
+=head1 SYNOPSIS
+
+    $e = Search::Elasticsearch(
+        # serializer => 'JSON'
+    );
+
+=head1 DESCRIPTION
+
+This default Serializer class chooses between:
+
+=over
+
+=item * L<Cpanel::JSON::XS>
+
+=item * L<JSON::XS>
+
+=item * L<JSON::PP>
+
+=back
+
+First it checks if either L<Cpanel::JSON::XS> or L<JSON::XS> is already
+loaded and, if so, uses the appropriate backend.  Otherwise it tries
+to load L<Cpanel::JSON::XS>, then L<JSON::XS> and finally L<JSON::PP>.
+
+If you would prefer to specify a particular JSON backend, then you can
+do so by using one of these modules:
+
+=over
+
+=item * L<Search::Elasticsearch::Serializer::JSON::Cpanel>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::XS>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::PP>
+
+=back
+
+See their documentation for details.
+
diff --git a/lib/Search/Elasticsearch/Serializer/JSON/Cpanel.pm b/lib/Search/Elasticsearch/Serializer/JSON/Cpanel.pm
new file mode 100644
index 0000000..e900c34
--- /dev/null
+++ b/lib/Search/Elasticsearch/Serializer/JSON/Cpanel.pm
@@ -0,0 +1,58 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Serializer::JSON::Cpanel;
+
+use Cpanel::JSON::XS;
+use Moo;
+
+has 'JSON' =>
+    ( is => 'ro', default => sub { Cpanel::JSON::XS->new->utf8(1) } );
+
+with 'Search::Elasticsearch::Role::Serializer::JSON';
+
+1;
+
+__END__
+
+# ABSTRACT: A JSON Serializer using Cpanel::JSON::XS
+
+=head1 SYNOPSIS
+
+    $e = Search::Elasticsearch(
+        serializer => 'JSON::Cpanel'
+    );
+
+=head1 DESCRIPTION
+
+While the default serializer, L<Search::Elasticsearch::Serializer::JSON>,
+tries to choose the appropriate JSON backend, this module allows you to
+choose the L<Cpanel::JSON::XS> backend specifically.
+
+This class does L<Search::Elasticsearch::Role::Serializer::JSON>.
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Serializer::JSON>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::XS>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::PP>
+
+=back
diff --git a/lib/Search/Elasticsearch/Serializer/JSON/PP.pm b/lib/Search/Elasticsearch/Serializer/JSON/PP.pm
new file mode 100644
index 0000000..4f42596
--- /dev/null
+++ b/lib/Search/Elasticsearch/Serializer/JSON/PP.pm
@@ -0,0 +1,58 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Serializer::JSON::PP;
+
+use Moo;
+use JSON::PP;
+
+has 'JSON' => ( is => 'ro', default => sub { JSON::PP->new->utf8(1) } );
+
+with 'Search::Elasticsearch::Role::Serializer::JSON';
+
+1;
+
+# ABSTRACT: A JSON Serializer using JSON::PP
+
+=head1 SYNOPSIS
+
+    $e = Search::Elasticsearch(
+        serializer => 'JSON::PP'
+    );
+
+=head1 DESCRIPTION
+
+While the default serializer, L<Search::Elasticsearch::Serializer::JSON>,
+tries to choose the appropriate JSON backend, this module allows you to
+choose the L<JSON::PP> backend specifically.
+
+B<NOTE:> You should really install and use either L<JSON::XS> or
+L<Cpanel::JSON::XS> as they are much much faster than L<JSON::PP>.
+
+This class does L<Search::Elasticsearch::Role::Serializer::JSON>.
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Serializer::JSON>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::XS>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::Cpanel>
+
+=back
diff --git a/lib/Search/Elasticsearch/Serializer/JSON/XS.pm b/lib/Search/Elasticsearch/Serializer/JSON/XS.pm
new file mode 100644
index 0000000..49bb97b
--- /dev/null
+++ b/lib/Search/Elasticsearch/Serializer/JSON/XS.pm
@@ -0,0 +1,57 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Serializer::JSON::XS;
+
+use Moo;
+use JSON::XS 2.26;
+
+has 'JSON' => ( is => 'ro', default => sub { JSON::XS->new->utf8(1) } );
+
+with 'Search::Elasticsearch::Role::Serializer::JSON';
+
+1;
+
+__END__
+
+# ABSTRACT: A JSON Serializer using JSON::XS
+
+=head1 SYNOPSIS
+
+    $e = Search::Elasticsearch(
+        serializer => 'JSON::XS'
+    );
+
+=head1 DESCRIPTION
+
+While the default serializer, L<Search::Elasticsearch::Serializer::JSON>,
+tries to choose the appropriate JSON backend, this module allows you to
+choose the L<JSON::XS> backend specifically.
+
+This class does L<Search::Elasticsearch::Role::Serializer::JSON>.
+
+=head1 SEE ALSO
+
+=over
+
+=item * L<Search::Elasticsearch::Serializer::JSON>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::Cpanel>
+
+=item * L<Search::Elasticsearch::Serializer::JSON::PP>
+
+=back
diff --git a/lib/Search/Elasticsearch/TestServer.pm b/lib/Search/Elasticsearch/TestServer.pm
new file mode 100644
index 0000000..59f486a
--- /dev/null
+++ b/lib/Search/Elasticsearch/TestServer.pm
@@ -0,0 +1,288 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::TestServer;
+
+use Moo;
+use Search::Elasticsearch();
+use POSIX 'setsid';
+use File::Temp();
+use IO::Socket();
+use HTTP::Tiny;
+
+use Search::Elasticsearch::Util qw(parse_params throw);
+use namespace::clean;
+
+has 'es_home'    => ( is => 'ro', default => $ENV{ES_HOME} );
+has 'es_version' => ( is => 'ro', default => $ENV{ES_VERSION} );
+has 'instances'  => ( is => 'ro', default => 1 );
+has 'http_port'  => ( is => 'ro', default => 9600 );
+has 'es_port'    => ( is => 'ro', default => 9700 );
+has 'pids'       => (
+    is        => 'ro',
+    default   => sub { [] },
+    clearer   => 1,
+    predicate => 1
+);
+
+has 'dirs' => ( is => 'ro', default => sub { [] } );
+has 'conf' => ( is => 'ro', default => sub { [] } );
+has '_starter_pid' => ( is => 'rw', required => 0, predicate => 1 );
+
+#===================================
+sub start {
+#===================================
+    my $self = shift;
+
+    my $home = $self->es_home
+        or throw( 'Param', "Missing required param <es_home>" );
+    $self->es_version
+        or throw( 'Param', "Missing required param <es_version>" );
+
+    my $instances = $self->instances;
+    my $port      = $self->http_port;
+    my $es_port   = $self->es_port;
+    my @http      = map { $port++ } ( 1 .. $instances );
+    my @transport = map { $es_port++ } ( 1 .. $instances );
+
+    $self->_check_ports( @http, @transport );
+
+    my $old_SIGINT = $SIG{INT};
+    $SIG{INT} = sub {
+        $self->shutdown;
+        if ( ref $old_SIGINT eq 'CODE' ) {
+            return $old_SIGINT->();
+        }
+        exit(1);
+    };
+
+    for ( 0 .. $instances - 1 ) {
+        my $dir = File::Temp->newdir();
+        push @{ $self->dirs }, $dir;
+        print "Starting node: http://127.0.0.1:$http[$_]\n";
+        $self->_start_node( $dir, $transport[$_], $http[$_] );
+    }
+
+    $self->_check_nodes(@http);
+    return [ map {"http://127.0.0.1:$_"} @http ];
+}
+
+#===================================
+sub _check_ports {
+#===================================
+    my $self = shift;
+    for my $port (@_) {
+        next unless IO::Socket::INET->new("127.0.0.1:$port");
+        throw( 'Param',
+                  "There is already a service running on 127.0.0.1:$port. "
+                . "Please shut it down before starting the test server" );
+    }
+}
+
+#===================================
+sub _check_nodes {
+#===================================
+    my $self = shift;
+    my $http = HTTP::Tiny->new;
+    for my $node (@_) {
+        print "Checking node: http://127.0.0.1:$node\n";
+        my $i = 20;
+        while (1) {
+            last
+                if $http->head("http://127.0.0.1:$node/")->{status} == 200;
+            throw( 'Cxn', "Couldn't connect to http://127.0.0.1:$node" )
+                unless $i--;
+            sleep 1;
+        }
+
+    }
+}
+
+#===================================
+sub _start_node {
+#===================================
+    my ( $self, $dir, $transport, $http ) = @_;
+
+    my $pid_file = File::Temp->new;
+    my @config = $self->_command_line( $pid_file, $dir, $transport, $http );
+
+    my $int_caught = 0;
+    {
+        local $SIG{INT} = sub { $int_caught++; };
+        defined( my $pid = fork )
+            or throw( 'Internal', "Couldn't fork a new process: $!" );
+        if ( $pid == 0 ) {
+            throw( 'Internal', "Can't start a new session: $!" )
+                if setsid == -1;
+            exec(@config) or die "Couldn't execute @config: $!";
+        }
+        else {
+            for ( 1 .. 5 ) {
+                last if -s $pid_file->filename();
+                sleep 1;
+            }
+            open my $pid_fh, '<', $pid_file->filename;
+            my $pid = <$pid_fh>;
+            throw( 'Internal', "No PID file found for Elasticsearch" )
+                unless $pid;
+            chomp $pid;
+            push @{ $self->{pids} }, $pid;
+            $self->_starter_pid($$);
+        }
+    }
+    $SIG{INT}->('INT') if $int_caught;
+}
+
+#===================================
+sub guarded_shutdown {
+#===================================
+    my $self = shift;
+    if ( $self->_has_starter_pid && $$ == $self->_starter_pid ) {
+        $self->shutdown();
+    }
+}
+
+#===================================
+sub shutdown {
+#===================================
+    my $self = shift;
+    local $?;
+
+    return unless $self->has_pids;
+
+    my $pids = $self->pids;
+    $self->clear_pids;
+    return unless @$pids;
+
+    kill 9, @$pids;
+    $self->clear_dirs;
+}
+
+#===================================
+sub _command_line {
+#===================================
+    my ( $self, $pid_file, $dir, $transport, $http ) = @_;
+
+    my $version = $self->es_version;
+    my $class   = "Search::Elasticsearch::Client::${version}::TestServer";
+    eval "require $class" || die $@;
+
+    return $class->command_line(@_);
+}
+
+#===================================
+sub clear_dirs {
+#===================================
+    my $self = shift;
+    @{ $self->dirs() } = ();
+}
+
+#===================================
+sub DEMOLISH { shift->guarded_shutdown }
+#===================================
+
+1;
+
+# ABSTRACT: A helper class to launch Elasticsearch nodes
+
+=head1 DESCRIPTION
+
+The L<Search::Elasticsearch::TestServer> class can be used to launch one or more
+instances of Elasticsearch for testing purposes.  The nodes will
+be shutdown automatically.
+
+=head1 SYNOPSIS
+
+    use Search::Elasticsearch;
+    use Search::Elasticsearch::TestServer;
+
+    my $server = Search::Elasticsearch::TestServer->new(
+        es_home    => '/path/to/elasticsearch',  # defaults to $ENV{ES_HOME}
+        es_version => '6_0'                      # defaults to $ENV{ES_VERSION}
+    );
+
+    my $nodes = $server->start;
+    my $es    = Search::Elasticsearch->new( nodes => $nodes );
+    # run tests
+    $server->shutdown;
+
+=head1 METHODS
+
+=head2 C<new()>
+
+    my $server = Search::Elasticsearch::TestServer->new(
+        es_home    => '/path/to/elasticsearch',
+        es_version => '6_0',
+        instances => 1,
+        http_port => 9600,
+        es_port   => 9700,
+        conf      => ['attr.foo=bar'],
+    );
+
+Params:
+
+=over
+
+=item * C<es_home>
+
+Required. Must point to the Elasticsearch home directory, which contains
+C<./bin/elasticsearch>.  Defaults to C<$ENV{ES_HOME}>
+
+=item * C<es_version>
+
+Required. Accepts a version of the client, eg `6_0`, `5_0`, `2_0`, `1_0`, `0_90`.
+Defaults to C<$ENV{ES_VERSION}>.
+
+=item * C<instances>
+
+The number of nodes to start. Defaults to 1
+
+=item * C<http_port>
+
+The port to use for HTTP. If multiple instances are started, the C<http_port>
+will be incremented for each subsequent instance. Defaults to 9600.
+
+=item * C<es_port>
+
+The port to use for Elasticsearch's internal transport. If multiple instances
+are started, the C<es_port> will be incremented for each subsequent instance.
+Defaults to 9700
+
+=item * C<conf>
+
+An array containing any extra startup options that should be passed
+to Elasticsearch.
+
+=back
+
+=head1 C<start()>
+
+    $nodes = $server->start;
+
+Starts the required instances and returns an array ref containing the IP
+and port of each node, suitable for passing to L<Search::Elasticsearch/new()>:
+
+    $es = Search::Elasticsearch->new( nodes => $nodes );
+
+=head1 C<shutdown()>
+
+    $server->shutdown;
+
+Kills the running instances.  This will be called automatically when
+C<$server> goes out of scope or if the program receives a C<SIGINT>.
+
+
diff --git a/lib/Search/Elasticsearch/Transport.pm b/lib/Search/Elasticsearch/Transport.pm
new file mode 100644
index 0000000..7381f77
--- /dev/null
+++ b/lib/Search/Elasticsearch/Transport.pm
@@ -0,0 +1,150 @@
+# Licensed to Elasticsearch B.V under one or more agreements.
+# Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+# See the LICENSE file in the project root for more information
+
+package Search::Elasticsearch::Transport;
+
+use Moo;
+
+use URI();
+use Time::HiRes qw(time);
+use Try::Tiny;
+use Search::Elasticsearch::Util qw(upgrade_error);
+use namespace::clean;
+
+with 'Search::Elasticsearch::Role::Is_Sync',
+    'Search::Elasticsearch::Role::Transport';
+
+#===================================
+sub perform_request {
+#===================================
+    my $self   = shift;
+    my $params = $self->tidy_request(@_);
+    my $pool   = $self->cxn_pool;
+    my $logger = $self->logger;
+
+    my ( $code, $response, $cxn, $error );
+
+    try {
+        $cxn = $pool->next_cxn;
+        my $start = time();
+        $logger->trace_request( $cxn, $params );
+
+        ( $code, $response ) = $cxn->perform_request($params);
+        $pool->request_ok($cxn);
+        $logger->trace_response( $cxn, $code, $response, time() - $start );
+    }
+    catch {
+        $error = upgrade_error(
+            $_,
+            {   request     => $params,
+                status_code => $code,
+                body        => $response
+            }
+        );
+    };
+
+    if ($error) {
+        if ( $pool->request_failed( $cxn, $error ) ) {
+            $logger->debugf( "[%s] %s", $cxn->stringify, "$error" );
+            $logger->info('Retrying request on a new cxn');
+            return $self->perform_request($params);
+        }
+
+        $logger->trace_error( $cxn, $error );
+        $error->is('NoNodes')
+            ? $logger->throw_critical($error)
+            : $logger->throw_error($error);
+    }
+
+    return $response;
+}
+
+1;
+
+#ABSTRACT: Provides interface between the client class and the Elasticsearch cluster
+
+=head1 DESCRIPTION
+
+The Transport class manages the request cycle. It receives parsed requests
+from the (user-facing) client class, and tries to execute the request on a
+node in the cluster, retrying a request if necessary.
+
+This class does L<Search::Elasticsearch::Role::Transport> and
+L<Search::Elasticsearch::Role::Is_Sync>.
+
+=head1 CONFIGURATION
+
+=head2 C<send_get_body_as>
+
+    $e = Search::Elasticsearch->new(
+        send_get_body_as => 'POST'
+    );
+
+Certain endpoints like L<Search::Elasticsearch::Client::6_0::Direct/search()>
+default to using a C<GET> method, even when they include a request body.
+Some proxy servers do not support C<GET> requests with a body.  To work
+around this, the C<send_get_body_as>  parameter accepts the following:
+
+=over
+
+=item * C<GET>
+
+The default.  Request bodies are sent as C<GET> requests.
+
+=item * C<POST>
+
+The method is changed to C<POST> when a body is present.
+
+=item * C<source>
+
+The body is encoded as JSON and added to the query string as the C<source>
+parameter.  This has the advantage of still being a C<GET> request (for those
+filtering on request method) but has the disadvantage of being restricted
+in size.  The limit depends on the proxies between the client and
+Elasticsearch, but usually is around 4kB.
+
+=back
+
+=head1 METHODS
+
+=head2 C<perform_request()>
+
+Raw requests can be executed using the transport class as follows:
+
+    $result = $e->transport->perform_request(
+        method => 'POST',
+        path   => '/_search',
+        qs     => { from => 0, size => 10 },
+        body   => {
+            query => {
+                match => {
+                    title => "Elasticsearch clients"
+                }
+            }
+        }
+    );
+
+Other than the C<method>, C<path>, C<qs> and C<body> parameters, which
+should be self-explanatory, it also accepts:
+
+=over
+
+=item C<ignore>
+
+The HTTP error codes which should be ignored instead of throwing an error,
+eg C<404 NOT FOUND>:
+
+    $result = $e->transport->perform_request(
+        method => 'GET',
+        path   => '/index/type/id'
+        ignore => [404],
+    );
+
+=item C<serialize>
+
+Whether the C<body> should be serialized in the standard way (as plain
+JSON) or using the special I<bulk> format:  C<"std"> or C<"bulk">.
+
+=back
+
diff --git a/lib/Search/Elasticsearch/Transport/Async.pm b/lib/Search/Elasticsearch/Transport/Async.pm
new file mode 100644
index 0000000..85463b8
--- /dev/null
+++ b/lib/Search/Elasticsearch/Transport/Async.pm
@@ -0,0 +1,180 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Transport::Async;
+
+use Moo;
+with 'Search::Elasticsearch::Role::Is_Async',
+    'Search::Elasticsearch::Role::Transport';
+
+use Time::HiRes qw(time);
+use Search::Elasticsearch::Util qw(upgrade_error);
+use Promises qw(deferred);
+use namespace::clean;
+
+#===================================
+sub perform_request {
+#===================================
+    my $self   = shift;
+    my $params = $self->tidy_request(@_);
+    my $pool   = $self->cxn_pool;
+    my $logger = $self->logger;
+
+    my $deferred = deferred;
+
+    my ( $start, $cxn );
+    $pool->next_cxn
+
+        # perform request
+        ->then(
+        sub {
+            $cxn   = shift;
+            $start = time();
+            $cxn->perform_request($params);
+        }
+        )
+
+        # log request regardless of success/failure
+        ->finally( sub { $logger->trace_request( $cxn, $params ) } )
+
+        ->done(
+        # request succeeded
+        sub {
+            my ( $code, $response ) = @_;
+            $pool->request_ok($cxn);
+            $logger->trace_response( $cxn, $code, $response,
+                time() - $start );
+            $deferred->resolve($response);
+        },
+
+        # request failed
+        sub {
+            my $error = upgrade_error( shift(), { request => $params } );
+            if ( $pool->request_failed( $cxn, $error ) ) {
+
+                # log failed, then retry
+                $logger->debugf( "[%s] %s", $cxn->stringify, "$error" );
+                $logger->info('Retrying request on a new cxn');
+                return $self->perform_request($params)->done(
+                    sub { $deferred->resolve(@_) },
+                    sub { $deferred->reject(@_) }
+                );
+            }
+            if ($cxn) {
+                $logger->trace_request( $cxn, $params );
+                $logger->trace_error( $cxn, $error );
+            }
+            $error->is('NoNodes')
+                ? $logger->critical($error)
+                : $logger->error($error);
+            $deferred->reject($error);
+        }
+        );
+    return $deferred->promise;
+}
+
+1;
+
+#ABSTRACT: Provides async interface between the client class and the Elasticsearch cluster
+
+=head1 DESCRIPTION
+
+The Async::Transport class manages the request cycle. It receives parsed requests
+from the (user-facing) client class, and tries to execute the request on a
+node in the cluster, retrying a request if necessary.
+
+This class does L<Search::Elasticsearch::Role::Transport> and
+L<Search::Elasticsearch::Role::Is_Async>.
+
+=head1 CONFIGURATION
+
+=head2 C<send_get_body_as>
+
+    $e = Search::Elasticsearch::Async->new(
+        send_get_body_as => 'POST'
+    );
+
+Certain endpoints like L<Search::Elasticsearch::Client::6_0::Direct/search()>
+default to using a C<GET> method, even when they include a request body.
+Some proxy servers do not support C<GET> requests with a body.  To work
+around this, the C<send_get_body_as>  parameter accepts the following:
+
+=over
+
+=item * C<GET>
+
+The default.  Request bodies are sent as C<GET> requests.
+
+=item * C<POST>
+
+The method is changed to C<POST> when a body is present.
+
+=item * C<source>
+
+The body is encoded as JSON and added to the query string as the C<source>
+parameter.  This has the advantage of still being a C<GET> request (for those
+filtering on request method) but has the disadvantage of being restricted
+in size.  The limit depends on the proxies between the client and
+Elasticsearch, but usually is around 4kB.
+
+=back
+
+=head1 METHODS
+
+=head2 C<perform_request()>
+
+Raw requests can be executed using the transport class as follows:
+
+    $promise = $e->transport->perform_request(
+        method => 'POST',
+        path   => '/_search',
+        qs     => { from => 0, size => 10 },
+        body   => {
+            query => {
+                match => {
+                    title => "Elasticsearch clients"
+                }
+            }
+        }
+    );
+
+C<perform_request()> returns a L<Promise> object, which will be resolved
+(success) or rejected (error) at some point in the future.
+
+Other than the C<method>, C<path>, C<qs> and C<body> parameters, which
+should be self-explanatory, it also accepts:
+
+=over
+
+=item C<ignore>
+
+The HTTP error codes which should be ignored instead of throwing an error,
+eg C<404 NOT FOUND>:
+
+    $promise = $e->transport->perform_request(
+        method => 'GET',
+        path   => '/index/type/id'
+        ignore => [404],
+    );
+
+=item C<serialize>
+
+Whether the C<body> should be serialized in the standard way (as plain
+JSON) or using the special I<bulk> format:  C<"std"> or C<"bulk">.
+
+=back
+
diff --git a/lib/Search/Elasticsearch/Util.pm b/lib/Search/Elasticsearch/Util.pm
new file mode 100644
index 0000000..fc96adc
--- /dev/null
+++ b/lib/Search/Elasticsearch/Util.pm
@@ -0,0 +1,125 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package Search::Elasticsearch::Util;
+
+use Moo;
+use Search::Elasticsearch::Error();
+use Scalar::Util qw(blessed);
+use Module::Runtime qw(compose_module_name is_module_name use_module);
+use Sub::Exporter -setup => {
+    exports => [ qw(
+            parse_params
+            to_list
+            load_plugin
+            new_error
+            throw
+            upgrade_error
+            is_compat
+            )
+    ]
+};
+
+#===================================
+sub to_list {
+#===================================
+    grep {defined} ref $_[0] eq 'ARRAY' ? @{ $_[0] } : @_;
+}
+
+#===================================
+sub parse_params {
+#===================================
+    my $self = shift;
+    my %params;
+    if ( @_ % 2 ) {
+        throw(
+            "Param",
+            'Expecting a HASH ref or a list of key-value pairs',
+            { params => \@_ }
+        ) unless ref $_[0] eq 'HASH';
+        %params = %{ shift() };
+    }
+    else {
+        %params = @_;
+    }
+    return ( $self, \%params );
+}
+
+#===================================
+sub load_plugin {
+#===================================
+    my ( $base, $spec ) = @_;
+    $spec ||= "+$base";
+    return $spec if blessed $spec;
+
+    my ( $class, $version );
+    if ( ref $spec eq 'ARRAY' ) {
+        ( $class, $version ) = @$spec;
+    }
+    else {
+        $class = $spec;
+    }
+
+    unless ( $class =~ s/\A\+// ) {
+        $class = compose_module_name( $base, $class );
+    }
+
+    $version ? use_module( $class, $version ) : use_module($class);
+}
+
+#===================================
+sub throw {
+#===================================
+    my ( $type, $msg, $vars ) = @_;
+    die Search::Elasticsearch::Error->new( $type, $msg, $vars, 1 );
+}
+
+#===================================
+sub new_error {
+#===================================
+    my ( $type, $msg, $vars ) = @_;
+    return Search::Elasticsearch::Error->new( $type, $msg, $vars, 1 );
+}
+
+#===================================
+sub upgrade_error {
+#===================================
+    my ( $error, $vars ) = @_;
+    return
+        ref($error) && $error->isa('Search::Elasticsearch::Error')
+        ? $error
+        : Search::Elasticsearch::Error->new( "Internal", $error, $vars || {},
+        1 );
+}
+
+#===================================
+sub is_compat {
+#===================================
+    my ( $attr, $one, $two ) = @_;
+    my $role
+        = $one->does('Search::Elasticsearch::Role::Is_Sync')
+        ? 'Search::Elasticsearch::Role::Is_Sync'
+        : 'Search::Elasticsearch::Role::Is_Async';
+
+    return if eval { $two->does($role); };
+    my $class = ref($two) || $two;
+    die "$attr ($class) does not do $role";
+}
+
+1;
+
+# ABSTRACT: A utility class for internal use by Search::Elasticsearch
diff --git a/t/10_Basic/10_load.t b/t/10_Basic/10_load.t
new file mode 100644
index 0000000..0bb2fa2
--- /dev/null
+++ b/t/10_Basic/10_load.t
@@ -0,0 +1,34 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+BEGIN { use_ok('Search::Elasticsearch') }
+
+my ( $e, $p, $t );
+
+ok $e = Search::Elasticsearch->new(), "new client";
+ok $e->does('Search::Elasticsearch::Role::Client::Direct'),
+    "client does Search::Elasticsearch::Role::Client::Direct";
+isa_ok $t = $e->transport, 'Search::Elasticsearch::Transport', "transport";
+isa_ok $p = $t->cxn_pool, 'Search::Elasticsearch::CxnPool::Static',
+    "cxn_pool";
+isa_ok $p->cxn_factory, 'Search::Elasticsearch::Cxn::Factory', "cxn_factory";
+isa_ok $e->logger, 'Search::Elasticsearch::Logger::LogAny', "logger";
+
+done_testing;
+
diff --git a/t/10_Basic_Async/10_load.t b/t/10_Basic_Async/10_load.t
new file mode 100644
index 0000000..771de6b
--- /dev/null
+++ b/t/10_Basic_Async/10_load.t
@@ -0,0 +1,36 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+BEGIN { use_ok('Search::Elasticsearch::Async') }
+
+my ( $e, $p, $t );
+
+ok $e = Search::Elasticsearch::Async->new(), "new client";
+ok $e->does('Search::Elasticsearch::Role::Client::Direct'),
+    "client does Search::Elasticsearch::Role::Client::Direct";
+
+isa_ok $t = $e->transport, 'Search::Elasticsearch::Transport::Async',
+    "transport";
+isa_ok $p = $t->cxn_pool, 'Search::Elasticsearch::CxnPool::Async::Static',
+    "cxn_pool";
+isa_ok $p->cxn_factory, 'Search::Elasticsearch::Cxn::Factory', "cxn_factory";
+isa_ok $e->logger, 'Search::Elasticsearch::Logger::LogAny', "logger";
+
+done_testing;
+
diff --git a/t/20_Serializer/10_load_cpanel.t b/t/20_Serializer/10_load_cpanel.t
new file mode 100644
index 0000000..1ea9b57
--- /dev/null
+++ b/t/20_Serializer/10_load_cpanel.t
@@ -0,0 +1,32 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+use Search::Elasticsearch;
+
+my $s = Search::Elasticsearch->new()->transport->serializer->JSON;
+
+SKIP: {
+    skip 'Cpanel::JSON::XS not installed' => 1
+        unless eval { require Cpanel::JSON::XS; 1 };
+
+    isa_ok $s, "Cpanel::JSON::XS", 'Cpanel';
+}
+
+done_testing;
+
diff --git a/t/20_Serializer/11_load_xs.t b/t/20_Serializer/11_load_xs.t
new file mode 100644
index 0000000..ed963cd
--- /dev/null
+++ b/t/20_Serializer/11_load_xs.t
@@ -0,0 +1,37 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+use lib sub {
+    die "No Cpanel" if $_[1] =~ m{Cpanel/JSON/XS.pm$};
+    return undef;
+};
+
+use Search::Elasticsearch;
+
+my $s = Search::Elasticsearch->new()->transport->serializer->JSON;
+
+SKIP: {
+    skip 'JSON::XS not installed' => 1
+        unless eval { require JSON::XS; 1 };
+
+    isa_ok $s, "JSON::XS", 'JSON::XS';
+}
+
+done_testing;
+
diff --git a/t/20_Serializer/12_load_pp.t b/t/20_Serializer/12_load_pp.t
new file mode 100644
index 0000000..ec374de
--- /dev/null
+++ b/t/20_Serializer/12_load_pp.t
@@ -0,0 +1,38 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+use lib sub {
+    die "No Cpanel"   if $_[1] =~ m{Cpanel/JSON/XS.pm$};
+    die "No JSON::XS" if $_[1] =~ m{JSON/XS.pm$};
+    return undef;
+};
+
+use Search::Elasticsearch;
+
+my $s = Search::Elasticsearch->new()->transport->serializer->JSON;
+
+SKIP: {
+    skip 'JSON::PP not installed' => 1
+        unless eval { require JSON::PP; 1 };
+
+    isa_ok $s, "JSON::PP", 'JSON::PP';
+}
+
+done_testing;
+
diff --git a/t/20_Serializer/13_preload_cpanel.t b/t/20_Serializer/13_preload_cpanel.t
new file mode 100644
index 0000000..ac7945d
--- /dev/null
+++ b/t/20_Serializer/13_preload_cpanel.t
@@ -0,0 +1,34 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+BEGIN {
+    eval { require Cpanel::JSON::XS; 1 } or do {
+        plan skip_all => 'Cpanel::JSON::XS not installed';
+        done_testing;
+        exit;
+        }
+}
+
+use Search::Elasticsearch;
+
+my $s = Search::Elasticsearch->new()->transport->serializer->JSON;
+isa_ok $s, "Cpanel::JSON::XS", 'Cpanel';
+
+done_testing;
+
diff --git a/t/20_Serializer/14_preload_xs.t b/t/20_Serializer/14_preload_xs.t
new file mode 100644
index 0000000..88286e3
--- /dev/null
+++ b/t/20_Serializer/14_preload_xs.t
@@ -0,0 +1,34 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+BEGIN {
+    eval { require JSON::XS; 1 } or do {
+        plan skip_all => 'JSON::XS not installed';
+        done_testing;
+        exit;
+        }
+}
+
+use Search::Elasticsearch;
+
+my $s = Search::Elasticsearch->new()->transport->serializer->JSON;
+isa_ok $s, "JSON::XS", 'JSON::XS';
+
+done_testing;
+
diff --git a/t/20_Serializer/20_xs_encode_decode.t b/t/20_Serializer/20_xs_encode_decode.t
new file mode 100644
index 0000000..d95cc51
--- /dev/null
+++ b/t/20_Serializer/20_xs_encode_decode.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::XS; 1 } or do {
+    plan skip_all => 'JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::XS';
+do './t/20_Serializer/encode_pretty.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer/21_xs_encode_bulk.t b/t/20_Serializer/21_xs_encode_bulk.t
new file mode 100644
index 0000000..f44b543
--- /dev/null
+++ b/t/20_Serializer/21_xs_encode_bulk.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::XS; 1 } or do {
+    plan skip_all => 'JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::XS';
+do './t/20_Serializer/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer/22_xs_encode_pretty.t b/t/20_Serializer/22_xs_encode_pretty.t
new file mode 100644
index 0000000..f44b543
--- /dev/null
+++ b/t/20_Serializer/22_xs_encode_pretty.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::XS; 1 } or do {
+    plan skip_all => 'JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::XS';
+do './t/20_Serializer/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer/30_cpanel_encode_decode.t b/t/20_Serializer/30_cpanel_encode_decode.t
new file mode 100644
index 0000000..2ebe5e0
--- /dev/null
+++ b/t/20_Serializer/30_cpanel_encode_decode.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require Cpanel::JSON::XS; 1 } or do {
+    plan skip_all => 'Cpanel::JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::Cpanel';
+do './t/20_Serializer/encode_pretty.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer/31_cpanel_encode_bulk.t b/t/20_Serializer/31_cpanel_encode_bulk.t
new file mode 100644
index 0000000..c27a1a7
--- /dev/null
+++ b/t/20_Serializer/31_cpanel_encode_bulk.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require Cpanel::JSON::XS; 1 } or do {
+    plan skip_all => 'Cpanel::JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::Cpanel';
+do './t/20_Serializer/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer/32_cpanel_encode_pretty.t b/t/20_Serializer/32_cpanel_encode_pretty.t
new file mode 100644
index 0000000..c27a1a7
--- /dev/null
+++ b/t/20_Serializer/32_cpanel_encode_pretty.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require Cpanel::JSON::XS; 1 } or do {
+    plan skip_all => 'Cpanel::JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::Cpanel';
+do './t/20_Serializer/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer/40_pp_encode_decode.t b/t/20_Serializer/40_pp_encode_decode.t
new file mode 100644
index 0000000..230ee20
--- /dev/null
+++ b/t/20_Serializer/40_pp_encode_decode.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::PP; 1 } or do {
+    plan skip_all => 'JSON::PP not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::PP';
+do './t/20_Serializer/encode_pretty.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer/41_pp_encode_bulk.t b/t/20_Serializer/41_pp_encode_bulk.t
new file mode 100644
index 0000000..5fbfab8
--- /dev/null
+++ b/t/20_Serializer/41_pp_encode_bulk.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::PP; 1 } or do {
+    plan skip_all => 'JSON::PP not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::PP';
+do './t/20_Serializer/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer/42_pp_encode_pretty.t b/t/20_Serializer/42_pp_encode_pretty.t
new file mode 100644
index 0000000..5fbfab8
--- /dev/null
+++ b/t/20_Serializer/42_pp_encode_pretty.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::PP; 1 } or do {
+    plan skip_all => 'JSON::PP not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::PP';
+do './t/20_Serializer/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer/encode_bulk.pl b/t/20_Serializer/encode_bulk.pl
new file mode 100644
index 0000000..ab1b1ac
--- /dev/null
+++ b/t/20_Serializer/encode_bulk.pl
@@ -0,0 +1,60 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use Search::Elasticsearch;
+
+our $JSON_BACKEND;
+my $utf8_bytes = "彈性搜索";
+my $utf8_str   = $utf8_bytes;
+utf8::decode($utf8_str);
+my $hash = { "foo" => "$utf8_str" };
+my $arr       = [ $hash, $hash ];
+my $json_hash = qq({"foo":"$utf8_bytes"});
+my $json_arr  = qq($json_hash\n$json_hash\n);
+
+isa_ok my $s
+    = Search::Elasticsearch->new( serializer => $JSON_BACKEND )
+    ->transport->serializer,
+    "Search::Elasticsearch::Serializer::$JSON_BACKEND", 'Serializer';
+
+is $s->encode_bulk(), undef,    #
+    'Enc - No args returns undef';
+is $s->encode_bulk(undef), undef,    #
+    'Enc - Undef returns undef';
+is $s->encode_bulk(''), '',          #
+    'Enc - Empty string returns same';
+is $s->encode_bulk('foo'), 'foo',    #
+    'Enc - String returns same';
+is $s->encode_bulk($utf8_str), $utf8_bytes,    #
+    'Enc - Unicode string returns encoded';
+is $s->encode_bulk($utf8_bytes), $utf8_bytes,    #
+    'Enc - Unicode bytes returns same';
+is $s->encode_bulk($arr), $json_arr,             #
+    'Enc - Array returns JSON';
+is $s->encode_bulk( [ $json_hash, $json_hash ] ), $json_arr,    #
+    'Enc - Array of strings';
+throws_ok { $s->encode_bulk($hash) } qr/must be an array/,      #
+    'Enc - Hash dies';
+throws_ok { $s->encode_bulk( \$utf8_str ) } qr/Serializer/,     #
+    'Enc - scalar ref dies';
+throws_ok { $s->encode_bulk( [ \$utf8_str ] ) } qr/Serializer/,    #
+    'Enc - array of scalar ref dies';
+
+done_testing;
diff --git a/t/20_Serializer/encode_decode.pl b/t/20_Serializer/encode_decode.pl
new file mode 100644
index 0000000..8116f31
--- /dev/null
+++ b/t/20_Serializer/encode_decode.pl
@@ -0,0 +1,62 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use Search::Elasticsearch;
+
+our $JSON_BACKEND;
+my $utf8_bytes = "彈性搜索";
+my $utf8_str   = $utf8_bytes;
+utf8::decode($utf8_str);
+my $hash      = { "foo" => "$utf8_str" };
+my $arr       = [$hash];
+my $json_hash = qq({"foo":"$utf8_bytes"});
+my $json_arr  = qq([$json_hash]);
+
+isa_ok my $s
+    = Search::Elasticsearch->new( serializer => $JSON_BACKEND )
+    ->transport->serializer,
+    "Search::Elasticsearch::Serializer::$JSON_BACKEND", 'Serializer';
+
+is $s->mime_type, 'application/json', 'Mime type is JSON';
+
+# encode
+is $s->encode(), undef, 'Enc - No args returns undef';
+is $s->encode(undef), undef, 'Enc - Undef returns undef';
+is $s->encode(''),    '',    'Enc - Empty string returns same';
+is $s->encode('foo'), 'foo', 'Enc - String returns same';
+is $s->encode($utf8_str), $utf8_bytes, 'Enc - Unicode string returns encoded';
+is $s->encode($utf8_bytes), $utf8_bytes, 'Enc - Unicode bytes returns same';
+is $s->encode($hash),       $json_hash,  'Enc - Hash returns JSON';
+is $s->encode($arr),        $json_arr,   'Enc - Array returns JSON';
+throws_ok { $s->encode( \$utf8_str ) } qr/Serializer/,
+    'Enc - scalar ref dies';
+
+# decode
+is $s->decode(), undef, 'Dec - No args returns undef';
+is $s->decode(undef), undef, 'Dec - Undef returns undef';
+is $s->decode(''),    '',    'Dec - Empty string returns same';
+is $s->decode('foo'), 'foo', 'Dec - String returns same';
+is $s->decode($utf8_bytes), $utf8_str, 'Dec - Unicode bytes returns decoded';
+is $s->decode($utf8_str),   $utf8_str, 'Dec - Unicode string returns same';
+cmp_deeply $s->decode($json_hash), $hash, 'Dec - JSON returns hash';
+cmp_deeply $s->decode($json_arr),  $arr,  'Dec - JSON returns array';
+throws_ok { $s->decode('{') } qr/Serializer/, 'Dec - invalid JSON dies';
+
+done_testing;
diff --git a/t/20_Serializer/encode_pretty.pl b/t/20_Serializer/encode_pretty.pl
new file mode 100644
index 0000000..83b9e85
--- /dev/null
+++ b/t/20_Serializer/encode_pretty.pl
@@ -0,0 +1,69 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use Search::Elasticsearch;
+
+our $JSON_BACKEND;
+my $utf8_bytes = "彈性搜索";
+my $utf8_str   = $utf8_bytes;
+utf8::decode($utf8_str);
+my $hash      = { "foo" => "$utf8_str" };
+my $arr       = [$hash];
+my $json_hash = <<JSON;
+{
+   "foo" : "$utf8_bytes"
+}
+JSON
+
+my $json_arr = <<JSON;
+[
+   {
+      "foo" : "$utf8_bytes"
+   }
+]
+JSON
+
+isa_ok my $s
+    = Search::Elasticsearch->new( serializer => $JSON_BACKEND )
+    ->transport->serializer,
+    "Search::Elasticsearch::Serializer::$JSON_BACKEND", 'Serializer';
+
+# encode
+is_pretty( [],            undef,       'Enc - No args returns undef' );
+is_pretty( [undef],       undef,       'Enc - Undef returns undef' );
+is_pretty( [''],          '',          'Enc - Empty string returns same' );
+is_pretty( ['foo'],       'foo',       'Enc - String returns same' );
+is_pretty( [$utf8_str],   $utf8_bytes, 'Enc - Unicode string returns encoded' );
+is_pretty( [$utf8_bytes], $utf8_bytes, 'Enc - Unicode bytes returns same' );
+is_pretty( [$hash],       $json_hash,  'Enc - Hash returns JSON' );
+is_pretty( [$arr],        $json_arr,   'Enc - Array returns JSON' );
+
+throws_ok { $s->encode_pretty( \$utf8_str ) } qr/Serializer/,    #
+    'Enc - scalar ref dies';
+
+sub is_pretty {
+    my ( $arg, $expect, $desc ) = @_;
+    my $got = $s->encode_pretty(@$arg);
+    defined $got    and $got =~ s/^\s+//gm;
+    defined $expect and $expect =~ s/^\s+//gm;
+    is $got, $expect, $desc;
+}
+
+done_testing;
diff --git a/t/20_Serializer_Async/10_load_cpanel.t b/t/20_Serializer_Async/10_load_cpanel.t
new file mode 100644
index 0000000..0fd4dce
--- /dev/null
+++ b/t/20_Serializer_Async/10_load_cpanel.t
@@ -0,0 +1,32 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+use Search::Elasticsearch::Async;
+
+my $s = Search::Elasticsearch::Async->new()->transport->serializer->JSON;
+
+SKIP: {
+    skip 'Cpanel::JSON::XS not installed' => 1
+        unless eval { require Cpanel::JSON::XS; 1 };
+
+    isa_ok $s, "Cpanel::JSON::XS", 'Cpanel';
+}
+
+done_testing;
+
diff --git a/t/20_Serializer_Async/11_load_xs.t b/t/20_Serializer_Async/11_load_xs.t
new file mode 100644
index 0000000..ed963cd
--- /dev/null
+++ b/t/20_Serializer_Async/11_load_xs.t
@@ -0,0 +1,37 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+use lib sub {
+    die "No Cpanel" if $_[1] =~ m{Cpanel/JSON/XS.pm$};
+    return undef;
+};
+
+use Search::Elasticsearch;
+
+my $s = Search::Elasticsearch->new()->transport->serializer->JSON;
+
+SKIP: {
+    skip 'JSON::XS not installed' => 1
+        unless eval { require JSON::XS; 1 };
+
+    isa_ok $s, "JSON::XS", 'JSON::XS';
+}
+
+done_testing;
+
diff --git a/t/20_Serializer_Async/12_load_pp.t b/t/20_Serializer_Async/12_load_pp.t
new file mode 100644
index 0000000..ec374de
--- /dev/null
+++ b/t/20_Serializer_Async/12_load_pp.t
@@ -0,0 +1,38 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+use lib sub {
+    die "No Cpanel"   if $_[1] =~ m{Cpanel/JSON/XS.pm$};
+    die "No JSON::XS" if $_[1] =~ m{JSON/XS.pm$};
+    return undef;
+};
+
+use Search::Elasticsearch;
+
+my $s = Search::Elasticsearch->new()->transport->serializer->JSON;
+
+SKIP: {
+    skip 'JSON::PP not installed' => 1
+        unless eval { require JSON::PP; 1 };
+
+    isa_ok $s, "JSON::PP", 'JSON::PP';
+}
+
+done_testing;
+
diff --git a/t/20_Serializer_Async/13_preload_cpanel.t b/t/20_Serializer_Async/13_preload_cpanel.t
new file mode 100644
index 0000000..ac7945d
--- /dev/null
+++ b/t/20_Serializer_Async/13_preload_cpanel.t
@@ -0,0 +1,34 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+BEGIN {
+    eval { require Cpanel::JSON::XS; 1 } or do {
+        plan skip_all => 'Cpanel::JSON::XS not installed';
+        done_testing;
+        exit;
+        }
+}
+
+use Search::Elasticsearch;
+
+my $s = Search::Elasticsearch->new()->transport->serializer->JSON;
+isa_ok $s, "Cpanel::JSON::XS", 'Cpanel';
+
+done_testing;
+
diff --git a/t/20_Serializer_Async/14_preload_xs.t b/t/20_Serializer_Async/14_preload_xs.t
new file mode 100644
index 0000000..88286e3
--- /dev/null
+++ b/t/20_Serializer_Async/14_preload_xs.t
@@ -0,0 +1,34 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+BEGIN {
+    eval { require JSON::XS; 1 } or do {
+        plan skip_all => 'JSON::XS not installed';
+        done_testing;
+        exit;
+        }
+}
+
+use Search::Elasticsearch;
+
+my $s = Search::Elasticsearch->new()->transport->serializer->JSON;
+isa_ok $s, "JSON::XS", 'JSON::XS';
+
+done_testing;
+
diff --git a/t/20_Serializer_Async/20_xs_encode_decode.t b/t/20_Serializer_Async/20_xs_encode_decode.t
new file mode 100644
index 0000000..7fe8c7b
--- /dev/null
+++ b/t/20_Serializer_Async/20_xs_encode_decode.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::XS; 1 } or do {
+    plan skip_all => 'JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::XS';
+do './t/20_Serializer_Async/encode_pretty.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer_Async/21_xs_encode_bulk.t b/t/20_Serializer_Async/21_xs_encode_bulk.t
new file mode 100644
index 0000000..dc30bf2
--- /dev/null
+++ b/t/20_Serializer_Async/21_xs_encode_bulk.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::XS; 1 } or do {
+    plan skip_all => 'JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::XS';
+do './t/20_Serializer_Async/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer_Async/22_xs_encode_pretty.t b/t/20_Serializer_Async/22_xs_encode_pretty.t
new file mode 100644
index 0000000..dc30bf2
--- /dev/null
+++ b/t/20_Serializer_Async/22_xs_encode_pretty.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::XS; 1 } or do {
+    plan skip_all => 'JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::XS';
+do './t/20_Serializer_Async/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer_Async/30_cpanel_encode_decode.t b/t/20_Serializer_Async/30_cpanel_encode_decode.t
new file mode 100644
index 0000000..145dac8
--- /dev/null
+++ b/t/20_Serializer_Async/30_cpanel_encode_decode.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require Cpanel::JSON::XS; 1 } or do {
+    plan skip_all => 'Cpanel::JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::Cpanel';
+do './t/20_Serializer_Async/encode_pretty.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer_Async/31_cpanel_encode_bulk.t b/t/20_Serializer_Async/31_cpanel_encode_bulk.t
new file mode 100644
index 0000000..228c458
--- /dev/null
+++ b/t/20_Serializer_Async/31_cpanel_encode_bulk.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require Cpanel::JSON::XS; 1 } or do {
+    plan skip_all => 'Cpanel::JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::Cpanel';
+do './t/20_Serializer_Async/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer_Async/32_cpanel_encode_pretty.t b/t/20_Serializer_Async/32_cpanel_encode_pretty.t
new file mode 100644
index 0000000..228c458
--- /dev/null
+++ b/t/20_Serializer_Async/32_cpanel_encode_pretty.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require Cpanel::JSON::XS; 1 } or do {
+    plan skip_all => 'Cpanel::JSON::XS not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::Cpanel';
+do './t/20_Serializer_Async/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer_Async/40_pp_encode_decode.t b/t/20_Serializer_Async/40_pp_encode_decode.t
new file mode 100644
index 0000000..c2b2a83
--- /dev/null
+++ b/t/20_Serializer_Async/40_pp_encode_decode.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::PP; 1 } or do {
+    plan skip_all => 'JSON::PP not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::PP';
+do './t/20_Serializer_Async/encode_pretty.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer_Async/41_pp_encode_bulk.t b/t/20_Serializer_Async/41_pp_encode_bulk.t
new file mode 100644
index 0000000..ec2728c
--- /dev/null
+++ b/t/20_Serializer_Async/41_pp_encode_bulk.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::PP; 1 } or do {
+    plan skip_all => 'JSON::PP not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::PP';
+do './t/20_Serializer_Async/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer_Async/42_pp_encode_pretty.t b/t/20_Serializer_Async/42_pp_encode_pretty.t
new file mode 100644
index 0000000..ec2728c
--- /dev/null
+++ b/t/20_Serializer_Async/42_pp_encode_pretty.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+
+eval { require JSON::PP; 1 } or do {
+    plan skip_all => 'JSON::PP not installed';
+    done_testing;
+};
+
+our $JSON_BACKEND = 'JSON::PP';
+do './t/20_Serializer_Async/encode_decode.pl' or die( $@ || $! );
+
diff --git a/t/20_Serializer_Async/encode_bulk.pl b/t/20_Serializer_Async/encode_bulk.pl
new file mode 100644
index 0000000..143fc9e
--- /dev/null
+++ b/t/20_Serializer_Async/encode_bulk.pl
@@ -0,0 +1,60 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+
+our $JSON_BACKEND;
+my $utf8_bytes = "彈性搜索";
+my $utf8_str   = $utf8_bytes;
+utf8::decode($utf8_str);
+my $hash = { "foo" => "$utf8_str" };
+my $arr       = [ $hash, $hash ];
+my $json_hash = qq({"foo":"$utf8_bytes"});
+my $json_arr  = qq($json_hash\n$json_hash\n);
+
+isa_ok my $s
+    = Search::Elasticsearch::Async->new( serializer => $JSON_BACKEND )
+    ->transport->serializer,
+    "Search::Elasticsearch::Serializer::$JSON_BACKEND", 'Serializer';
+
+is $s->encode_bulk(), undef,    #
+    'Enc - No args returns undef';
+is $s->encode_bulk(undef), undef,    #
+    'Enc - Undef returns undef';
+is $s->encode_bulk(''), '',          #
+    'Enc - Empty string returns same';
+is $s->encode_bulk('foo'), 'foo',    #
+    'Enc - String returns same';
+is $s->encode_bulk($utf8_str), $utf8_bytes,    #
+    'Enc - Unicode string returns encoded';
+is $s->encode_bulk($utf8_bytes), $utf8_bytes,    #
+    'Enc - Unicode bytes returns same';
+is $s->encode_bulk($arr), $json_arr,             #
+    'Enc - Array returns JSON';
+is $s->encode_bulk( [ $json_hash, $json_hash ] ), $json_arr,    #
+    'Enc - Array of strings';
+throws_ok { $s->encode_bulk($hash) } qr/must be an array/,      #
+    'Enc - Hash dies';
+throws_ok { $s->encode_bulk( \$utf8_str ) } qr/Serializer/,     #
+    'Enc - scalar ref dies';
+throws_ok { $s->encode_bulk( [ \$utf8_str ] ) } qr/Serializer/,    #
+    'Enc - array of scalar ref dies';
+
+done_testing;
diff --git a/t/20_Serializer_Async/encode_decode.pl b/t/20_Serializer_Async/encode_decode.pl
new file mode 100644
index 0000000..c5e57b4
--- /dev/null
+++ b/t/20_Serializer_Async/encode_decode.pl
@@ -0,0 +1,62 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+
+our $JSON_BACKEND;
+my $utf8_bytes = "彈性搜索";
+my $utf8_str   = $utf8_bytes;
+utf8::decode($utf8_str);
+my $hash      = { "foo" => "$utf8_str" };
+my $arr       = [$hash];
+my $json_hash = qq({"foo":"$utf8_bytes"});
+my $json_arr  = qq([$json_hash]);
+
+isa_ok my $s
+    = Search::Elasticsearch::Async->new( serializer => $JSON_BACKEND )
+    ->transport->serializer,
+    "Search::Elasticsearch::Serializer::$JSON_BACKEND", 'Serializer';
+
+is $s->mime_type, 'application/json', 'Mime type is JSON';
+
+# encode
+is $s->encode(), undef, 'Enc - No args returns undef';
+is $s->encode(undef), undef, 'Enc - Undef returns undef';
+is $s->encode(''),    '',    'Enc - Empty string returns same';
+is $s->encode('foo'), 'foo', 'Enc - String returns same';
+is $s->encode($utf8_str), $utf8_bytes, 'Enc - Unicode string returns encoded';
+is $s->encode($utf8_bytes), $utf8_bytes, 'Enc - Unicode bytes returns same';
+is $s->encode($hash),       $json_hash,  'Enc - Hash returns JSON';
+is $s->encode($arr),        $json_arr,   'Enc - Array returns JSON';
+throws_ok { $s->encode( \$utf8_str ) } qr/Serializer/,
+    'Enc - scalar ref dies';
+
+# decode
+is $s->decode(), undef, 'Dec - No args returns undef';
+is $s->decode(undef), undef, 'Dec - Undef returns undef';
+is $s->decode(''),    '',    'Dec - Empty string returns same';
+is $s->decode('foo'), 'foo', 'Dec - String returns same';
+is $s->decode($utf8_bytes), $utf8_str, 'Dec - Unicode bytes returns decoded';
+is $s->decode($utf8_str),   $utf8_str, 'Dec - Unicode string returns same';
+cmp_deeply $s->decode($json_hash), $hash, 'Dec - JSON returns hash';
+cmp_deeply $s->decode($json_arr),  $arr,  'Dec - JSON returns array';
+throws_ok { $s->decode('{') } qr/Serializer/, 'Dec - invalid JSON dies';
+
+done_testing;
diff --git a/t/20_Serializer_Async/encode_pretty.pl b/t/20_Serializer_Async/encode_pretty.pl
new file mode 100644
index 0000000..8271e2c
--- /dev/null
+++ b/t/20_Serializer_Async/encode_pretty.pl
@@ -0,0 +1,69 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+
+our $JSON_BACKEND;
+my $utf8_bytes = "彈性搜索";
+my $utf8_str   = $utf8_bytes;
+utf8::decode($utf8_str);
+my $hash      = { "foo" => "$utf8_str" };
+my $arr       = [$hash];
+my $json_hash = <<JSON;
+{
+   "foo" : "$utf8_bytes"
+}
+JSON
+
+my $json_arr = <<JSON;
+[
+   {
+      "foo" : "$utf8_bytes"
+   }
+]
+JSON
+
+isa_ok my $s
+    = Search::Elasticsearch::Async->new( serializer => $JSON_BACKEND )
+    ->transport->serializer,
+    "Search::Elasticsearch::Serializer::$JSON_BACKEND", 'Serializer';
+
+# encode
+is_pretty( [],            undef,       'Enc - No args returns undef' );
+is_pretty( [undef],       undef,       'Enc - Undef returns undef' );
+is_pretty( [''],          '',          'Enc - Empty string returns same' );
+is_pretty( ['foo'],       'foo',       'Enc - String returns same' );
+is_pretty( [$utf8_str],   $utf8_bytes, 'Enc - Unicode string returns encoded' );
+is_pretty( [$utf8_bytes], $utf8_bytes, 'Enc - Unicode bytes returns same' );
+is_pretty( [$hash],       $json_hash,  'Enc - Hash returns JSON' );
+is_pretty( [$arr],        $json_arr,   'Enc - Array returns JSON' );
+
+throws_ok { $s->encode_pretty( \$utf8_str ) } qr/Serializer/,    #
+    'Enc - scalar ref dies';
+
+sub is_pretty {
+    my ( $arg, $expect, $desc ) = @_;
+    my $got = $s->encode_pretty(@$arg);
+    defined $got    and $got =~ s/^\s+//gm;
+    defined $expect and $expect =~ s/^\s+//gm;
+    is $got, $expect, $desc;
+}
+
+done_testing;
diff --git a/t/30_Logger/10_explicit.t b/t/30_Logger/10_explicit.t
new file mode 100644
index 0000000..b5ee632
--- /dev/null
+++ b/t/30_Logger/10_explicit.t
@@ -0,0 +1,62 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Search::Elasticsearch;
+use File::Temp;
+my $file = File::Temp->new( EXLOCK => 0 );
+
+# default
+
+isa_ok my $l = Search::Elasticsearch->new->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Default Logger';
+
+is $l->log_as,   'elasticsearch.event', 'Log as';
+is $l->trace_as, 'elasticsearch.trace', 'Trace as';
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::Null',
+    'Default - Log to NULL';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::Null',
+    'Default - Trace to NULL';
+
+# stdout/stderr
+
+isa_ok $l
+    = Search::Elasticsearch->new( log_to => 'Stderr', trace_to => 'Stdout' )
+    ->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Std Logger';
+
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::Stderr',
+    'Std - Log to Stderr';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::Stdout',
+    'Std - Trace to Stdout';
+
+# file
+
+isa_ok $l = Search::Elasticsearch->new(
+    log_to   => [ 'File', $file->filename ],
+    trace_to => [ 'File', $file->filename ]
+    )->logger, 'Search::Elasticsearch::Logger::LogAny',
+    'File Logger';
+
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::File',
+    'File - Log to file';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::File',
+    'File - Trace to file';
+
+done_testing;
diff --git a/t/30_Logger/20_implicit.t b/t/30_Logger/20_implicit.t
new file mode 100644
index 0000000..bd3965c
--- /dev/null
+++ b/t/30_Logger/20_implicit.t
@@ -0,0 +1,50 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Search::Elasticsearch;
+
+use Log::Any::Adapter;
+
+Log::Any::Adapter->set( { category => 'elasticsearch.event' }, 'Stdout' );
+Log::Any::Adapter->set( { category => 'elasticsearch.trace' }, 'Stderr' );
+
+# default
+
+isa_ok my $l = Search::Elasticsearch->new->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Default Logger';
+
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::Stdout',
+    'Default - Log to Stdout';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::Stderr',
+    'Default - Trace to Stderr';
+
+# override
+
+isa_ok $l
+    = Search::Elasticsearch->new( log_to => 'Stderr', trace_to => 'Stdout' )
+    ->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Override Logger';
+
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::Stderr',
+    'Override - Log to Stderr';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::Stdout',
+    'Override - Trace to Stdout';
+
+done_testing;
diff --git a/t/30_Logger/30_log_methods.t b/t/30_Logger/30_log_methods.t
new file mode 100644
index 0000000..09ac0d7
--- /dev/null
+++ b/t/30_Logger/30_log_methods.t
@@ -0,0 +1,73 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+isa_ok my $l = Search::Elasticsearch->new->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Logger';
+
+test_level($_) for qw(debug info warning error critical trace);
+test_throw($_) for qw(error critical);
+
+done_testing;
+
+#===================================
+sub test_level {
+#===================================
+    my $level    = shift;
+    my $levelf   = $level . 'f';
+    my $is_level = 'is_' . $level;
+
+    # ->debug
+    ( $method, $format ) = ();
+    ok $l->$level("foo"), "$level";
+    is $method, $level, "$level - method";
+    is $format, "foo", "$level - format";
+
+    # ->debugf
+    ( $method, $format ) = ();
+    ok $l->$levelf( "foo %s", "bar" ), "$levelf";
+    is $method, $level, "$levelf - method";
+    is $format, "foo bar", "$levelf - format";
+
+    # ->is_debug
+    ( $method, $format ) = ();
+    ok $l->$is_level(), "$is_level";
+    is $method, $is_level, "$is_level - method";
+    is $format, undef, "$is_level - format";
+}
+
+#===================================
+sub test_throw {
+#===================================
+    my $level = shift;
+    my $throw = 'throw_' . $level;
+    my $re    = qr/\[Request\] \*\* Foo/;
+    ( $method, $format ) = ();
+
+    throws_ok { $l->$throw( 'Request', 'Foo', 42 ) } $re, $throw;
+
+    is $@->{vars}, 42, "$throw - vars";
+    is $method,   $level, "$throw - method";
+    like $format, $re,    "$throw - format";
+
+}
diff --git a/t/30_Logger/40_trace_request.t b/t/30_Logger/40_trace_request.t
new file mode 100644
index 0000000..20d2cae
--- /dev/null
+++ b/t/30_Logger/40_trace_request.t
@@ -0,0 +1,119 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+ok my $e
+    = Search::Elasticsearch->new( nodes => 'https://foo.bar:444/some/path' ),
+    'Client';
+
+isa_ok my $l = $e->logger, 'Search::Elasticsearch::Logger::LogAny', 'Logger';
+my $c = $e->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+# No body
+
+ok $l->trace_request(
+    $c,
+    {   method    => 'POST',
+        qs        => { foo => 'bar' },
+        serialize => 'std',
+        path      => '/xyz'
+    }
+    ),
+    'No body';
+
+is $format, <<'REQUEST', 'No body - format';
+# Request to: https://foo.bar:444/some/path
+curl -XPOST 'http://localhost:9200/xyz?foo=bar&pretty=true'
+REQUEST
+
+# Std body
+
+ok $l->trace_request(
+    $c,
+    {   method    => 'POST',
+        qs        => { foo => 'bar' },
+        serialize => 'std',
+        path      => '/xyz',
+        body      => { foo => qq(bar\n'baz) },
+        data      => qq({"foo":"bar\n'baz"}),
+        mime_type => 'application/json',
+    }
+    ),
+    'Body';
+
+is $format, <<'REQUEST', 'Body - format';
+# Request to: https://foo.bar:444/some/path
+curl -H "Content-type: application/json" -XPOST 'http://localhost:9200/xyz?foo=bar&pretty=true' -d '
+{
+   "foo" : "bar\n\u0027baz"
+}
+'
+REQUEST
+
+# Bulk body
+
+ok $l->trace_request(
+    $c,
+    {   method    => 'POST',
+        qs        => { foo => 'bar' },
+        serialize => 'bulk',
+        path      => '/xyz',
+        body      => [ { foo => qq(bar\n'baz) }, { foo => qq(bar\n'baz) } ],
+        data => qq({"foo":"bar\\n\\u0027baz"}\n{"foo":"bar\\n\\u0027baz"}\n),
+        mime_type => 'application/json',
+    }
+    ),
+    'Bulk';
+
+is $format, <<'REQUEST', 'Bulk - format';
+# Request to: https://foo.bar:444/some/path
+curl -H "Content-type: application/json" -XPOST 'http://localhost:9200/xyz?foo=bar&pretty=true' -d '
+{"foo":"bar\n\u0027baz"}
+{"foo":"bar\n\u0027baz"}
+'
+REQUEST
+
+# String body
+
+ok $l->trace_request(
+    $c,
+    {   method    => 'POST',
+        qs        => { foo => 'bar' },
+        serialize => 'std',
+        path      => '/xyz',
+        body => qq(The quick brown fox\njumped over the lazy dog's basket),
+        mime_type => 'application/json',
+    }
+    ),
+    'Body string';
+
+is $format, <<'REQUEST', 'Body string - format';
+# Request to: https://foo.bar:444/some/path
+curl -H "Content-type: application/json" -XPOST 'http://localhost:9200/xyz?foo=bar&pretty=true' -d '
+The quick brown fox
+jumped over the lazy dog\u0027s basket'
+REQUEST
+
+done_testing;
+
diff --git a/t/30_Logger/50_trace_response.t b/t/30_Logger/50_trace_response.t
new file mode 100644
index 0000000..84c9551
--- /dev/null
+++ b/t/30_Logger/50_trace_response.t
@@ -0,0 +1,53 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+ok my $e
+    = Search::Elasticsearch->new( nodes => 'https://foo.bar:444/some/path' ),
+    'Client';
+
+isa_ok my $l = $e->logger, 'Search::Elasticsearch::Logger::LogAny', 'Logger';
+my $c = $e->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+# No body
+
+ok $l->trace_response( $c, 200, undef, 0.123 ), 'No body';
+
+is $format, <<"RESPONSE", 'No body - format';
+# Response: 200, Took: 123 ms
+#\x20
+RESPONSE
+
+# Body
+
+ok $l->trace_response( $c, 200, { foo => 'bar' }, 0.123 ), 'Body';
+is $format, <<'RESPONSE', 'Body - format';
+# Response: 200, Took: 123 ms
+# {
+#    "foo" : "bar"
+# }
+RESPONSE
+
+done_testing;
+
diff --git a/t/30_Logger/60_trace_error.t b/t/30_Logger/60_trace_error.t
new file mode 100644
index 0000000..4f58585
--- /dev/null
+++ b/t/30_Logger/60_trace_error.t
@@ -0,0 +1,68 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+ok my $e
+    = Search::Elasticsearch->new( nodes => 'https://foo.bar:444/some/path' ),
+    'Client';
+
+isa_ok my $l = $e->logger, 'Search::Elasticsearch::Logger::LogAny', 'Logger';
+my $c = $e->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+# No body
+
+ok $l->trace_error(
+    $c,
+    Search::Elasticsearch::Error->new(
+        'Missing',
+        "Foo missing",
+        { code => 404 }
+    )
+    ),
+    'No body';
+
+is $format, <<"RESPONSE", 'No body - format';
+# ERROR: Search::Elasticsearch::Error::Missing Foo missing
+#\x20
+RESPONSE
+
+# Body
+
+ok $l->trace_error(
+    $c,
+    Search::Elasticsearch::Error->new(
+        'Missing', "Foo missing", { code => 404, body => { foo => 'bar' } }
+    )
+    ),
+    'Body';
+
+is $format, <<"RESPONSE", 'Body - format';
+# ERROR: Search::Elasticsearch::Error::Missing Foo missing
+# {
+#    "foo" : "bar"
+# }
+RESPONSE
+
+done_testing;
+
diff --git a/t/30_Logger/70_trace_comment.t b/t/30_Logger/70_trace_comment.t
new file mode 100644
index 0000000..cb6dc9c
--- /dev/null
+++ b/t/30_Logger/70_trace_comment.t
@@ -0,0 +1,42 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+our $format;
+
+ok my $e
+    = Search::Elasticsearch->new( nodes => 'https://foo.bar:444/some/path' ),
+    'Client';
+
+isa_ok my $l = $e->logger, 'Search::Elasticsearch::Logger::LogAny', 'Logger';
+my $c = $e->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+ok $l->trace_comment("The quick fox\njumped"), 'Comment';
+
+is $format, <<"COMMENT", 'Comment - format';
+# *** The quick fox
+# *** jumped
+COMMENT
+
+done_testing;
+
diff --git a/t/30_Logger/80_deprecation_methods.t b/t/30_Logger/80_deprecation_methods.t
new file mode 100644
index 0000000..d08a1a7
--- /dev/null
+++ b/t/30_Logger/80_deprecation_methods.t
@@ -0,0 +1,33 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+isa_ok my $l = Search::Elasticsearch->new->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Logger';
+
+( $method, $format ) = ();
+ok $l->deprecation( "foo", { foo => 1 } ), "deprecation";
+is $method, "warning", "deprecation - method";
+is $format, "[DEPRECATION] foo - In request: {foo => 1}", "deprecation - format";
+
+done_testing;
diff --git a/t/30_Logger/90_error_json.t b/t/30_Logger/90_error_json.t
new file mode 100644
index 0000000..f0d7bb2
--- /dev/null
+++ b/t/30_Logger/90_error_json.t
@@ -0,0 +1,39 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More tests => 3;
+use Test::Exception;
+use lib 't/lib';
+
+use_ok('Search::Elasticsearch::Error');
+
+eval 'use JSON::PP;';
+SKIP: {
+    skip 'JSON::PP module not installed', 2 if $@;
+    ok( my $es_error = Search::Elasticsearch::Error->new(
+            'Missing',
+            "Foo missing",
+            { code => 404 }
+        ),
+        'Create test error'
+    );
+    like(
+        JSON::PP->new->convert_blessed(1)->encode( { eserr => $es_error } ),
+        qr/Foo missing/,
+        'encode_json',
+    );
+}
diff --git a/t/30_Logger_Async/10_explicit.t b/t/30_Logger_Async/10_explicit.t
new file mode 100644
index 0000000..faf4fe9
--- /dev/null
+++ b/t/30_Logger_Async/10_explicit.t
@@ -0,0 +1,63 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Search::Elasticsearch::Async;
+use File::Temp;
+my $file = File::Temp->new( EXLOCK => 0 );
+
+# default
+
+isa_ok my $l = Search::Elasticsearch::Async->new->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Default Logger';
+
+is $l->log_as,   'elasticsearch.event', 'Log as';
+is $l->trace_as, 'elasticsearch.trace', 'Trace as';
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::Null',
+    'Default - Log to NULL';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::Null',
+    'Default - Trace to NULL';
+
+# stdout/stderr
+
+isa_ok $l = Search::Elasticsearch::Async->new(
+    log_to   => 'Stderr',
+    trace_to => 'Stdout'
+    )->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Std Logger';
+
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::Stderr',
+    'Std - Log to Stderr';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::Stdout',
+    'Std - Trace to Stdout';
+
+# file
+
+isa_ok $l = Search::Elasticsearch::Async->new(
+    log_to   => [ 'File', $file->filename ],
+    trace_to => [ 'File', $file->filename ]
+    )->logger, 'Search::Elasticsearch::Logger::LogAny',
+    'File Logger';
+
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::File',
+    'File - Log to file';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::File',
+    'File - Trace to file';
+
+done_testing;
diff --git a/t/30_Logger_Async/20_implicit.t b/t/30_Logger_Async/20_implicit.t
new file mode 100644
index 0000000..da5428f
--- /dev/null
+++ b/t/30_Logger_Async/20_implicit.t
@@ -0,0 +1,51 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Search::Elasticsearch::Async;
+
+use Log::Any::Adapter;
+
+Log::Any::Adapter->set( { category => 'elasticsearch.event' }, 'Stdout' );
+Log::Any::Adapter->set( { category => 'elasticsearch.trace' }, 'Stderr' );
+
+# default
+
+isa_ok my $l = Search::Elasticsearch::Async->new->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Default Logger';
+
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::Stdout',
+    'Default - Log to Stdout';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::Stderr',
+    'Default - Trace to Stderr';
+
+# override
+
+isa_ok $l = Search::Elasticsearch::Async->new(
+    log_to   => 'Stderr',
+    trace_to => 'Stdout'
+    )->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Override Logger';
+
+isa_ok $l->log_handle->adapter, 'Log::Any::Adapter::Stderr',
+    'Override - Log to Stderr';
+isa_ok $l->trace_handle->adapter, 'Log::Any::Adapter::Stdout',
+    'Override - Trace to Stdout';
+
+done_testing;
diff --git a/t/30_Logger_Async/30_log_methods.t b/t/30_Logger_Async/30_log_methods.t
new file mode 100644
index 0000000..def3c9a
--- /dev/null
+++ b/t/30_Logger_Async/30_log_methods.t
@@ -0,0 +1,73 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+isa_ok my $l = Search::Elasticsearch::Async->new->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Logger';
+
+test_level($_) for qw(debug info warning error critical trace);
+test_throw($_) for qw(error critical);
+
+done_testing;
+
+#===================================
+sub test_level {
+#===================================
+    my $level    = shift;
+    my $levelf   = $level . 'f';
+    my $is_level = 'is_' . $level;
+
+    # ->debug
+    ( $method, $format ) = ();
+    ok $l->$level("foo"), "$level";
+    is $method, $level, "$level - method";
+    is $format, "foo", "$level - format";
+
+    # ->debugf
+    ( $method, $format ) = ();
+    ok $l->$levelf( "foo %s", "bar" ), "$levelf";
+    is $method, $level, "$levelf - method";
+    is $format, "foo bar", "$levelf - format";
+
+    # ->is_debug
+    ( $method, $format ) = ();
+    ok $l->$is_level(), "$is_level";
+    is $method, $is_level, "$is_level - method";
+    is $format, undef, "$is_level - format";
+}
+
+#===================================
+sub test_throw {
+#===================================
+    my $level = shift;
+    my $throw = 'throw_' . $level;
+    my $re    = qr/\[Request\] \*\* Foo/;
+    ( $method, $format ) = ();
+
+    throws_ok { $l->$throw( 'Request', 'Foo', 42 ) } $re, $throw;
+
+    is $@->{vars}, 42, "$throw - vars";
+    is $method,   $level, "$throw - method";
+    like $format, $re,    "$throw - format";
+
+}
diff --git a/t/30_Logger_Async/40_trace_request.t b/t/30_Logger_Async/40_trace_request.t
new file mode 100644
index 0000000..64423b3
--- /dev/null
+++ b/t/30_Logger_Async/40_trace_request.t
@@ -0,0 +1,119 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+ok my $e
+    = Search::Elasticsearch::Async->new(
+    nodes => 'https://foo.bar:444/some/path' ), 'Client';
+
+isa_ok my $l = $e->logger, 'Search::Elasticsearch::Logger::LogAny', 'Logger';
+my $c = $e->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+# No body
+
+ok $l->trace_request(
+    $c,
+    {   method    => 'POST',
+        qs        => { foo => 'bar' },
+        serialize => 'std',
+        path      => '/xyz'
+    }
+    ),
+    'No body';
+
+is $format, <<'REQUEST', 'No body - format';
+# Request to: https://foo.bar:444/some/path
+curl -XPOST 'http://localhost:9200/xyz?foo=bar&pretty=true'
+REQUEST
+
+# Std body
+
+ok $l->trace_request(
+    $c,
+    {   method    => 'POST',
+        qs        => { foo => 'bar' },
+        serialize => 'std',
+        path      => '/xyz',
+        body      => { foo => qq(bar\n'baz) },
+        data      => qq({"foo":"bar\n'baz"}),
+        mime_type => 'application/json',
+    }
+    ),
+    'Body';
+
+is $format, <<'REQUEST', 'Body - format';
+# Request to: https://foo.bar:444/some/path
+curl -H "Content-type: application/json" -XPOST 'http://localhost:9200/xyz?foo=bar&pretty=true' -d '
+{
+   "foo" : "bar\n\u0027baz"
+}
+'
+REQUEST
+
+# Bulk body
+
+ok $l->trace_request(
+    $c,
+    {   method    => 'POST',
+        qs        => { foo => 'bar' },
+        serialize => 'bulk',
+        path      => '/xyz',
+        body      => [ { foo => qq(bar\n'baz) }, { foo => qq(bar\n'baz) } ],
+        data => qq({"foo":"bar\\n\\u0027baz"}\n{"foo":"bar\\n\\u0027baz"}\n),
+        mime_type => 'application/json',
+    }
+    ),
+    'Bulk';
+
+is $format, <<'REQUEST', 'Bulk - format';
+# Request to: https://foo.bar:444/some/path
+curl -H "Content-type: application/json" -XPOST 'http://localhost:9200/xyz?foo=bar&pretty=true' -d '
+{"foo":"bar\n\u0027baz"}
+{"foo":"bar\n\u0027baz"}
+'
+REQUEST
+
+# String body
+
+ok $l->trace_request(
+    $c,
+    {   method    => 'POST',
+        qs        => { foo => 'bar' },
+        serialize => 'std',
+        path      => '/xyz',
+        body => qq(The quick brown fox\njumped over the lazy dog's basket),
+        mime_type => 'application/json',
+    }
+    ),
+    'Body string';
+
+is $format, <<'REQUEST', 'Body string - format';
+# Request to: https://foo.bar:444/some/path
+curl -H "Content-type: application/json" -XPOST 'http://localhost:9200/xyz?foo=bar&pretty=true' -d '
+The quick brown fox
+jumped over the lazy dog\u0027s basket'
+REQUEST
+
+done_testing;
+
diff --git a/t/30_Logger_Async/50_trace_response.t b/t/30_Logger_Async/50_trace_response.t
new file mode 100644
index 0000000..e78ce1d
--- /dev/null
+++ b/t/30_Logger_Async/50_trace_response.t
@@ -0,0 +1,54 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+ok my $e
+    = Search::Elasticsearch::Async->new(
+    nodes => 'https://foo.bar:444/some/path' ),
+    'Client';
+
+isa_ok my $l = $e->logger, 'Search::Elasticsearch::Logger::LogAny', 'Logger';
+my $c = $e->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+# No body
+
+ok $l->trace_response( $c, 200, undef, 0.123 ), 'No body';
+
+is $format, <<"RESPONSE", 'No body - format';
+# Response: 200, Took: 123 ms
+#\x20
+RESPONSE
+
+# Body
+
+ok $l->trace_response( $c, 200, { foo => 'bar' }, 0.123 ), 'Body';
+is $format, <<'RESPONSE', 'Body - format';
+# Response: 200, Took: 123 ms
+# {
+#    "foo" : "bar"
+# }
+RESPONSE
+
+done_testing;
+
diff --git a/t/30_Logger_Async/60_trace_error.t b/t/30_Logger_Async/60_trace_error.t
new file mode 100644
index 0000000..dcf6ecd
--- /dev/null
+++ b/t/30_Logger_Async/60_trace_error.t
@@ -0,0 +1,69 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+ok my $e
+    = Search::Elasticsearch::Async->new(
+    nodes => 'https://foo.bar:444/some/path' ),
+    'Client';
+
+isa_ok my $l = $e->logger, 'Search::Elasticsearch::Logger::LogAny', 'Logger';
+my $c = $e->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+# No body
+
+ok $l->trace_error(
+    $c,
+    Search::Elasticsearch::Error->new(
+        'Missing',
+        "Foo missing",
+        { code => 404 }
+    )
+    ),
+    'No body';
+
+is $format, <<"RESPONSE", 'No body - format';
+# ERROR: Search::Elasticsearch::Error::Missing Foo missing
+#\x20
+RESPONSE
+
+# Body
+
+ok $l->trace_error(
+    $c,
+    Search::Elasticsearch::Error->new(
+        'Missing', "Foo missing", { code => 404, body => { foo => 'bar' } }
+    )
+    ),
+    'Body';
+
+is $format, <<"RESPONSE", 'Body - format';
+# ERROR: Search::Elasticsearch::Error::Missing Foo missing
+# {
+#    "foo" : "bar"
+# }
+RESPONSE
+
+done_testing;
+
diff --git a/t/30_Logger_Async/70_trace_comment.t b/t/30_Logger_Async/70_trace_comment.t
new file mode 100644
index 0000000..313513f
--- /dev/null
+++ b/t/30_Logger_Async/70_trace_comment.t
@@ -0,0 +1,43 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+our $format;
+
+ok my $e
+    = Search::Elasticsearch::Async->new(
+    nodes => 'https://foo.bar:444/some/path' ),
+    'Client';
+
+isa_ok my $l = $e->logger, 'Search::Elasticsearch::Logger::LogAny', 'Logger';
+my $c = $e->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+ok $l->trace_comment("The quick fox\njumped"), 'Comment';
+
+is $format, <<"COMMENT", 'Comment - format';
+# *** The quick fox
+# *** jumped
+COMMENT
+
+done_testing;
+
diff --git a/t/30_Logger_Async/80_deprecation_methods.t b/t/30_Logger_Async/80_deprecation_methods.t
new file mode 100644
index 0000000..d08a1a7
--- /dev/null
+++ b/t/30_Logger_Async/80_deprecation_methods.t
@@ -0,0 +1,33 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+
+do './t/lib/LogCallback.pl' or die( $@ || $! );
+
+isa_ok my $l = Search::Elasticsearch->new->logger,
+    'Search::Elasticsearch::Logger::LogAny',
+    'Logger';
+
+( $method, $format ) = ();
+ok $l->deprecation( "foo", { foo => 1 } ), "deprecation";
+is $method, "warning", "deprecation - method";
+is $format, "[DEPRECATION] foo - In request: {foo => 1}", "deprecation - format";
+
+done_testing;
diff --git a/t/40_Transport/10_tidy_request.t b/t/40_Transport/10_tidy_request.t
new file mode 100644
index 0000000..3c325d5
--- /dev/null
+++ b/t/40_Transport/10_tidy_request.t
@@ -0,0 +1,93 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Search::Elasticsearch;
+
+isa_ok my $t = Search::Elasticsearch->new->transport,
+    'Search::Elasticsearch::Transport';
+test_tidy( 'Empty', {}, {} );
+test_tidy( 'Method', { method => 'POST' }, { method => 'POST' } );
+test_tidy( 'Path',   { path   => '/foo' }, { path   => '/foo' } );
+test_tidy( 'QS', { qs => { foo => 'bar' } }, { qs => { foo => 'bar' } } );
+
+test_tidy(
+    'Body - Str',
+    { body => 'foo' },
+    {   body      => 'foo',
+        data      => 'foo',
+        serialize => 'std',
+        mime_type => 'application/json',
+    }
+);
+
+test_tidy(
+    'Body - Hash',
+    { body => { foo => 'bar' } },
+    {   body      => { foo => 'bar' },
+        data      => '{"foo":"bar"}',
+        serialize => 'std',
+        mime_type => 'application/json',
+    }
+);
+
+test_tidy(
+    'Body - Array',
+    { body => [ { foo => 'bar' } ] },
+    {   body      => [ { foo => 'bar' } ],
+        data      => '[{"foo":"bar"}]',
+        serialize => 'std',
+        mime_type => 'application/json',
+    }
+);
+
+test_tidy(
+    'Body - Bulk',
+    { body => [ { foo => 'bar' } ], serialize => 'bulk' },
+    {   body      => [ { foo => 'bar' } ],
+        data      => qq({"foo":"bar"}\n),
+        serialize => 'bulk',
+        mime_type => 'application/json',
+    }
+);
+
+test_tidy(
+    'MimeType',
+    { mime_type => 'text/plain', body => 'foo' },
+    {   mime_type => 'text/plain',
+        body      => 'foo',
+        data      => 'foo',
+        serialize => 'std'
+    }
+);
+
+#===================================
+sub test_tidy {
+#===================================
+    my ( $title, $params, $test ) = @_;
+    $test = {
+        method => 'GET',
+        path   => '/',
+        qs     => {},
+        ignore => [],
+        %$test
+    };
+    cmp_deeply $t->tidy_request($params), $test, $title;
+}
+
+done_testing;
diff --git a/t/40_Transport/20_send_body_as.t b/t/40_Transport/20_send_body_as.t
new file mode 100644
index 0000000..67ce46d
--- /dev/null
+++ b/t/40_Transport/20_send_body_as.t
@@ -0,0 +1,77 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Search::Elasticsearch;
+
+my $t = Search::Elasticsearch->new( send_get_body_as => 'GET' )->transport;
+
+test_tidy( 'GET-empty', { path => '/_search' }, {} );
+test_tidy(
+    'GET-body',
+    { path => '/_search', body => { foo => 'bar' } },
+    {   body      => { foo => 'bar' },
+        data      => '{"foo":"bar"}',
+        method    => 'GET',
+        mime_type => 'application/json',
+        serialize => 'std',
+    }
+);
+
+$t = Search::Elasticsearch->new( send_get_body_as => 'POST' )->transport;
+
+test_tidy( 'POST-empty', { path => '/_search' }, {} );
+test_tidy(
+    'POST-eody',
+    { path => '/_search', body => { foo => 'bar' } },
+    {   body      => { foo => 'bar' },
+        data      => '{"foo":"bar"}',
+        method    => 'POST',
+        mime_type => 'application/json',
+        serialize => 'std',
+    }
+);
+
+$t = Search::Elasticsearch->new( send_get_body_as => 'source' )->transport;
+
+test_tidy( 'source-empty', { path => '/_search' }, {} );
+test_tidy(
+    'source-body',
+    { path => '/_search', body => { foo => 'bar' } },
+    {   method    => 'GET',
+        qs        => { source => '{"foo":"bar"}' },
+        mime_type => 'application/json',
+        serialize => 'std',
+    }
+);
+
+#===================================
+sub test_tidy {
+#===================================
+    my ( $title, $params, $test ) = @_;
+    $test = {
+        method => 'GET',
+        path   => '/_search',
+        qs     => {},
+        ignore => [],
+        %$test
+    };
+    cmp_deeply $t->tidy_request($params), $test, $title;
+}
+
+done_testing;
diff --git a/t/40_Transport/30_perform_request.t b/t/40_Transport/30_perform_request.t
new file mode 100644
index 0000000..17a78dd
--- /dev/null
+++ b/t/40_Transport/30_perform_request.t
@@ -0,0 +1,86 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_static_client);
+
+our $t;
+
+# good request
+$t = mock_static_client(
+    { nodes => ['one'] },                         #
+    { node  => 1, ping => 1 },                    #
+    { node  => 1, code => '200', content => 1 }
+);
+
+ok $t->perform_request, 'Simple request';
+
+# Request error
+$t = mock_static_client(
+    { nodes => ['one'] },
+    { node  => 1, ping => 1 },
+    { node  => 1, code => '404', error => 'NotFound' }
+);
+
+throws_ok { $t->perform_request } qr/Missing/, 'Request error';
+
+# Timeout error
+$t = mock_static_client(
+    { nodes => ['one'] },
+    { node  => 1, ping => 1 },
+    { node  => 1, code => '509', error => 'Timeout' },
+    { node => 1, ping => 1 },
+    { node => 1, code => '200', content => 1 }
+);
+
+throws_ok { $t->perform_request } qr/Timeout/, 'Timeout error';
+ok $t->perform_request, 'Timeout resolved';
+
+# Cxn error
+$t = mock_static_client(
+    { nodes => ['one'] },
+    { node  => 1, ping => 1 },
+    { node  => 1, code => '509', error => 'Cxn' },
+    { node => 1, ping => 1 },
+    { node => 1, code => '200', content => 1 }
+);
+
+ok $t->perform_request, 'Retried connection error';
+
+# NoNodes from failure
+$t = mock_static_client(
+    { nodes => ['one'] },
+    { node  => 1, ping => 1 },
+    { node  => 1, code => '509', error => 'Cxn' },
+    { node => 1, ping => 0 },
+);
+
+throws_ok { $t->perform_request } qr/NoNodes/, 'Cxn then bad ping';
+
+# NoNodes reachable
+$t = mock_static_client(
+    { nodes => ['one'] },       #
+    { node => 1, ping => 0 },
+);
+
+throws_ok { $t->perform_request } qr/NoNodes/, 'Initial bad ping';
+
+done_testing;
diff --git a/t/40_Transport_Async/10_tidy_request.t b/t/40_Transport_Async/10_tidy_request.t
new file mode 100644
index 0000000..03da03d
--- /dev/null
+++ b/t/40_Transport_Async/10_tidy_request.t
@@ -0,0 +1,93 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Search::Elasticsearch::Async;
+
+isa_ok my $t = Search::Elasticsearch::Async->new->transport,
+    'Search::Elasticsearch::Transport::Async';
+test_tidy( 'Empty', {}, {} );
+test_tidy( 'Method', { method => 'POST' }, { method => 'POST' } );
+test_tidy( 'Path',   { path   => '/foo' }, { path   => '/foo' } );
+test_tidy( 'QS', { qs => { foo => 'bar' } }, { qs => { foo => 'bar' } } );
+
+test_tidy(
+    'Body - Str',
+    { body => 'foo' },
+    {   body      => 'foo',
+        data      => 'foo',
+        serialize => 'std',
+        mime_type => 'application/json',
+    }
+);
+
+test_tidy(
+    'Body - Hash',
+    { body => { foo => 'bar' } },
+    {   body      => { foo => 'bar' },
+        data      => '{"foo":"bar"}',
+        serialize => 'std',
+        mime_type => 'application/json',
+    }
+);
+
+test_tidy(
+    'Body - Array',
+    { body => [ { foo => 'bar' } ] },
+    {   body      => [ { foo => 'bar' } ],
+        data      => '[{"foo":"bar"}]',
+        serialize => 'std',
+        mime_type => 'application/json',
+    }
+);
+
+test_tidy(
+    'Body - Bulk',
+    { body => [ { foo => 'bar' } ], serialize => 'bulk' },
+    {   body      => [ { foo => 'bar' } ],
+        data      => qq({"foo":"bar"}\n),
+        serialize => 'bulk',
+        mime_type => 'application/json',
+    }
+);
+
+test_tidy(
+    'MimeType',
+    { mime_type => 'text/plain', body => 'foo' },
+    {   mime_type => 'text/plain',
+        body      => 'foo',
+        data      => 'foo',
+        serialize => 'std'
+    }
+);
+
+#===================================
+sub test_tidy {
+#===================================
+    my ( $title, $params, $test ) = @_;
+    $test = {
+        method => 'GET',
+        path   => '/',
+        qs     => {},
+        ignore => [],
+        %$test
+    };
+    cmp_deeply $t->tidy_request($params), $test, $title;
+}
+
+done_testing;
diff --git a/t/40_Transport_Async/20_send_body_as.t b/t/40_Transport_Async/20_send_body_as.t
new file mode 100644
index 0000000..cc66671
--- /dev/null
+++ b/t/40_Transport_Async/20_send_body_as.t
@@ -0,0 +1,80 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Search::Elasticsearch::Async;
+
+my $t = Search::Elasticsearch::Async->new( send_get_body_as => 'GET' )
+    ->transport;
+
+test_tidy( 'GET-empty', { path => '/_search' }, {} );
+test_tidy(
+    'GET-body',
+    { path => '/_search', body => { foo => 'bar' } },
+    {   body      => { foo => 'bar' },
+        data      => '{"foo":"bar"}',
+        method    => 'GET',
+        mime_type => 'application/json',
+        serialize => 'std',
+    }
+);
+
+$t = Search::Elasticsearch::Async->new( send_get_body_as => 'POST' )
+    ->transport;
+
+test_tidy( 'POST-empty', { path => '/_search' }, {} );
+test_tidy(
+    'POST-eody',
+    { path => '/_search', body => { foo => 'bar' } },
+    {   body      => { foo => 'bar' },
+        data      => '{"foo":"bar"}',
+        method    => 'POST',
+        mime_type => 'application/json',
+        serialize => 'std',
+    }
+);
+
+$t = Search::Elasticsearch::Async->new( send_get_body_as => 'source' )
+    ->transport;
+
+test_tidy( 'source-empty', { path => '/_search' }, {} );
+test_tidy(
+    'source-body',
+    { path => '/_search', body => { foo => 'bar' } },
+    {   method    => 'GET',
+        qs        => { source => '{"foo":"bar"}' },
+        mime_type => 'application/json',
+        serialize => 'std',
+    }
+);
+
+#===================================
+sub test_tidy {
+#===================================
+    my ( $title, $params, $test ) = @_;
+    $test = {
+        method => 'GET',
+        path   => '/_search',
+        qs     => {},
+        ignore => [],
+        %$test
+    };
+    cmp_deeply $t->tidy_request($params), $test, $title;
+}
+
+done_testing;
diff --git a/t/40_Transport_Async/30_perform_request.t b/t/40_Transport_Async/30_perform_request.t
new file mode 100644
index 0000000..098ef7c
--- /dev/null
+++ b/t/40_Transport_Async/30_perform_request.t
@@ -0,0 +1,86 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_static_client);
+
+our $t;
+
+# good request
+$t = mock_static_client(
+    { nodes => ['one'] },                         #
+    { node  => 1, ping => 1 },                    #
+    { node  => 1, code => '200', content => 1 }
+);
+
+ok $t->perform_sync_request, 'Simple request';
+
+# Request error
+$t = mock_static_client(
+    { nodes => ['one'] },
+    { node  => 1, ping => 1 },
+    { node  => 1, code => '404', error => 'NotFound' }
+);
+
+throws_ok { $t->perform_sync_request } qr/Missing/, 'Request error';
+
+# Timeout error
+$t = mock_static_client(
+    { nodes => ['one'] },
+    { node  => 1, ping => 1 },
+    { node  => 1, code => '509', error => 'Timeout' },
+    { node => 1, ping => 1 },
+    { node => 1, code => '200', content => 1 }
+);
+
+throws_ok { $t->perform_sync_request } qr/Timeout/, 'Timeout error';
+ok $t->perform_sync_request, 'Timeout resolved';
+
+# Cxn error
+$t = mock_static_client(
+    { nodes => ['one'] },
+    { node  => 1, ping => 1 },
+    { node  => 1, code => '509', error => 'Cxn' },
+    { node => 1, ping => 1 },
+    { node => 1, code => '200', content => 1 }
+);
+
+is $t->perform_sync_request, 1, 'Retried connection error';
+
+# NoNodes from failure
+$t = mock_static_client(
+    { nodes => ['one'] },
+    { node  => 1, ping => 1 },
+    { node  => 1, code => '509', error => 'Cxn' },
+    { node => 1, ping => 0 },
+);
+
+throws_ok { $t->perform_sync_request } qr/NoNodes/, 'Cxn then bad ping';
+
+# NoNodes reachable
+$t = mock_static_client(
+    { nodes => ['one'] },       #
+    { node => 1, ping => 0 },
+);
+
+throws_ok { $t->perform_sync_request } qr/NoNodes/, 'Initial bad ping';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/10_static_normal.t b/t/50_Cxn_Pool/10_static_normal.t
new file mode 100644
index 0000000..6707f67
--- /dev/null
+++ b/t/50_Cxn_Pool/10_static_normal.t
@@ -0,0 +1,44 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_static_client);
+
+## Both nodes respond - check ping before first use
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'Ping before first use';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/11_static_node_missing.t b/t/50_Cxn_Pool/11_static_node_missing.t
new file mode 100644
index 0000000..d70bb8e
--- /dev/null
+++ b/t/50_Cxn_Pool/11_static_node_missing.t
@@ -0,0 +1,51 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_static_client);
+
+## One node missing at first, then joins later
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 0 },
+    { node => 1, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+
+    # force ping on missing node
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+);
+
+ok $t->perform_request && $t->perform_request && $t->perform_request,
+    'One node missing';
+
+# force ping on missing node
+$t->cxn_pool->cxns->[1]->next_ping(-1);
+
+ok $t->perform_request && $t->perform_request && $t->perform_request,
+    'Missing node joined - 2';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/12_static_node_fails.t b/t/50_Cxn_Pool/12_static_node_fails.t
new file mode 100644
index 0000000..b1c3414
--- /dev/null
+++ b/t/50_Cxn_Pool/12_static_node_fails.t
@@ -0,0 +1,58 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_static_client);
+
+## One node fails with a Cxn error, then rejoins
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 509, error => 'Cxn' },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+
+    # force ping on missing node
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+);
+
+ok $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'One node throws Cxn';
+
+# force ping on missing node
+$t->cxn_pool->cxns->[0]->next_ping(-1);
+
+ok $t->perform_request && $t->perform_request && $t->perform_request,
+    'Failed node recovers';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/13_static_node_timesout.t b/t/50_Cxn_Pool/13_static_node_timesout.t
new file mode 100644
index 0000000..ce60770
--- /dev/null
+++ b/t/50_Cxn_Pool/13_static_node_timesout.t
@@ -0,0 +1,49 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_static_client);
+
+## One node fails with a Timeout error, then rejoins
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 509, error => 'Timeout' },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+
+);
+
+ok $t->perform_request
+    && $t->perform_request
+    && !eval { $t->perform_request }
+    && $@ =~ /Timeout/
+    && $t->perform_request,
+    'One node throws Timeout then recovers';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/14_static_both_nodes_timeout.t b/t/50_Cxn_Pool/14_static_both_nodes_timeout.t
new file mode 100644
index 0000000..3fa2951
--- /dev/null
+++ b/t/50_Cxn_Pool/14_static_both_nodes_timeout.t
@@ -0,0 +1,51 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_static_client);
+
+## One node fails with a Timeout error and causes good node to timeout
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 509, error => 'Timeout' },
+    { node => 2, ping => 1 },
+    { node => 2, code => 509, error => 'Timeout' },
+    { node => 1, ping => 0 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+
+);
+
+ok $t->perform_request
+    && $t->perform_request
+    && !eval { $t->perform_request }
+    && $@ =~ /Timeout/
+    && !eval { $t->perform_request }
+    && $@ =~ /Timeout/
+    && $t->perform_request,
+    'One node throws Timeout, causing Timeout on other node';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/15_static_both_nodes_fail.t b/t/50_Cxn_Pool/15_static_both_nodes_fail.t
new file mode 100644
index 0000000..61fd77b
--- /dev/null
+++ b/t/50_Cxn_Pool/15_static_both_nodes_fail.t
@@ -0,0 +1,66 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_static_client);
+
+## All nodes fail
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 509, error => 'Cxn' },
+    { node => 2, ping => 0 },
+    { node => 1, ping => 0 },
+
+    # NoNodes
+    { node => 2, ping => 0 },
+    { node => 1, ping => 0 },
+
+    # NoNodes
+    { node => 2, ping => 0 },
+    { node => 1, ping => 0 },
+
+    # NoNodes
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+
+);
+
+ok $t->perform_request
+    && $t->perform_request
+    && !eval { $t->perform_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_request }
+    && $@ =~ /NoNodes/
+    && $t->perform_request
+    && $t->perform_request,
+    'Both nodes fails then recover';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/16_static_nodes_starting.t b/t/50_Cxn_Pool/16_static_nodes_starting.t
new file mode 100644
index 0000000..15ecfcd
--- /dev/null
+++ b/t/50_Cxn_Pool/16_static_nodes_starting.t
@@ -0,0 +1,52 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_static_client);
+
+## Nodes initially unavailable
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 2, ping => 0 },
+    { node => 1, ping => 0 },
+
+    # NoNodes
+    { node => 2, ping => 0 },
+    { node => 1, ping => 0 },
+
+    # NoNodes
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+
+);
+
+ok !eval { $t->perform_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_request }
+    && $@ =~ /NoNodes/
+    && $t->perform_request
+    && $t->perform_request,
+    'Nodes initially unavailable';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/17_static_runaway_nodes.t b/t/50_Cxn_Pool/17_static_runaway_nodes.t
new file mode 100644
index 0000000..ed45997
--- /dev/null
+++ b/t/50_Cxn_Pool/17_static_runaway_nodes.t
@@ -0,0 +1,58 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_static_client);
+
+## Runaway nodes (ie wrong HTTP response codes signal node failure, instead of
+## request failure)
+
+my $t = mock_static_client(
+    { nodes => 'one' },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 1, code => 503, error => 'Unavailable' },
+    { node => 1, ping => 1 },
+    { node => 1, code => 503, error => 'Unavailable' },
+
+    # throw Unavailable: too many retries
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 503, error => 'Unavailable' },
+    { node => 1, ping => 1 },
+    { node => 1, code => 503, error => 'Unavailable' },
+
+    # throw Unavailable: too many retries
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+
+);
+
+ok $t->perform_request
+    && !eval { $t->perform_request }
+    && $@ =~ /Unavailable/
+    && !eval { $t->perform_request }
+    && $@ =~ /Unavailable/
+    && $t->perform_request,
+    "Runaway nodes";
+
+done_testing;
diff --git a/t/50_Cxn_Pool/30_sniff_normal.t b/t/50_Cxn_Pool/30_sniff_normal.t
new file mode 100644
index 0000000..392a7f6
--- /dev/null
+++ b/t/50_Cxn_Pool/30_sniff_normal.t
@@ -0,0 +1,42 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## Both nodes respond - check ping before first use
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { sniff => [ 'one', 'two' ] },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'Sniff before first use';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/31_sniff_new_nodes.t b/t/50_Cxn_Pool/31_sniff_new_nodes.t
new file mode 100644
index 0000000..f603524
--- /dev/null
+++ b/t/50_Cxn_Pool/31_sniff_new_nodes.t
@@ -0,0 +1,51 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## Sniff new nodes
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [],      code => 509, error => 'Cxn' },
+    { node => 2, sniff => [ 'two', 'three' ] },
+    { node => 3, code => 200, content => 1 },
+    { node => 4, code => 200, content => 1 },
+
+    # force sniff
+    { node => 3, sniff => [ 'one', 'two', 'three' ] },
+    { node => 5, code => 200, content => 1 },
+    { node => 6, code => 200, content => 1 },
+    { node => 7, code => 200, content => 1 },
+    { node => 5, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'Sniff new nodes';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/32_sniff_node_fails.t b/t/50_Cxn_Pool/32_sniff_node_fails.t
new file mode 100644
index 0000000..8c77cb4
--- /dev/null
+++ b/t/50_Cxn_Pool/32_sniff_node_fails.t
@@ -0,0 +1,50 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## Sniff node failures
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, code  => 200, content => 1 },
+    { node => 3, code  => 509, error   => 'Cxn' },
+    { node => 2, sniff => ['one'] },
+    { node => 4, code  => 200, content => 1 },
+    { node => 4, code  => 200, content => 1 },
+
+    # force sniff
+    { node => 4, sniff => [ 'one', 'two' ] },
+    { node => 5, code => 200, content => 1 },
+    { node => 6, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->perform_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_request
+    && $t->perform_request,
+    'Sniff after failure';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/33_sniff_both_nodes_fail.t b/t/50_Cxn_Pool/33_sniff_both_nodes_fail.t
new file mode 100644
index 0000000..4b5192d
--- /dev/null
+++ b/t/50_Cxn_Pool/33_sniff_both_nodes_fail.t
@@ -0,0 +1,60 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## Sniff all nodes fail
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 509, error   => 'Cxn' },
+    { node => 2, sniff => [], error => 'Cxn', code => 509 },
+    { node => 3, sniff => [], error => 'Cxn', code => 509 },
+    { node => 4, sniff => [], error => 'Cxn', code => 509 },
+    { node => 5, sniff => [], error => 'Cxn', code => 509 },
+
+    # throws NoNodes
+
+    { node => 2, sniff => [], error => 'Cxn', code => 509 },
+    { node => 3, sniff => [], error => 'Cxn', code => 509 },
+    { node => 6, sniff => [], error => 'Cxn', code => 509 },
+    { node => 7, sniff => [], error => 'Cxn', code => 509 },
+
+    # throws NoNodes
+
+    { node => 2, sniff => [ 'one', 'two' ] },
+    { node => 8, code => 200, content => 1 },
+    { node => 9, code => 200, content => 1 },
+    { node => 8, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && !eval { $t->perform_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_request }
+    && $@ =~ /NoNodes/
+    && $t->perform_request,
+    'Sniff after all nodes fail';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/34_sniff_node_timeout.t b/t/50_Cxn_Pool/34_sniff_node_timeout.t
new file mode 100644
index 0000000..300b732
--- /dev/null
+++ b/t/50_Cxn_Pool/34_sniff_node_timeout.t
@@ -0,0 +1,55 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## Sniff after Timeout error
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 509, error   => 'Timeout' },
+
+    # throws Timeout
+
+    { node => 2, sniff => ['one'] },
+    { node => 4, code  => 200, content => 1 },
+    { node => 4, code  => 200, content => 1 },
+
+    # force sniff
+    { node => 4, sniff => [ 'one', 'two' ] },
+    { node => 5, code => 200, content => 1 },
+    { node => 6, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && !eval { $t->perform_request }
+    && $@ =~ /Timeout/
+    && $t->perform_request
+    && $t->perform_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_request
+    && $t->perform_request,
+    'Sniff after timeout';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/35_sniff_both_nodes_timeout.t b/t/50_Cxn_Pool/35_sniff_both_nodes_timeout.t
new file mode 100644
index 0000000..dc2caef
--- /dev/null
+++ b/t/50_Cxn_Pool/35_sniff_both_nodes_timeout.t
@@ -0,0 +1,60 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## Sniff when bad node timesout causing good node to timeout too
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 509, error   => 'Timeout' },
+
+    # throws Timeout
+
+    { node => 2, sniff => ['one'] },
+    { node => 4, code  => 509, error => 'Timeout' },
+
+    # throws Timeout
+
+    { node => 4, sniff => ['one'] },
+    { node => 5, code  => 200, content => 1 },
+
+    # force sniff
+    { node => 5, sniff => [ 'one', 'two' ] },
+    { node => 6, code => 200, content => 1 },
+    { node => 7, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && !eval { $t->perform_request }
+    && $@ =~ /Timeout/
+    && !eval { $t->perform_request }
+    && $@ =~ /Timeout/
+    && $t->perform_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_request
+    && $t->perform_request,
+    'Sniff after both nodes timeout';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/36_sniff_nodes_starting.t b/t/50_Cxn_Pool/36_sniff_nodes_starting.t
new file mode 100644
index 0000000..356a9ea
--- /dev/null
+++ b/t/50_Cxn_Pool/36_sniff_nodes_starting.t
@@ -0,0 +1,61 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## Nodes initially unavailable
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [], error => 'Cxn', code => 509 },
+    { node => 2, sniff => [], error => 'Cxn', code => 509 },
+
+    # NoNodes
+
+    { node => 3, sniff => [], error => 'Cxn', code => 509 },
+    { node => 4, sniff => [], error => 'Cxn', code => 509 },
+
+    # NoNodes
+
+    { node => 5, sniff => ['one'] },
+    { node => 6, code  => 200, content => 1 },
+    { node => 6, code  => 200, content => 1 },
+
+    # force sniff
+    { node => 6, sniff => [ 'one', 'two' ] },
+    { node => 7, code => 200, content => 1 },
+    { node => 8, code => 200, content => 1 },
+
+);
+
+ok !eval { $t->perform_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_request }
+    && $@ =~ /NoNodes/
+    && $t->perform_request
+    && $t->perform_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_request
+    && $t->perform_request,
+    'Sniff unavailable nodes while starting up';
+
+done_testing;
diff --git a/t/50_Cxn_Pool/37_sniff_runaway_nodes.t b/t/50_Cxn_Pool/37_sniff_runaway_nodes.t
new file mode 100644
index 0000000..0bea598
--- /dev/null
+++ b/t/50_Cxn_Pool/37_sniff_runaway_nodes.t
@@ -0,0 +1,57 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## Runaway nodes (ie wrong HTTP response codes signal node failure, instead of
+## request failure)
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, code  => 200,     content => 1 },
+    { node => 3, code  => 503,     error   => 'Unavailable' },
+    { node => 2, sniff => [ 'one', 'two' ] },
+    { node => 4, code  => 503,     error   => 'Unavailable' },
+
+    # throw Unavailable: too many retries
+
+    { node => 5, sniff => [ 'one', 'two' ] },
+    { node => 6, code  => 503,     error => 'Unavailable' },
+    { node => 7, sniff => [ 'one', 'two' ] },
+    { node => 8, code  => 503,     error => 'Unavailable' },
+
+    # throw Unavailable: too many retries
+
+    { node => 9, sniff => [ 'one', 'two' ] },
+    { node => 10, code => 200, content => 1 },
+);
+
+ok $t->perform_request
+    && !eval { $t->perform_request }
+    && $@ =~ /Unavailable/
+    && !eval { $t->perform_request }
+    && $@ =~ /Unavailable/
+    && $t->perform_request,
+    "Runaway nodes";
+
+done_testing;
diff --git a/t/50_Cxn_Pool/38_bad_sniff.t b/t/50_Cxn_Pool/38_bad_sniff.t
new file mode 100644
index 0000000..cd0132f
--- /dev/null
+++ b/t/50_Cxn_Pool/38_bad_sniff.t
@@ -0,0 +1,37 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## For whatever reason, sniffing returns bad data
+
+my $t = mock_sniff_client(
+    { nodes => ['one'] },
+    { node  => 1, code => 200, content => '{"nodes":{"one":{}}}' },
+
+    # throw NoNodes
+);
+
+ok !eval { $t->perform_request }
+    && $@ =~ /NoNodes/,
+    "Missing http_address";
+
+done_testing;
diff --git a/t/50_Cxn_Pool/39_sniff_max_content.t b/t/50_Cxn_Pool/39_sniff_max_content.t
new file mode 100644
index 0000000..08ee57d
--- /dev/null
+++ b/t/50_Cxn_Pool/39_sniff_max_content.t
@@ -0,0 +1,68 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_sniff_client);
+
+## Dynamic max content length
+
+my $response = <<RESPONSE;
+{
+    "nodes": {
+        "one": {
+            "http_address": "inet[/one]",
+            "http": {
+                "max_content_length_in_bytes": 200
+            }
+        },
+        "two": {
+            "http_address": "inet[/two]",
+            "http": {
+                "max_content_length_in_bytes": 509
+            }
+        },
+        "three": {
+            "http_address": "inet[/two]"
+        }
+    }
+}
+RESPONSE
+
+my $t = mock_sniff_client(
+    { nodes => ['one'] },
+    { node  => 1, code => 200, content => $response },
+    { node  => 2, code => 200, content => 1 },
+);
+
+is $t->perform_request
+    && $t->cxn_pool->next_cxn->max_content_length, 200,
+    "Dynamic max content length";
+
+$t = mock_sniff_client(
+    { nodes => ['one'], max_content_length => 1000 },
+    { node => 1, code => 200, content => $response },
+    { node => 2, code => 200, content => 1 },
+);
+
+is $t->perform_request
+    && $t->cxn_pool->next_cxn->max_content_length, 1000,
+    "Dynamic max content length";
+
+done_testing;
diff --git a/t/50_Cxn_Pool/40_sniff_extract_host.t b/t/50_Cxn_Pool/40_sniff_extract_host.t
new file mode 100644
index 0000000..686b9e0
--- /dev/null
+++ b/t/50_Cxn_Pool/40_sniff_extract_host.t
@@ -0,0 +1,38 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Search::Elasticsearch;
+
+my $pool
+    = Search::Elasticsearch->new( cxn_pool => 'Sniff' )->transport->cxn_pool;
+
+is $pool->_extract_host('127.0.0.1:9200'), '127.0.0.1:9200', "IP";
+
+is $pool->_extract_host('myhost/127.0.0.1:9200'), '127.0.0.1:9200', "Host/IP";
+
+is $pool->_extract_host('inet[127.0.0.1:9200]'), '127.0.0.1:9200', "inet[IP]";
+
+is $pool->_extract_host('inet[myhost/127.0.0.1:9200]'), '127.0.0.1:9200',
+    "inet[Host/IP]";
+
+is $pool->_extract_host('inet[/127.0.0.1:9200]'), '127.0.0.1:9200',
+    "inet[/IP]";
+
+ok !$pool->_extract_host(), "Undefined";
+
+done_testing;
diff --git a/t/50_Cxn_Pool/50_noping_normal.t b/t/50_Cxn_Pool/50_noping_normal.t
new file mode 100644
index 0000000..b294996
--- /dev/null
+++ b/t/50_Cxn_Pool/50_noping_normal.t
@@ -0,0 +1,46 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_noping_client);
+
+## All nodes respond
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'Round robin';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/51_noping_node_fails.t b/t/50_Cxn_Pool/51_noping_node_fails.t
new file mode 100644
index 0000000..9ce6d9a
--- /dev/null
+++ b/t/50_Cxn_Pool/51_noping_node_fails.t
@@ -0,0 +1,52 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_noping_client);
+
+## Node fails and recover
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 509, error   => 'Cxn' },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+    # force check
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->cxn_pool->cxns->[1]->force_ping
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'Node fails and recovers';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/52_noping_node_timesout.t b/t/50_Cxn_Pool/52_noping_node_timesout.t
new file mode 100644
index 0000000..f28f733
--- /dev/null
+++ b/t/50_Cxn_Pool/52_noping_node_timesout.t
@@ -0,0 +1,54 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_noping_client);
+
+## Nodes fail and recover
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 509, error   => 'Timeout' },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+    # force check
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && !eval { $t->perform_request }
+    && $@ =~ /Timeout/
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->cxn_pool->cxns->[1]->force_ping
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'Node timesout and recovers';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/53_noping_all_nodes_fail.t b/t/50_Cxn_Pool/53_noping_all_nodes_fail.t
new file mode 100644
index 0000000..4eec050
--- /dev/null
+++ b/t/50_Cxn_Pool/53_noping_all_nodes_fail.t
@@ -0,0 +1,59 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_noping_client);
+
+## All nodes fail and recover
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 509, error   => 'Cxn' },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 509, error   => 'Cxn' },
+    { node => 3, code => 509, error   => 'Cxn' },
+    { node => 2, code => 200, content => 1 },
+
+    # force check
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->cxn_pool->cxns->[0]->force_ping
+    && $t->cxn_pool->cxns->[2]->force_ping
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'All nodes fail and recover';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/54_noping_nodes_starting.t b/t/50_Cxn_Pool/54_noping_nodes_starting.t
new file mode 100644
index 0000000..a83d5e9
--- /dev/null
+++ b/t/50_Cxn_Pool/54_noping_nodes_starting.t
@@ -0,0 +1,50 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_noping_client);
+
+## Nodes initially unavailable
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 509, error   => 'Cxn' },
+    { node => 2, code => 509, error   => 'Cxn' },
+    { node => 3, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+    # force check
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->cxn_pool->cxns->[0]->force_ping
+    && $t->cxn_pool->cxns->[1]->force_ping
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'Nodes starting';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/55_noping_runaway_nodes.t b/t/50_Cxn_Pool/55_noping_runaway_nodes.t
new file mode 100644
index 0000000..749774c
--- /dev/null
+++ b/t/50_Cxn_Pool/55_noping_runaway_nodes.t
@@ -0,0 +1,56 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_noping_client);
+
+## Runaway nodes
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 509, error   => 'Unavailable' },
+    { node => 2, code => 509, error   => 'Unavailable' },
+    { node => 3, code => 509, error   => 'Unavailable' },
+
+    # throws unavailable
+    { node => 1, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->perform_request
+    && !eval { $t->perform_request }
+    && $@ =~ /Unavailable/
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request
+    && $t->perform_request,
+    'Runaway nodes';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool/56_max_retries.t b/t/50_Cxn_Pool/56_max_retries.t
new file mode 100644
index 0000000..bfe6c34
--- /dev/null
+++ b/t/50_Cxn_Pool/56_max_retries.t
@@ -0,0 +1,51 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch;
+use lib 't/lib';
+use MockCxn qw(mock_noping_client);
+
+## Max retries
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ], max_retries => 1 },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 509, error   => 'Unavailable' },
+    { node => 2, code => 509, error   => 'Unavailable' },
+
+    # throws unavailable
+    { node => 3, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+);
+
+ok $t->perform_request()
+    && $t->perform_request
+    && $t->perform_request
+    && !eval { $t->perform_request }
+    && $@ =~ /Unavailable/
+    && $t->perform_request
+    && $t->perform_request,
+    'Max retries';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/10_static_normal.t b/t/50_Cxn_Pool_Async/10_static_normal.t
new file mode 100644
index 0000000..daa2ae3
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/10_static_normal.t
@@ -0,0 +1,44 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_static_client);
+
+## Both nodes respond - check ping before first use
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Ping before first use';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/11_static_node_missing.t b/t/50_Cxn_Pool_Async/11_static_node_missing.t
new file mode 100644
index 0000000..52d3642
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/11_static_node_missing.t
@@ -0,0 +1,55 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_static_client);
+
+## One node missing at first, then joins later
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 0 },
+    { node => 1, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+
+    # force ping on missing node
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'One node missing';
+
+# force ping on missing node
+$t->cxn_pool->cxns->[1]->next_ping(-1);
+
+ok $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Missing node joined - 2';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/12_static_node_fails.t b/t/50_Cxn_Pool_Async/12_static_node_fails.t
new file mode 100644
index 0000000..95c1e0f
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/12_static_node_fails.t
@@ -0,0 +1,60 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_static_client);
+
+## One node fails with a Cxn error, then rejoins
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 509, error => 'Cxn' },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+
+    # force ping on missing node
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'One node throws Cxn';
+
+# force ping on missing node
+$t->cxn_pool->cxns->[0]->next_ping(-1);
+
+ok $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Failed node recovers';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/13_static_node_timesout.t b/t/50_Cxn_Pool_Async/13_static_node_timesout.t
new file mode 100644
index 0000000..c865172
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/13_static_node_timesout.t
@@ -0,0 +1,49 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_static_client);
+
+## One node fails with a Timeout error, then rejoins
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 509, error => 'Timeout' },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+
+);
+
+ok $t->perform_sync_request
+    && $t->perform_sync_request
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Timeout/
+    && $t->perform_sync_request,
+    'One node throws Timeout then recovers';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/14_static_both_nodes_timeout.t b/t/50_Cxn_Pool_Async/14_static_both_nodes_timeout.t
new file mode 100644
index 0000000..6a201c3
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/14_static_both_nodes_timeout.t
@@ -0,0 +1,51 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_static_client);
+
+## One node fails with a Timeout error and causes good node to timeout
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 509, error => 'Timeout' },
+    { node => 2, ping => 1 },
+    { node => 2, code => 509, error => 'Timeout' },
+    { node => 1, ping => 0 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+
+);
+
+ok $t->perform_sync_request
+    && $t->perform_sync_request
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Timeout/
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Timeout/
+    && $t->perform_sync_request,
+    'One node throws Timeout, causing Timeout on other node';
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/15_static_both_nodes_fail.t b/t/50_Cxn_Pool_Async/15_static_both_nodes_fail.t
new file mode 100644
index 0000000..8467d9d
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/15_static_both_nodes_fail.t
@@ -0,0 +1,66 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_static_client);
+
+## All nodes fail
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 1, code => 509, error => 'Cxn' },
+    { node => 2, ping => 0 },
+    { node => 1, ping => 0 },
+
+    # NoNodes
+    { node => 1, ping => 0 },
+    { node => 2, ping => 0 },
+
+    # NoNodes
+    { node => 1, ping => 0 },
+    { node => 2, ping => 0 },
+
+    # NoNodes
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+
+);
+
+ok $t->perform_sync_request
+    && $t->perform_sync_request
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Both nodes fails then recover';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/16_static_nodes_starting.t b/t/50_Cxn_Pool_Async/16_static_nodes_starting.t
new file mode 100644
index 0000000..0d9a6e1
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/16_static_nodes_starting.t
@@ -0,0 +1,52 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_static_client);
+
+## Nodes initially unavailable
+
+my $t = mock_static_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 2, ping => 0 },
+    { node => 1, ping => 0 },
+
+    # NoNodes
+    { node => 2, ping => 0 },
+    { node => 1, ping => 0 },
+
+    # NoNodes
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, ping => 1 },
+    { node => 2, code => 200, content => 1 },
+
+);
+
+ok !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Nodes initially unavailable';
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/17_static_runaway_nodes.t b/t/50_Cxn_Pool_Async/17_static_runaway_nodes.t
new file mode 100644
index 0000000..225af3f
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/17_static_runaway_nodes.t
@@ -0,0 +1,58 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_static_client);
+
+## Runaway nodes (ie wrong HTTP response codes signal node failure, instead of
+## request failure)
+
+my $t = mock_static_client(
+    { nodes => 'one' },
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 1, code => 503, error => 'Unavailable' },
+    { node => 1, ping => 1 },
+    { node => 1, code => 503, error => 'Unavailable' },
+
+    # throw Unavailable: too many retries
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 503, error => 'Unavailable' },
+    { node => 1, ping => 1 },
+    { node => 1, code => 503, error => 'Unavailable' },
+
+    # throw Unavailable: too many retries
+
+    { node => 1, ping => 1 },
+    { node => 1, code => 200, content => 1 },
+
+);
+
+ok $t->perform_sync_request
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Unavailable/
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Unavailable/
+    && $t->perform_sync_request,
+    "Runaway nodes";
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/30_sniff_normal.t b/t/50_Cxn_Pool_Async/30_sniff_normal.t
new file mode 100644
index 0000000..9158e74
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/30_sniff_normal.t
@@ -0,0 +1,43 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## Both nodes respond - check ping before first use
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { sniff => [ 'one', 'two' ] },
+    { sniff => [ 'one', 'two' ] },
+    { node => 3, code => 200, content => 1 },
+    { node => 4, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 4, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Sniff before first use';
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/31_sniff_new_nodes.t b/t/50_Cxn_Pool_Async/31_sniff_new_nodes.t
new file mode 100644
index 0000000..9e92e17
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/31_sniff_new_nodes.t
@@ -0,0 +1,52 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## Sniff new nodes
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [],      code => 509, error => 'Cxn' },
+    { node => 2, sniff => [ 'two', 'three' ] },
+    { node => 3, code => 200, content => 1 },
+    { node => 4, code => 200, content => 1 },
+
+    # force sniff
+    { node => 3, sniff => [ 'one', 'two', 'three' ] },
+    { node => 4, sniff => [ 'one', 'two', 'three' ] },
+    { node => 5, code => 200, content => 1 },
+    { node => 6, code => 200, content => 1 },
+    { node => 7, code => 200, content => 1 },
+    { node => 5, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Sniff new nodes';
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/32_sniff_node_fails.t b/t/50_Cxn_Pool_Async/32_sniff_node_fails.t
new file mode 100644
index 0000000..1ba2cd4
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/32_sniff_node_fails.t
@@ -0,0 +1,52 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## Sniff node failures
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, sniff => [ 'one', 'two' ] },
+    { node => 3, code  => 200, content => 1 },
+    { node => 4, code  => 509, error   => 'Cxn' },
+    { node => 3, sniff => ['one'] },
+    { node => 4, sniff => [] },
+    { node => 5, code  => 200, content => 1 },
+    { node => 5, code  => 200, content => 1 },
+
+    # force sniff
+    { node => 5, sniff => [ 'one', 'two' ] },
+    { node => 6, code => 200, content => 1 },
+    { node => 7, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Sniff after failure';
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/33_sniff_both_nodes_fail.t b/t/50_Cxn_Pool_Async/33_sniff_both_nodes_fail.t
new file mode 100644
index 0000000..ccfde85
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/33_sniff_both_nodes_fail.t
@@ -0,0 +1,62 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## Sniff all nodes fail
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, sniff => [ 'one', 'two' ] },
+    { node => 3, code => 200, content => 1 },
+    { node => 4, code => 509, error   => 'Cxn' },
+    { node => 3, sniff => [], error => 'Cxn', code => 509 },
+    { node => 4, sniff => [], error => 'Cxn', code => 509 },
+    { node => 5, sniff => [], error => 'Cxn', code => 509 },
+    { node => 6, sniff => [], error => 'Cxn', code => 509 },
+
+    # throws NoNodes
+
+    { node => 3, sniff => [], error => 'Cxn', code => 509 },
+    { node => 4, sniff => [], error => 'Cxn', code => 509 },
+    { node => 7, sniff => [], error => 'Cxn', code => 509 },
+    { node => 8, sniff => [], error => 'Cxn', code => 509 },
+
+    # throws NoNodes
+
+    { node => 3, sniff => [ 'one', 'two' ] },
+    { node => 4, sniff => [ 'one', 'two' ] },
+    { node => 9,  code => 200, content => 1 },
+    { node => 10, code => 200, content => 1 },
+    { node => 9,  code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/
+    && $t->perform_sync_request,
+    'Sniff after all nodes fail';
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/34_sniff_node_timeout.t b/t/50_Cxn_Pool_Async/34_sniff_node_timeout.t
new file mode 100644
index 0000000..4065bda
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/34_sniff_node_timeout.t
@@ -0,0 +1,57 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## Sniff after Timeout error
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, sniff => [ 'one', 'two' ] },
+    { node => 3, code => 200, content => 1 },
+    { node => 4, code => 509, error   => 'Timeout' },
+
+    # throws Timeout
+
+    { node => 3, sniff => ['one'] },
+    { node => 4, sniff => ['one'] },
+    { node => 5, code  => 200, content => 1 },
+    { node => 5, code  => 200, content => 1 },
+
+    # force sniff
+    { node => 5, sniff => [ 'one', 'two' ] },
+    { node => 6, code => 200, content => 1 },
+    { node => 7, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Timeout/
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Sniff after timeout';
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/35_sniff_both_nodes_timeout.t b/t/50_Cxn_Pool_Async/35_sniff_both_nodes_timeout.t
new file mode 100644
index 0000000..72a62fb
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/35_sniff_both_nodes_timeout.t
@@ -0,0 +1,62 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## Sniff when bad node timesout causing good node to timeout too
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, sniff => [ 'one', 'two' ] },
+    { node => 3, code => 200, content => 1 },
+    { node => 4, code => 509, error   => 'Timeout' },
+
+    # throws Timeout
+
+    { node => 3, sniff => ['one'] },
+    { node => 4, sniff => ['one'] },
+    { node => 5, code  => 509, error => 'Timeout' },
+
+    # throws Timeout
+
+    { node => 5, sniff => ['one'] },
+    { node => 6, code  => 200, content => 1 },
+
+    # force sniff
+    { node => 6, sniff => [ 'one', 'two' ] },
+    { node => 7, code => 200, content => 1 },
+    { node => 8, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Timeout/
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Timeout/
+    && $t->perform_sync_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Sniff after both nodes timeout';
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/36_sniff_nodes_starting.t b/t/50_Cxn_Pool_Async/36_sniff_nodes_starting.t
new file mode 100644
index 0000000..818e183
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/36_sniff_nodes_starting.t
@@ -0,0 +1,62 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## Nodes initially unavailable
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [], error => 'Cxn', code => 509 },
+    { node => 2, sniff => [], error => 'Cxn', code => 509 },
+
+    # NoNodes
+
+    { node => 3, sniff => [], error => 'Cxn', code => 509 },
+    { node => 4, sniff => [], error => 'Cxn', code => 509 },
+
+    # NoNodes
+
+    { node => 5, sniff => ['one'] },
+    { node => 6, sniff => ['one'] },
+    { node => 7, code  => 200, content => 1 },
+    { node => 7, code  => 200, content => 1 },
+
+    # force sniff
+    { node => 7, sniff => [ 'one', 'two' ] },
+    { node => 8, code => 200, content => 1 },
+    { node => 9, code => 200, content => 1 },
+
+);
+
+ok !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->cxn_pool->schedule_check
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Sniff unavailable nodes while starting up';
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/37_sniff_runaway_nodes.t b/t/50_Cxn_Pool_Async/37_sniff_runaway_nodes.t
new file mode 100644
index 0000000..ba9139b
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/37_sniff_runaway_nodes.t
@@ -0,0 +1,62 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## Runaway nodes (ie wrong HTTP response codes signal node failure, instead of
+## request failure)
+
+my $t = mock_sniff_client(
+    { nodes => [ 'one', 'two' ] },
+
+    { node => 1, sniff => [ 'one', 'two' ] },
+    { node => 2, sniff => [ 'one', 'two' ] },
+    { node => 3, code  => 200,     content => 1 },
+    { node => 4, code  => 503,     error   => 'Unavailable' },
+    { node => 3, sniff => [ 'one', 'two' ] },
+    { node => 4, sniff => [ 'one', 'two' ] },
+    { node => 5, code  => 503,     error   => 'Unavailable' },
+
+    # throw Unavailable: too many retries
+
+    { node => 6, sniff => [ 'one', 'two' ] },
+    { node => 5, sniff => [ 'one', 'two' ] },
+    { node => 7, code  => 503,     error => 'Unavailable' },
+    { node => 8, sniff => [ 'one', 'two' ] },
+    { node => 7, sniff => [ 'one', 'two' ] },
+    { node => 9, code  => 503,     error => 'Unavailable' },
+
+    # throw Unavailable: too many retries
+
+    { node => 10, sniff => [ 'one', 'two' ] },
+    { node => 9,  sniff => [ 'one', 'two' ] },
+    { node => 11, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Unavailable/
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Unavailable/
+    && $t->perform_sync_request,
+    "Runaway nodes";
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/38_bad_sniff.t b/t/50_Cxn_Pool_Async/38_bad_sniff.t
new file mode 100644
index 0000000..92d428d
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/38_bad_sniff.t
@@ -0,0 +1,36 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## For whatever reason, sniffing returns bad data
+
+my $t = mock_sniff_client(
+    { nodes => ['one'] },
+    { node  => 1, code => 200, content => '{"nodes":{"one":{}}}' },
+
+    # throw NoNodes
+);
+
+ok !eval { $t->perform_sync_request }
+    && $@ =~ /NoNodes/,
+    "Missing http_address";
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/39_sniff_max_content.t b/t/50_Cxn_Pool_Async/39_sniff_max_content.t
new file mode 100644
index 0000000..5c3872f
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/39_sniff_max_content.t
@@ -0,0 +1,74 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_sniff_client);
+
+## Dynamic max content length
+
+my $response = <<RESPONSE;
+{
+    "nodes": {
+        "one": {
+            "http_address": "inet[/one]",
+            "http": {
+                "max_content_length_in_bytes": 200
+            }
+        },
+        "two": {
+            "http_address": "inet[/two]",
+            "http": {
+                "max_content_length_in_bytes": 509
+            }
+        },
+        "three": {
+            "http_address": "inet[/two]"
+        }
+    }
+}
+RESPONSE
+
+my $t = mock_sniff_client(
+    { nodes => ['one'] },
+    { node  => 1, code => 200, content => $response },
+    { node  => 2, code => 200, content => 1 },
+);
+
+$t->perform_sync_request
+    && $t->cxn_pool->next_cxn->then(
+    sub {
+        is shift()->max_content_length, 200, "Dynamic max content length";
+    }
+    );
+
+$t = mock_sniff_client(
+    { nodes => ['one'], max_content_length => 1000 },
+    { node => 1, code => 200, content => $response },
+    { node => 2, code => 200, content => 1 },
+);
+
+$t->perform_sync_request
+    && $t->cxn_pool->next_cxn->then(
+    sub {
+        is shift()->max_content_length, 1000, "Dynamic max content length";
+    }
+    );
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/40_sniff_extract_host.t b/t/50_Cxn_Pool_Async/40_sniff_extract_host.t
new file mode 100644
index 0000000..71829f9
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/40_sniff_extract_host.t
@@ -0,0 +1,39 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Search::Elasticsearch::Async;
+
+my $pool
+    = Search::Elasticsearch::Async->new( cxn_pool => 'Async::Sniff' )
+    ->transport->cxn_pool;
+
+is $pool->_extract_host('127.0.0.1:9200'), '127.0.0.1:9200', "IP";
+
+is $pool->_extract_host('myhost/127.0.0.1:9200'), '127.0.0.1:9200', "Host/IP";
+
+is $pool->_extract_host('inet[127.0.0.1:9200]'), '127.0.0.1:9200', "inet[IP]";
+
+is $pool->_extract_host('inet[myhost/127.0.0.1:9200]'), '127.0.0.1:9200',
+    "inet[Host/IP]";
+
+is $pool->_extract_host('inet[/127.0.0.1:9200]'), '127.0.0.1:9200',
+    "inet[/IP]";
+
+ok !$pool->_extract_host(), "Undefined";
+
+done_testing;
diff --git a/t/50_Cxn_Pool_Async/50_noping_normal.t b/t/50_Cxn_Pool_Async/50_noping_normal.t
new file mode 100644
index 0000000..04837db
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/50_noping_normal.t
@@ -0,0 +1,46 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_noping_client);
+
+## All nodes respond
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Round robin';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/51_noping_node_fails.t b/t/50_Cxn_Pool_Async/51_noping_node_fails.t
new file mode 100644
index 0000000..17195fc
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/51_noping_node_fails.t
@@ -0,0 +1,52 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_noping_client);
+
+## Node fails and recover
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 509, error   => 'Cxn' },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+    # force check
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->cxn_pool->cxns->[1]->force_ping
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Node fails and recovers';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/52_noping_node_timesout.t b/t/50_Cxn_Pool_Async/52_noping_node_timesout.t
new file mode 100644
index 0000000..17c9b5c
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/52_noping_node_timesout.t
@@ -0,0 +1,54 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_noping_client);
+
+## Nodes fail and recover
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 509, error   => 'Timeout' },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+    # force check
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Timeout/
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->cxn_pool->cxns->[1]->force_ping
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Node timesout and recovers';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/53_noping_all_nodes_fail.t b/t/50_Cxn_Pool_Async/53_noping_all_nodes_fail.t
new file mode 100644
index 0000000..abc2f87
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/53_noping_all_nodes_fail.t
@@ -0,0 +1,59 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_noping_client);
+
+## All nodes fail and recover
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 509, error   => 'Cxn' },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 509, error   => 'Cxn' },
+    { node => 3, code => 509, error   => 'Cxn' },
+    { node => 2, code => 200, content => 1 },
+
+    # force check
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->cxn_pool->cxns->[0]->force_ping
+    && $t->cxn_pool->cxns->[2]->force_ping
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'All nodes fail and recover';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/54_noping_nodes_starting.t b/t/50_Cxn_Pool_Async/54_noping_nodes_starting.t
new file mode 100644
index 0000000..f342a1c
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/54_noping_nodes_starting.t
@@ -0,0 +1,50 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_noping_client);
+
+## Nodes initially unavailable
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 509, error   => 'Cxn' },
+    { node => 2, code => 509, error   => 'Cxn' },
+    { node => 3, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+    # force check
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->cxn_pool->cxns->[0]->force_ping
+    && $t->cxn_pool->cxns->[1]->force_ping
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Nodes starting';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/55_noping_runaway_nodes.t b/t/50_Cxn_Pool_Async/55_noping_runaway_nodes.t
new file mode 100644
index 0000000..00f0edc
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/55_noping_runaway_nodes.t
@@ -0,0 +1,56 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_noping_client);
+
+## Runaway nodes
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ] },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 509, error   => 'Unavailable' },
+    { node => 2, code => 509, error   => 'Unavailable' },
+    { node => 3, code => 509, error   => 'Unavailable' },
+
+    # throws unavailable
+    { node => 1, code => 200, content => 1 },
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Unavailable/
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Runaway nodes';
+
+done_testing;
+
diff --git a/t/50_Cxn_Pool_Async/56_max_retries.t b/t/50_Cxn_Pool_Async/56_max_retries.t
new file mode 100644
index 0000000..03abf07
--- /dev/null
+++ b/t/50_Cxn_Pool_Async/56_max_retries.t
@@ -0,0 +1,51 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Search::Elasticsearch::Async;
+use lib 't/lib';
+use MockAsyncCxn qw(mock_noping_client);
+
+## Max retries
+
+my $t = mock_noping_client(
+    { nodes => [ 'one', 'two', 'three' ], max_retries => 1 },
+
+    { node => 1, code => 200, content => 1 },
+    { node => 2, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+    { node => 1, code => 509, error   => 'Unavailable' },
+    { node => 2, code => 509, error   => 'Unavailable' },
+
+    # throws unavailable
+    { node => 3, code => 200, content => 1 },
+    { node => 3, code => 200, content => 1 },
+
+);
+
+ok $t->perform_sync_request()
+    && $t->perform_sync_request
+    && $t->perform_sync_request
+    && !eval { $t->perform_sync_request }
+    && $@ =~ /Unavailable/
+    && $t->perform_sync_request
+    && $t->perform_sync_request,
+    'Max retries';
+
+done_testing;
+
diff --git a/t/60_Cxn/10_basic.t b/t/60_Cxn/10_basic.t
new file mode 100644
index 0000000..9aaa1ef
--- /dev/null
+++ b/t/60_Cxn/10_basic.t
@@ -0,0 +1,61 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Search::Elasticsearch;
+
+my $c = Search::Elasticsearch->new->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+# MARK LIVE
+
+$c->mark_live;
+
+ok $c->is_live,       "Cxn is live";
+is $c->ping_failures, 0, "No ping failures";
+is $c->next_ping,     0, "No ping scheduled";
+
+# MARK DEAD
+
+$c->mark_dead;
+
+ok $c->is_dead, "Cxn is dead";
+is $c->ping_failures, 1, "Has ping failure";
+ok $c->next_ping > time(), "Ping scheduled";
+ok $c->next_ping <= time() + $c->dead_timeout, "Dead timeout x 1";
+
+$c->mark_dead;
+ok $c->is_dead, "Cxn still dead";
+is $c->ping_failures, 2, "Has 2 ping failures";
+ok $c->next_ping > time(), "Ping scheduled";
+ok $c->next_ping <= time() + 2 * $c->dead_timeout, "Dead timeout x 2";
+
+$c->mark_dead for 1 .. 100;
+ok $c->is_dead, "Cxn still dead";
+is $c->ping_failures, 102, "Has 102 ping failures";
+ok $c->next_ping > time(), "Ping scheduled";
+ok $c->next_ping <= time() + $c->max_dead_timeout, "Max dead timeout";
+
+# FORCE PING
+
+$c->force_ping;
+ok $c->is_dead,       "Cxn is dead after force ping";
+is $c->ping_failures, 0, "Force ping has no ping failures";
+is $c->next_ping,     -1, "Next ping scheduled for now";
+
+done_testing;
diff --git a/t/60_Cxn/20_process_response.t b/t/60_Cxn/20_process_response.t
new file mode 100644
index 0000000..dbe4615
--- /dev/null
+++ b/t/60_Cxn/20_process_response.t
@@ -0,0 +1,125 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Test::Deep;
+use Search::Elasticsearch;
+use Search::Elasticsearch::Role::Cxn qw(PRODUCT_CHECK_HEADER PRODUCT_CHECK_VALUE);
+
+our $PRODUCT_CHECK_VALUE = $Search::Elasticsearch::Role::Cxn::PRODUCT_CHECK_VALUE;
+our $PRODUCT_CHECK_HEADER = $Search::Elasticsearch::Role::Cxn::PRODUCT_CHECK_HEADER;
+
+my $c = Search::Elasticsearch->new->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn'),
+    'Does Search::Elasticsearch::Role::Cxn';
+
+my ( $code, $response );
+
+### OK GET
+( $code, $response )
+    = $c->process_response( { method => 'GET', ignore => [] },
+    200, "OK", '{"ok":1}', { 'content-type' => 'application/json', $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE } );
+
+is $code, 200, "OK GET - code";
+cmp_deeply $response, { ok => 1 }, "OK GET - body";
+
+### OK GET - Text body
+( $code, $response )
+    = $c->process_response( { method => 'GET', ignore => [] },
+    200, "OK", 'Foo', { 'content-type' => 'text/plain', $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE } );
+
+is $code,             200,   "OK GET Text body - code";
+cmp_deeply $response, 'Foo', "OK GET Text body - body";
+
+### OK GET - Empty body
+( $code, $response )
+    = $c->process_response( { method => 'GET', ignore => [] },
+    200, "OK", '', { $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE } );
+
+is $code,             200, "OK GET Empty body - code";
+cmp_deeply $response, '',  "OK GET Empty body - body";
+
+### OK HEAD
+( $code, $response )
+    = $c->process_response( { method => 'HEAD', ignore => [] }, 200, "OK", '', { $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE } );
+
+is $code,     200, "OK HEAD - code";
+is $response, 1,   "OK HEAD - body";
+
+### Missing GET
+throws_ok {
+    $c->process_response(
+        { method => 'GET', ignore => [] },
+        404, "Missing",
+        '{"error": "Something is missing"}',
+        { 'content-type' => 'application/json' }
+    );
+}
+qr/Missing/, "Missing GET";
+
+### Missing GET ignore
+( $code, $response ) = $c->process_response(
+    { method => 'GET', ignore => [404] },
+    404, "Missing",
+    '{"error": "Something is missing"}',
+    { 'content-type' => 'application/json' }
+);
+
+is $code,     404,   "Missing GET - code";
+is $response, undef, "Missing GET - body";
+
+### Missing HEAD
+( $code, $response )
+    = $c->process_response( { method => 'HEAD', ignore => [] },
+    404, "Missing", '', { $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE });
+is $code,     404,   "Missing HEAD - code";
+is $response, undef, "Missing HEAD - body";
+
+### Request error
+throws_ok {
+    $c->process_response(
+        { method => 'GET', ignore => [] },
+        400, "Request",
+        '{"error":"error in body"}',
+        { 'content-type' => 'application/json', $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE }
+    );
+}
+qr/\[400\] error in body/, "Request error";
+
+### Timeout error
+throws_ok {
+    $c->process_response( { method => 'GET', ignore => [] },
+        509, "28: Timed out,read timeout" );
+}
+qr/Timeout/, "Timeout error";
+
+### Product check without x-elastic-product header throws error
+throws_ok {
+    $c->process_response( { method => 'GET', ignore => [] },
+        200, "OK" );
+}
+qr/ProductCheck/, "ProductCheck error";
+
+### Product check with x-elastic-product is OK
+( $code, $response )
+    = $c->process_response( { method => 'GET', ignore => [] },
+    200, "OK", '', { $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE } );
+
+is $code,             200, "OK Product check is present";
+
+done_testing;
diff --git a/t/60_Cxn/30_http.t b/t/60_Cxn/30_http.t
new file mode 100644
index 0000000..b3288a7
--- /dev/null
+++ b/t/60_Cxn/30_http.t
@@ -0,0 +1,267 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Test::Deep;
+use Search::Elasticsearch;
+use Search::Elasticsearch::Role::Cxn;
+use MIME::Base64;
+
+sub is_cxn(@);
+
+my $username     = 'ThisIsAVeryLongUsernameAndThatIsOKYouSee';
+my $password     = 'CorrectHorseBatteryStapleCorrectHorseBatteryStaple';
+my $userinfo     = "$username:$password";
+my $userinfo_b64 = MIME::Base64::encode_base64( $userinfo, "" );
+my $useragent    = Search::Elasticsearch::Role::Cxn::get_user_agent();
+my $metaheader   = Search::Elasticsearch::Role::Cxn::get_meta_header();
+
+### Scalar nodes ###
+
+is_cxn "Default", new_cxn(), {};
+
+is_cxn "Host",
+    new_cxn( nodes => 'foo' ),
+    { host => 'foo', port => '80', uri => 'http://foo:80' };
+
+is_cxn "Host:Port",
+    new_cxn( nodes => 'foo:1000' ),
+    { host => 'foo', port => '1000', uri => 'http://foo:1000' };
+
+is_cxn "HTTPS", new_cxn( nodes => 'https://foo' ),
+    {
+    scheme => 'https',
+    host   => 'foo',
+    port   => '443',
+    uri    => 'https://foo:443'
+    };
+
+is_cxn "Path",
+    new_cxn( nodes => 'foo/bar' ),
+    { host => 'foo', port => '80', uri => 'http://foo:80/bar' };
+
+is_cxn "Userinfo", new_cxn( nodes => "http://$userinfo\@localhost/" ),
+    {
+    port            => '80',
+    uri             => 'http://localhost:80',
+    default_headers => {
+        'Authorization' => "Basic $userinfo_b64",
+        'User-Agent' => $useragent,
+        'x-elastic-client-meta' => $metaheader
+    },
+    userinfo        => $userinfo
+    };
+
+is_cxn "IPv4",
+    new_cxn( nodes => '127.0.0.1' ),
+    { host => '127.0.0.1', port => '80', uri => 'http://127.0.0.1:80' };
+
+is_cxn "Scheme:IPv4", new_cxn( nodes => 'https://127.0.0.1' ),
+    {
+    host   => '127.0.0.1',
+    port   => '443',
+    uri    => 'https://127.0.0.1:443',
+    scheme => 'https'
+    };
+
+is_cxn "IPv4:Port",
+    new_cxn( nodes => '127.0.0.1:1000' ),
+    { host => '127.0.0.1', port => '1000', uri => 'http://127.0.0.1:1000' };
+
+is_cxn "Scheme:IPv4:Port", new_cxn( nodes => 'https://127.0.0.1:1000' ),
+    {
+    host   => '127.0.0.1',
+    port   => '1000',
+    uri    => 'https://127.0.0.1:1000',
+    scheme => 'https'
+    };
+
+is_cxn "IPv6",
+    new_cxn( nodes => '::1' ),
+    { host => '::1', port => '80', uri => 'http://[::1]:80' };
+
+is_cxn "Scheme:IPv6", new_cxn( nodes => 'https://[::1]' ),
+    {
+    host   => '::1',
+    port   => '443',
+    uri    => 'https://[::1]:443',
+    scheme => 'https'
+    };
+
+is_cxn "IPv6:Port",
+    new_cxn( nodes => '[::1]:1000' ),
+    { host => '::1', port => '1000', uri => 'http://[::1]:1000' };
+
+is_cxn "Scheme:IPv6:Port", new_cxn( nodes => 'https://[::1]:1000' ),
+    {
+    host   => '::1',
+    port   => '1000',
+    uri    => 'https://[::1]:1000',
+    scheme => 'https'
+    };
+
+### Options with scalar ###
+
+is_cxn "HTTPS option", new_cxn( nodes => 'foo', use_https => 1 ),
+    {
+    scheme => 'https',
+    host   => 'foo',
+    port   => '443',
+    uri    => 'https://foo:443'
+    };
+
+is_cxn "HTTPS option with settings",
+    new_cxn( nodes => 'http://foo', use_https => 1 ),
+    { scheme => 'http', host => 'foo', port => '80', uri => 'http://foo:80' };
+
+is_cxn "Port option",
+    new_cxn( nodes => 'foo', port => 456 ),
+    { host => 'foo', port => '456', uri => 'http://foo:456' };
+
+is_cxn "Port option with settings",
+    new_cxn( nodes => 'foo:123', port => 456 ),
+    { host => 'foo', port => '123', uri => 'http://foo:123' };
+
+is_cxn "Path option",
+    new_cxn( nodes => 'foo', path_prefix => '/bar/' ),
+    { host => 'foo', port => 80, uri => 'http://foo:80/bar' };
+
+is_cxn "Path option with settings",
+    new_cxn( nodes => 'foo/baz/', path_prefix => '/bar/' ),
+    { host => 'foo', port => 80, uri => 'http://foo:80/baz' };
+
+is_cxn "Userinfo option", new_cxn( nodes => 'foo', userinfo => $userinfo ),
+    {
+    host            => 'foo',
+    port            => 80,
+    uri             => 'http://foo:80',
+    default_headers => {
+        'Authorization' => "Basic $userinfo_b64",
+        'User-Agent' => $useragent,
+        'x-elastic-client-meta' => $metaheader
+    },
+    userinfo        => $userinfo
+    };
+
+is_cxn "Userinfo option with settings",
+
+    # Note that userinfo as specified is explicitly different to that
+    # provided in the nodes string
+    new_cxn( nodes => "$userinfo\@foo", userinfo => 'foo:baz' ),
+    {
+    host            => 'foo',
+    port            => 80,
+    uri             => 'http://foo:80',
+    default_headers => {
+        'Authorization' => "Basic $userinfo_b64",
+        'User-Agent' => $useragent,
+        'x-elastic-client-meta' => $metaheader
+    },
+    userinfo        => $userinfo
+    };
+
+is_cxn "Deflate option",
+    new_cxn( deflate => 1 ),
+    { default_headers => { 'Accept-Encoding' => 'deflate', 'User-Agent' => $useragent, 'x-elastic-client-meta' => $metaheader } };
+
+is_cxn "IPv4 with Port",
+    new_cxn( nodes => '127.0.0.1', port => 456 ),
+    { host => '127.0.0.1', port => '456', uri => 'http://127.0.0.1:456' };
+
+is_cxn "IPv6 with Port",
+    new_cxn( nodes => '::1', port => 456 ),
+    { host => '::1', port => '456', uri => 'http://[::1]:456' };
+
+### Hash ###
+is_cxn "Hash host",
+    new_cxn( nodes => { host => 'foo' } ),
+    { host => 'foo', port => 80, uri => 'http://foo:80' };
+
+is_cxn "Hash port",
+    new_cxn( nodes => { port => '123' } ),
+    { port => 123, uri => 'http://localhost:123' };
+
+is_cxn "Hash path",
+    new_cxn( nodes => { path => 'baz' } ),
+    { port => 80, uri => 'http://localhost:80/baz' };
+
+is_cxn "Hash IPv4 host",
+    new_cxn( nodes => { host => '127.0.0.1' } ),
+    { host => '127.0.0.1', port => 80, uri => 'http://127.0.0.1:80' };
+
+is_cxn "Hash IPv6 host",
+    new_cxn( nodes => { host => '::1' } ),
+    { host => '::1', port => 80, uri => 'http://[::1]:80' };
+
+# Build URI
+is new_cxn()->build_uri( { path => '/' } ), 'http://localhost:9200/',
+    "Default URI";
+
+is new_cxn( { nodes => 'http://localhost:9200/foo' } )
+    ->build_uri( { path => '/_search' } ),
+    'http://localhost:9200/foo/_search',
+    "URI with path";
+
+is new_cxn( { default_qs_params => { session => 'key' } } )
+    ->build_uri( { path => '/_search' } ),
+    'http://localhost:9200/_search?session=key',
+    "default_qs_params";
+
+my $uri = new_cxn( { default_qs_params => { session => 'key' } } )
+    ->build_uri( { path => '/_search', qs => { foo => 'bar' } } );
+
+like $uri, qr{^http://localhost:9200/_search?}, "default_qs_params and qs - 1";
+like $uri, qr{session=key},                     "default_qs_params and qs - 2";
+like $uri, qr{foo=bar},                         "default_qs_params and qs - 3";
+
+is new_cxn( { default_qs_params => { session => 'key' } } )
+    ->build_uri( { path => '/_search', qs => { session => 'bar' } } ),
+    'http://localhost:9200/_search?session=bar',
+    "default_qs_params overwritten";
+
+done_testing;
+
+#===================================
+sub is_cxn (@) {
+#===================================
+    my ( $title, $cxn, $params ) = @_;
+    my %params = (
+        host            => 'localhost',
+        port            => '9200',
+        scheme          => 'http',
+        uri             => 'http://localhost:9200',
+        default_headers => {
+            'User-Agent' => $useragent,
+            'x-elastic-client-meta' => $metaheader
+        },
+        userinfo        => '',
+        %$params
+    );
+
+    for my $key ( sort keys %params ) {
+        my $val = $cxn->$key;
+        $val = "$val" unless ref $val eq 'HASH';
+        cmp_deeply $val, $params{$key}, "$title - $key";
+    }
+}
+
+#===================================
+sub new_cxn {
+#===================================
+    return Search::Elasticsearch->new(@_)->transport->cxn_pool->cxns->[0];
+}
diff --git a/t/60_Cxn_Async/10_basic.t b/t/60_Cxn_Async/10_basic.t
new file mode 100644
index 0000000..d27ba52
--- /dev/null
+++ b/t/60_Cxn_Async/10_basic.t
@@ -0,0 +1,61 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Search::Elasticsearch::Async;
+
+my $c = Search::Elasticsearch::Async->new->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn::Async'),
+    'Does Search::Elasticsearch::Role::Cxn::Async';
+
+# MARK LIVE
+
+$c->mark_live;
+
+ok $c->is_live,       "Cxn is live";
+is $c->ping_failures, 0, "No ping failures";
+is $c->next_ping,     0, "No ping scheduled";
+
+# MARK DEAD
+
+$c->mark_dead;
+
+ok $c->is_dead, "Cxn is dead";
+is $c->ping_failures, 1, "Has ping failure";
+ok $c->next_ping > time(), "Ping scheduled";
+ok $c->next_ping <= time() + $c->dead_timeout, "Dead timeout x 1";
+
+$c->mark_dead;
+ok $c->is_dead, "Cxn still dead";
+is $c->ping_failures, 2, "Has 2 ping failures";
+ok $c->next_ping > time(), "Ping scheduled";
+ok $c->next_ping <= time() + 2 * $c->dead_timeout, "Dead timeout x 2";
+
+$c->mark_dead for 1 .. 100;
+ok $c->is_dead, "Cxn still dead";
+is $c->ping_failures, 102, "Has 102 ping failures";
+ok $c->next_ping > time(), "Ping scheduled";
+ok $c->next_ping <= time() + $c->max_dead_timeout, "Max dead timeout";
+
+# FORCE PING
+
+$c->force_ping;
+ok $c->is_dead,       "Cxn is dead after force ping";
+is $c->ping_failures, 0, "Force ping has no ping failures";
+is $c->next_ping,     -1, "Next ping scheduled for now";
+
+done_testing;
diff --git a/t/60_Cxn_Async/20_process_response.t b/t/60_Cxn_Async/20_process_response.t
new file mode 100644
index 0000000..9288172
--- /dev/null
+++ b/t/60_Cxn_Async/20_process_response.t
@@ -0,0 +1,111 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Test::Deep;
+use Search::Elasticsearch::Async;
+use Search::Elasticsearch::Role::Cxn qw(PRODUCT_CHECK_HEADER PRODUCT_CHECK_VALUE);
+
+our $PRODUCT_CHECK_VALUE = $Search::Elasticsearch::Role::Cxn::PRODUCT_CHECK_VALUE;
+our $PRODUCT_CHECK_HEADER = $Search::Elasticsearch::Role::Cxn::PRODUCT_CHECK_HEADER;
+
+my $c = Search::Elasticsearch::Async->new->transport->cxn_pool->cxns->[0];
+ok $c->does('Search::Elasticsearch::Role::Cxn::Async'),
+    'Does Search::Elasticsearch::Role::Cxn::Async';
+
+my ( $code, $response );
+
+### OK GET
+( $code, $response )
+    = $c->process_response( { method => 'GET', ignore => [] },
+    200, "OK", '{"ok":1}', { 'content-type' => 'application/json', $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE } );
+
+is $code, 200, "OK GET - code";
+cmp_deeply $response, { ok => 1 }, "OK GET - body";
+
+### OK GET - Text body
+( $code, $response )
+    = $c->process_response( { method => 'GET', ignore => [] },
+    200, "OK", 'Foo', { 'content-type' => 'text/plain', $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE } );
+
+is $code,             200,   "OK GET Text body - code";
+cmp_deeply $response, 'Foo', "OK GET Text body - body";
+
+### OK GET - Empty body
+( $code, $response )
+    = $c->process_response( { method => 'GET', ignore => [] }, 200, "OK",
+    '', { $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE } );
+
+is $code,             200, "OK GET Empty body - code";
+cmp_deeply $response, '',  "OK GET Empty body - body";
+
+### OK HEAD
+( $code, $response )
+    = $c->process_response( { method => 'HEAD', ignore => [] }, 200, "OK", '', { $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE });
+
+is $code,     200, "OK HEAD - code";
+is $response, 1,   "OK HEAD - body";
+
+### Missing GET
+throws_ok {
+    $c->process_response(
+        { method => 'GET', ignore => [] },
+        404, "Missing",
+        '{"error": "Something is missing"}',
+        { 'content-type' => 'application/json', $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE }
+    );
+}
+qr/Missing/, "Missing GET";
+
+### Missing GET ignore
+( $code, $response ) = $c->process_response(
+    { method => 'GET', ignore => [404] },
+    404, "Missing",
+    '{"error": "Something is missing"}',
+    { 'content-type' => 'application/json', $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE }
+);
+
+is $code,     404,   "Missing GET - code";
+is $response, undef, "Missing GET - body";
+
+### Missing HEAD
+( $code, $response )
+    = $c->process_response( { method => 'HEAD', ignore => [] },
+    404, "Missing" );
+is $code,     404,   "Missing HEAD - code";
+is $response, undef, "Missing HEAD - body";
+
+### Request error
+throws_ok {
+    $c->process_response(
+        { method => 'GET', ignore => [] },
+        400, "Request",
+        '{"error":"error in body"}',
+        { 'content-type' => 'application/json', $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE }
+    );
+}
+qr/\[400\] error in body/, "Request error";
+
+### Timeout error
+throws_ok {
+    $c->process_response( { method => 'GET', ignore => [] },
+        509, "28: Timed out,read timeout" );
+}
+qr/Timeout/, "Timeout error";
+
+done_testing;
diff --git a/t/60_Cxn_Async/30_http.t b/t/60_Cxn_Async/30_http.t
new file mode 100644
index 0000000..92c135f
--- /dev/null
+++ b/t/60_Cxn_Async/30_http.t
@@ -0,0 +1,191 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Exception;
+use Test::Deep;
+use Search::Elasticsearch::Async;
+use Search::Elasticsearch::Role::Cxn;
+
+sub is_cxn(@);
+
+my $username     = 'ThisIsAVeryLongUsernameAndThatIsOKYouSee';
+my $password     = 'CorrectHorseBatteryStapleCorrectHorseBatteryStaple';
+my $useragent    = Search::Elasticsearch::Role::Cxn::get_user_agent();
+my $metaheader   = Search::Elasticsearch::Role::Cxn::get_meta_header();
+
+### Scalar nodes ###
+
+is_cxn "Default", new_cxn(), {};
+
+is_cxn "Host",
+    new_cxn( nodes => 'foo' ),
+    { host => 'foo', port => '80', uri => 'http://foo:80' };
+
+is_cxn "Host:Port",
+    new_cxn( nodes => 'foo:1000' ),
+    { host => 'foo', port => '1000', uri => 'http://foo:1000' };
+
+is_cxn "HTTPS", new_cxn( nodes => 'https://foo' ),
+    {
+    scheme => 'https',
+    host   => 'foo',
+    port   => '443',
+    uri    => 'https://foo:443'
+    };
+
+is_cxn "Path",
+    new_cxn( nodes => 'foo/bar' ),
+    { host => 'foo', port => '80', uri => 'http://foo:80/bar' };
+
+is_cxn "IPv4",
+    new_cxn( nodes => '127.0.0.1' ),
+    { host => '127.0.0.1', port => '80', uri => 'http://127.0.0.1:80' };
+
+is_cxn "Scheme:IPv4", new_cxn( nodes => 'https://127.0.0.1' ),
+    {
+    host   => '127.0.0.1',
+    port   => '443',
+    uri    => 'https://127.0.0.1:443',
+    scheme => 'https'
+    };
+
+is_cxn "IPv4:Port",
+    new_cxn( nodes => '127.0.0.1:1000' ),
+    { host => '127.0.0.1', port => '1000', uri => 'http://127.0.0.1:1000' };
+
+is_cxn "Scheme:IPv4:Port", new_cxn( nodes => 'https://127.0.0.1:1000' ),
+    {
+    host   => '127.0.0.1',
+    port   => '1000',
+    uri    => 'https://127.0.0.1:1000',
+    scheme => 'https'
+    };
+
+### Options with scalar ###
+
+is_cxn "HTTPS option", new_cxn( nodes => 'foo', use_https => 1 ),
+    {
+    scheme => 'https',
+    host   => 'foo',
+    port   => '443',
+    uri    => 'https://foo:443'
+    };
+
+is_cxn "HTTPS option with settings",
+    new_cxn( nodes => 'http://foo', use_https => 1 ),
+    { scheme => 'http', host => 'foo', port => '80', uri => 'http://foo:80' };
+
+is_cxn "Port option",
+    new_cxn( nodes => 'foo', port => 456 ),
+    { host => 'foo', port => '456', uri => 'http://foo:456' };
+
+is_cxn "Port option with settings",
+    new_cxn( nodes => 'foo:123', port => 456 ),
+    { host => 'foo', port => '123', uri => 'http://foo:123' };
+
+is_cxn "Path option",
+    new_cxn( nodes => 'foo', path_prefix => '/bar/' ),
+    { host => 'foo', port => 80, uri => 'http://foo:80/bar' };
+
+is_cxn "Path option with settings",
+    new_cxn( nodes => 'foo/baz/', path_prefix => '/bar/' ),
+    { host => 'foo', port => 80, uri => 'http://foo:80/baz' };
+
+is_cxn "Deflate option",
+    new_cxn( deflate => 1 ),
+    { default_headers => { 'Accept-Encoding' => 'deflate', 'User-Agent' => $useragent, 'x-elastic-client-meta' => $metaheader } };
+
+is_cxn "IPv4 with Port",
+    new_cxn( nodes => '127.0.0.1', port => 456 ),
+    { host => '127.0.0.1', port => '456', uri => 'http://127.0.0.1:456' };
+
+### Hash ###
+is_cxn "Hash host",
+    new_cxn( nodes => { host => 'foo' } ),
+    { host => 'foo', port => 80, uri => 'http://foo:80' };
+
+is_cxn "Hash port",
+    new_cxn( nodes => { port => '123' } ),
+    { port => 123, uri => 'http://localhost:123' };
+
+is_cxn "Hash path",
+    new_cxn( nodes => { path => 'baz' } ),
+    { port => 80, uri => 'http://localhost:80/baz' };
+
+is_cxn "Hash IPv4 host",
+    new_cxn( nodes => { host => '127.0.0.1' } ),
+    { host => '127.0.0.1', port => 80, uri => 'http://127.0.0.1:80' };
+
+# Build URI
+is new_cxn()->build_uri( { path => '/' } ), 'http://localhost:9200/',
+    "Default URI";
+
+is new_cxn( { nodes => 'http://localhost:9200/foo' } )
+    ->build_uri( { path => '/_search' } ),
+    'http://localhost:9200/foo/_search',
+    "URI with path";
+
+is new_cxn( { default_qs_params => { session => 'key' } } )
+    ->build_uri( { path => '/_search' } ),
+    'http://localhost:9200/_search?session=key',
+    "default_qs_params";
+
+my $uri = new_cxn( { default_qs_params => { session => 'key' } } )
+    ->build_uri( { path => '/_search', qs => { foo => 'bar' } } );
+
+like $uri, qr{^http://localhost:9200/_search?}, "default_qs_params and qs - 1";
+like $uri, qr{session=key},                     "default_qs_params and qs - 2";
+like $uri, qr{foo=bar},                         "default_qs_params and qs - 3";
+
+is new_cxn( { default_qs_params => { session => 'key' } } )
+    ->build_uri( { path => '/_search', qs => { session => 'bar' } } ),
+    'http://localhost:9200/_search?session=bar',
+    "default_qs_params overwritten";
+
+done_testing;
+
+#===================================
+sub is_cxn (@) {
+#===================================
+    my ( $title, $cxn, $params ) = @_;
+    my %params = (
+        host            => 'localhost',
+        port            => '9200',
+        scheme          => 'http',
+        uri             => 'http://localhost:9200',
+        default_headers => {
+            'User-Agent' => $useragent,
+            'x-elastic-client-meta' => $metaheader
+        },
+        userinfo        => '',
+        %$params
+    );
+
+    for my $key ( sort keys %params ) {
+        my $val = $cxn->$key;
+        $val = "$val" unless ref $val eq 'HASH';
+        cmp_deeply $val, $params{$key}, "$title - $key";
+    }
+}
+
+#===================================
+sub new_cxn {
+#===================================
+    return Search::Elasticsearch::Async->new(@_)
+        ->transport->cxn_pool->cxns->[0];
+}
diff --git a/t/95_TestServer/00_test_server.t b/t/95_TestServer/00_test_server.t
new file mode 100644
index 0000000..49c9a1c
--- /dev/null
+++ b/t/95_TestServer/00_test_server.t
@@ -0,0 +1,81 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use File::Temp;
+use POSIX ":sys_wait_h";
+
+use Search::Elasticsearch;
+use Search::Elasticsearch::TestServer;
+
+my @pids;
+
+SKIP: {
+    skip 'ES_HOME not set', 7 unless $ENV{ES_HOME};
+
+    my $tempdir = File::Temp->newdir( 'testserver-XXXXX', DIR => '/tmp' );
+    my $server = Search::Elasticsearch::TestServer->new;
+
+    my $nodes = $server->start();
+
+    ok( $nodes, "server->start returned nodes" )
+        or diag explain { server => $server };
+    ok( defined( $server->pids ), "server->pids defined" );
+    cmp_ok( scalar @{ $server->pids }, '>', 0, "more than 0 pids" );
+    @pids = @{ $server->pids };
+
+    subtest 'ES pids are alive' => sub {
+        verify_pids_alive(@pids);
+    };
+
+    $server->shutdown;
+
+    note 'sleep to give ES time to die';
+    sleep 5;
+
+    subtest 'ES pids are dead after shutdown' => sub {
+        verify_pids_dead(@pids);
+    };
+
+    eval { $server->shutdown };
+    is( $@, '', "second shutdown did not set error" );
+
+    subtest 'ES pids are dead after second shutdown' => sub {
+        verify_pids_dead(@pids);
+    };
+}
+
+done_testing;
+
+#important to waitpid or kill0 will return true for zombies.
+sub verify_pids_alive {
+    for my $pid (@_) {
+        waitpid( $pid, WNOHANG );
+        ok( kill( 0, $pid ), "pid $pid is alive" );
+    }
+}
+
+sub verify_pids_dead {
+    for my $pid (@_) {
+        waitpid( $pid, WNOHANG );
+        ok( !kill( 0, $pid ), "pid $pid is dead" );
+    }
+}
diff --git a/t/95_TestServer/10_test_server_fork.t b/t/95_TestServer/10_test_server_fork.t
new file mode 100644
index 0000000..d68e35a
--- /dev/null
+++ b/t/95_TestServer/10_test_server_fork.t
@@ -0,0 +1,92 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::SharedFork;
+
+use File::Temp;
+use POSIX ":sys_wait_h";
+
+use Search::Elasticsearch;
+use Search::Elasticsearch::TestServer;
+
+my $pids = [];
+
+SKIP: {
+    skip 'ES_HOME not set', 8 unless $ENV{ES_HOME};
+
+    my $tempdir = File::Temp->newdir( 'testserver-XXXXX', DIR => '/tmp' );
+    my $server = Search::Elasticsearch::TestServer->new();
+
+    my $nodes = $server->start();
+
+    ok( $nodes, "server->start returned nodes" )
+        or diag explain { server => $server };
+    ok( defined( $server->pids ), "server->pids defined" );
+    cmp_ok( scalar @{ $server->pids }, '>', 0, "more than 0 pids" );
+    $pids = \@{ $server->pids };
+
+    verify_pids_alive( $pids, 'ES pids are alive' );
+
+    {
+        my $pid = fork;
+        die "cannot fork" unless defined $pid;
+
+        if ( $pid == 0 ) {
+            verify_pids_alive( $pids, 'ES pids are alive in child' );
+            exit 0;
+        }
+        else {
+            verify_pids_alive( $pids, 'ES pids are alive in parent' );
+            waitpid( $pid, 0 );
+            sleep 5;
+            verify_pids_alive( $pids,
+                'ES pids are alive in parent after child dies' );
+        }
+    }
+
+    $server->shutdown;
+
+    note 'sleep to give ES time to die';
+    sleep 5;
+
+    verify_pids_dead( $pids, 'ES pids are dead after shutdown' );
+
+}
+done_testing;
+
+#important to waitpid or kill0 will return true for zombies.
+sub verify_pids_alive {
+    my ( $pids, $msg ) = @_;
+    $msg = '' if !defined $msg;
+    for my $pid (@$pids) {
+        waitpid( $pid, WNOHANG );
+        ok( kill( 0, $pid ), "$msg: pid $pid is alive" );
+    }
+}
+
+sub verify_pids_dead {
+    my ( $pids, $msg ) = @_;
+    $msg = '' if !defined $msg;
+    for my $pid (@$pids) {
+        waitpid( $pid, WNOHANG );
+        ok( !kill( 0, $pid ), "$msg: pid $pid is dead" );
+    }
+}
diff --git a/t/Client_2_0/00_print_version.t b/t/Client_2_0/00_print_version.t
deleted file mode 100644
index 58e75fa..0000000
--- a/t/Client_2_0/00_print_version.t
+++ /dev/null
@@ -1,23 +0,0 @@
-use Test::More;
-use lib 't/lib';
-$ENV{ES_VERSION} = '2_0';
-my $es = do "es_sync.pl" or die( $@ || $! );
-
-eval {
-    my $v = $es->info->{version};
-    diag "";
-    diag "";
-    diag "Testing against Elasticsearch v" . $v->{number};
-    for ( sort keys %$v ) {
-        diag sprintf "%-20s: %s", $_, $v->{$_};
-    }
-    diag "";
-    diag "Client:   " . ref($es);
-    diag "Cxn:      " . $es->transport->cxn_pool->cxn_factory->cxn_class;
-    diag "GET Body: " . $es->transport->send_get_body_as;
-    diag "";
-    pass "ES Version";
-} or fail "ES Version";
-
-done_testing;
-
diff --git a/t/Client_2_0/10_live.t b/t/Client_2_0/10_live.t
deleted file mode 100644
index 4a9c3c0..0000000
--- a/t/Client_2_0/10_live.t
+++ /dev/null
@@ -1,27 +0,0 @@
-use Test::More;
-use Test::Deep;
-use Test::Exception;
-use strict;
-use warnings;
-use lib 't/lib';
-
-my $es;
-$ENV{ES_VERSION} = '2_0';
-local $ENV{ES_CXN_POOL};
-
-$ENV{ES_CXN_POOL} = 'Static';
-$es = do "es_sync.pl" or die( $@ || $! );
-is $es->info->{tagline}, "You Know, for Search", 'CxnPool::Static';
-
-$ENV{ES_CXN_POOL} = 'Static::NoPing';
-$es = do "es_sync.pl" or die( $@ || $! );
-is $es->info->{tagline}, "You Know, for Search", 'CxnPool::Static::NoPing';
-
-$ENV{ES_CXN_POOL} = 'Sniff';
-$es = do "es_sync.pl" or die( $@ || $! );
-is $es->info->{tagline}, "You Know, for Search", 'CxnPool::Sniff';
-
-my ($node) = values %{ $es->transport->cxn_pool->next_cxn->sniff };
-ok $node->{http}{max_content_length_in_bytes}, 'Sniffs max_content length';
-
-done_testing;
diff --git a/t/Client_2_0/15_conflict.t b/t/Client_2_0/15_conflict.t
deleted file mode 100644
index ae95ac5..0000000
--- a/t/Client_2_0/15_conflict.t
+++ /dev/null
@@ -1,29 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-use lib 't/lib';
-
-$ENV{ES_VERSION} = '2_0';
-my $es = do "es_sync.pl" or die( $@ || $! );
-
-$es->indices->delete( index => '_all' );
-
-$es->index( index => 'test', type => 'test', id => 1, body => {} );
-
-my $error;
-
-eval {
-    $es->index(
-        index   => 'test',
-        type    => 'test',
-        id      => 1,
-        body    => {},
-        version => 2
-    );
-    1;
-} or $error = $@;
-
-ok $error->is('Conflict'), 'Conflict Exception';
-is $error->{vars}{current_version}, 1, "Error has current version v1";
-
-done_testing;
diff --git a/t/Client_2_0/20_fork_httptiny.t b/t/Client_2_0/20_fork_httptiny.t
deleted file mode 100644
index 4a3665c..0000000
--- a/t/Client_2_0/20_fork_httptiny.t
+++ /dev/null
@@ -1,6 +0,0 @@
-use lib 't/lib';
-
-$ENV{ES_VERSION} = '2_0';
-$ENV{ES_CXN} = 'HTTPTiny';
-do "es_sync_fork.pl" or die( $@ || $! );
-
diff --git a/t/Client_2_0/21_fork_lwp.t b/t/Client_2_0/21_fork_lwp.t
deleted file mode 100644
index 0a969ba..0000000
--- a/t/Client_2_0/21_fork_lwp.t
+++ /dev/null
@@ -1,6 +0,0 @@
-use lib 't/lib';
-
-$ENV{ES_VERSION} = '2_0';
-$ENV{ES_CXN} = 'LWP';
-do "es_sync_fork.pl" or die( $@ || $! );
-
diff --git a/t/Client_2_0/22_fork_hijk.t b/t/Client_2_0/22_fork_hijk.t
deleted file mode 100644
index 1f004a6..0000000
--- a/t/Client_2_0/22_fork_hijk.t
+++ /dev/null
@@ -1,6 +0,0 @@
-use lib 't/lib';
-
-$ENV{ES_VERSION} = '2_0';
-$ENV{ES_CXN} = 'Hijk';
-do "es_sync_fork.pl" or die( $@ || $! );
-
diff --git a/t/Client_2_0/30_bulk_add_action.t b/t/Client_2_0/30_bulk_add_action.t
deleted file mode 100644
index 226d2e9..0000000
--- a/t/Client_2_0/30_bulk_add_action.t
+++ /dev/null
@@ -1,277 +0,0 @@
-use Test::More;
-use Test::Deep;
-use Test::Exception;
-use strict;
-use warnings;
-use lib 't/lib';
-
-$ENV{ES_VERSION} = '2_0';
-my $es = do "es_sync.pl" or die( $@ || $! );
-my $b = $es->bulk_helper;
-
-$b->_serializer->_set_canonical;
-
-## EMPTY
-
-ok $b->add_action(), 'Empty add action';
-
-## INDEX ACTIONS ##
-
-ok $b->add_action(
-    index => {
-        index        => 'foo',
-        type         => 'bar',
-        id           => 1,
-        routing      => 1,
-        parent       => 1,
-        timestamp    => 1380019061000,
-        ttl          => '10m',
-        version      => 1,
-        version_type => 'external',
-        source       => { foo => 'bar' },
-    },
-    index => {
-        _index        => 'foo',
-        _type         => 'bar',
-        _id           => 2,
-        _routing      => 2,
-        _parent       => 2,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        _source       => { foo => 'bar' },
-    }
-    ),
-    'Add index actions';
-
-cmp_deeply $b->_buffer,
-    [
-    q({"index":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"foo":"bar"}),
-    q({"index":{"_id":2,"_index":"foo","_parent":2,"_routing":2,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"foo":"bar"})
-    ],
-    "Index actions in buffer";
-
-is $b->_buffer_size,  336, "Index actions buffer size";
-is $b->_buffer_count, 2,   "Index actions buffer count";
-
-$b->clear_buffer;
-
-## CREATE ACTIONS ##
-
-ok $b->add_action(
-    create => {
-        index        => 'foo',
-        type         => 'bar',
-        id           => 1,
-        routing      => 1,
-        parent       => 1,
-        timestamp    => 1380019061000,
-        ttl          => '10m',
-        version      => 1,
-        version_type => 'external',
-        source       => { foo => 'bar' },
-    },
-    create => {
-        _index        => 'foo',
-        _type         => 'bar',
-        _id           => 2,
-        _routing      => 2,
-        _parent       => 2,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        _source       => { foo => 'bar' },
-    }
-    ),
-    'Add create actions';
-
-cmp_deeply $b->_buffer,
-    [
-    q({"create":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"foo":"bar"}),
-    q({"create":{"_id":2,"_index":"foo","_parent":2,"_routing":2,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"foo":"bar"})
-    ],
-    "Create actions in buffer";
-
-is $b->_buffer_size,  338, "Create actions buffer size";
-is $b->_buffer_count, 2,   "Create actions buffer count";
-
-$b->clear_buffer;
-
-## DELETE ACTIONS ##
-
-ok $b->add_action(
-    delete => {
-        index        => 'foo',
-        type         => 'bar',
-        id           => 1,
-        routing      => 1,
-        parent       => 1,
-        version      => 1,
-        version_type => 'external',
-    },
-    delete => {
-        _index        => 'foo',
-        _type         => 'bar',
-        _id           => 2,
-        _routing      => 2,
-        _parent       => 2,
-        _version      => 1,
-        _version_type => 'external',
-    }
-    ),
-    'Add delete actions';
-
-cmp_deeply $b->_buffer,
-    [
-    q({"delete":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"delete":{"_id":2,"_index":"foo","_parent":2,"_routing":2,"_type":"bar","_version":1,"_version_type":"external"}}),
-    ],
-    "Delete actions in buffer";
-
-is $b->_buffer_size,  230, "Delete actions buffer size";
-is $b->_buffer_count, 2,   "Delete actions buffer count";
-
-$b->clear_buffer;
-
-## UPDATE ACTIONS ##
-
-ok $b->add_action(
-    update => {
-        index         => 'foo',
-        type          => 'bar',
-        id            => 1,
-        routing       => 1,
-        parent        => 1,
-        timestamp     => 1380019061000,
-        ttl           => '10m',
-        version       => 1,
-        version_type  => 'external',
-        doc           => { foo => 'bar' },
-        doc_as_upsert => 1,
-    },
-    update => {
-        index        => 'foo',
-        type         => 'bar',
-        id           => 1,
-        routing      => 1,
-        parent       => 1,
-        timestamp    => 1380019061000,
-        ttl          => '10m',
-        version      => 1,
-        version_type => 'external',
-        upsert       => { counter => 0 },
-        script       => '_ctx.source.counter+=incr',
-        lang         => 'mvel',
-        params       => { incr => 1 },
-    },
-    update => {
-        _index        => 'foo',
-        _type         => 'bar',
-        _id           => 1,
-        _routing      => 1,
-        _parent       => 1,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        doc           => { foo => 'bar' },
-        doc_as_upsert => 1,
-    },
-    update => {
-        _index        => 'foo',
-        _type         => 'bar',
-        _id           => 1,
-        _routing      => 1,
-        _parent       => 1,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        upsert        => { counter => 0 },
-        script        => '_ctx.source.counter+=incr',
-        lang          => 'mvel',
-        params        => { incr => 1 },
-    },
-    update => {
-        _index        => 'foo',
-        _type         => 'bar',
-        _id           => 1,
-        _routing      => 1,
-        _parent       => 1,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        doc           => { foo => 'bar' },
-        doc_as_upsert => 1,
-        detect_noop   => 1,
-    },
-    update => {
-        _index             => 'foo',
-        _type              => 'bar',
-        _id                => 1,
-        _routing           => 1,
-        _parent            => 1,
-        _timestamp         => 1380019061000,
-        _ttl               => '10m',
-        _version           => 1,
-        _version_type      => 'external',
-        upsert             => { counter => 0 },
-        script             => '_ctx.source.counter+=incr',
-        lang               => 'mvel',
-        params             => { incr => 1 },
-        detect_noop        => 1,
-        _retry_on_conflict => 3,
-    },
-    ),
-    'Add update actions';
-
-cmp_deeply $b->_buffer,
-    [
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"doc":{"foo":"bar"},"doc_as_upsert":1}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"lang":"mvel","params":{"incr":1},"script":"_ctx.source.counter+=incr","upsert":{"counter":0}}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"doc":{"foo":"bar"},"doc_as_upsert":1}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"lang":"mvel","params":{"incr":1},"script":"_ctx.source.counter+=incr","upsert":{"counter":0}}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"detect_noop":1,"doc":{"foo":"bar"},"doc_as_upsert":1}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"_retry_on_conflict":3,"detect_noop":1,"lang":"mvel","params":{"incr":1},"script":"_ctx.source.counter+=incr","upsert":{"counter":0}}),
-    ],
-    "Update actions in buffer";
-
-is $b->_buffer_size,  1393, "Update actions buffer size";
-is $b->_buffer_count, 6,    "Update actions buffer count";
-
-$b->clear_buffer;
-
-## ERRORS ##
-throws_ok { $b->add_action( 'foo' => {} ) } qr/Unrecognised action/,
-    'Bad action';
-
-throws_ok { $b->add_action( 'index', 'bar' ) } qr/Missing <params>/,
-    'Missing params';
-
-throws_ok { $b->add_action( index => { type => 't' } ) }
-qr/Missing .*<index>/, 'Missing index';
-throws_ok { $b->add_action( index => { index => 'i' } ) }
-qr/Missing .*<type>/, 'Missing type';
-throws_ok { $b->add_action( index => { index => 'i', type => 't' } ) }
-qr/Missing <source>/, 'Missing source';
-
-throws_ok {
-    $b->add_action(
-        index => { index => 'i', type => 't', _source => {}, foo => 1 } );
-}
-qr/Unknown params/, 'Unknown params';
-
-done_testing;
diff --git a/t/Client_2_0/31_bulk_helpers.t b/t/Client_2_0/31_bulk_helpers.t
deleted file mode 100644
index 0f60b0a..0000000
--- a/t/Client_2_0/31_bulk_helpers.t
+++ /dev/null
@@ -1,281 +0,0 @@
-use Test::More;
-use Test::Deep;
-use Test::Exception;
-use strict;
-use warnings;
-use lib 't/lib';
-
-$ENV{ES_VERSION} = '2_0';
-my $es = do "es_sync.pl" or die( $@ || $! );
-my $b = $es->bulk_helper(
-    index => 'i',
-    type  => 't'
-);
-my $s = $b->_serializer;
-$s->_set_canonical;
-
-## INDEX ##
-
-ok $b->index(), 'Empty index';
-
-ok $b->index(
-    {   index        => 'foo',
-        type         => 'bar',
-        id           => 1,
-        routing      => 1,
-        parent       => 1,
-        timestamp    => 1380019061000,
-        ttl          => '10m',
-        version      => 1,
-        version_type => 'external',
-        source       => { foo => 'bar' },
-    },
-    {   _index        => 'foo',
-        _type         => 'bar',
-        _id           => 2,
-        _routing      => 2,
-        _parent       => 2,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        _source       => { foo => 'bar' },
-    }
-    ),
-    'Index';
-
-cmp_deeply $b->_buffer,
-    [
-    q({"index":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"foo":"bar"}),
-    q({"index":{"_id":2,"_index":"foo","_parent":2,"_routing":2,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"foo":"bar"})
-    ],
-    "Index in buffer";
-
-is $b->_buffer_size,  336, "Index buffer size";
-is $b->_buffer_count, 2,   "Index buffer count";
-
-$b->clear_buffer;
-
-## CREATE ##
-
-ok $b->create(), 'Create empty';
-
-ok $b->create(
-    {   index        => 'foo',
-        type         => 'bar',
-        id           => 1,
-        routing      => 1,
-        parent       => 1,
-        timestamp    => 1380019061000,
-        ttl          => '10m',
-        version      => 1,
-        version_type => 'external',
-        source       => { foo => 'bar' },
-    },
-    {   _index        => 'foo',
-        _type         => 'bar',
-        _id           => 2,
-        _routing      => 2,
-        _parent       => 2,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        _source       => { foo => 'bar' },
-    }
-    ),
-    'Create';
-
-cmp_deeply $b->_buffer,
-    [
-    q({"create":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"foo":"bar"}),
-    q({"create":{"_id":2,"_index":"foo","_parent":2,"_routing":2,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"foo":"bar"})
-    ],
-    "Create in buffer";
-
-is $b->_buffer_size,  338, "Create buffer size";
-is $b->_buffer_count, 2,   "Create buffer count";
-
-$b->clear_buffer;
-
-## CREATE DOCS##
-
-ok $b->create_docs(), 'Create_docs empty';
-
-ok $b->create_docs( { foo => 'bar' }, { foo => 'baz' } ), 'Create docs';
-
-cmp_deeply $b->_buffer,
-    [ q({"create":{}}), q({"foo":"bar"}), q({"create":{}}), q({"foo":"baz"}) ],
-    "Create docs in buffer";
-
-is $b->_buffer_size,  56, "Create docs buffer size";
-is $b->_buffer_count, 2,  "Create docs buffer count";
-
-$b->clear_buffer;
-
-## DELETE ##
-ok $b->delete(), 'Delete empty';
-
-ok $b->delete(
-    {   index        => 'foo',
-        type         => 'bar',
-        id           => 1,
-        routing      => 1,
-        parent       => 1,
-        version      => 1,
-        version_type => 'external',
-    },
-    {   _index        => 'foo',
-        _type         => 'bar',
-        _id           => 2,
-        _routing      => 2,
-        _parent       => 2,
-        _version      => 1,
-        _version_type => 'external',
-    }
-    ),
-    'Delete';
-
-cmp_deeply $b->_buffer,
-    [
-    q({"delete":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"delete":{"_id":2,"_index":"foo","_parent":2,"_routing":2,"_type":"bar","_version":1,"_version_type":"external"}}),
-    ],
-    "Delete in buffer";
-
-is $b->_buffer_size,  230, "Delete buffer size";
-is $b->_buffer_count, 2,   "Delete buffer count";
-
-$b->clear_buffer;
-
-## DELETE IDS ##
-ok $b->delete_ids(), 'Delete IDs empty';
-
-ok $b->delete_ids( 1, 2, 3 ), 'Delete IDs';
-
-cmp_deeply $b->_buffer,
-    [
-    q({"delete":{"_id":1}}), q({"delete":{"_id":2}}),
-    q({"delete":{"_id":3}}),
-    ],
-    "Delete IDs in buffer";
-
-is $b->_buffer_size,  63, "Delete IDs buffer size";
-is $b->_buffer_count, 3,  "Delete IDS buffer count";
-
-$b->clear_buffer;
-
-## UPDATE ACTIONS ##
-ok $b->update(), 'Update empty';
-ok $b->update(
-    {   index         => 'foo',
-        type          => 'bar',
-        id            => 1,
-        routing       => 1,
-        parent        => 1,
-        timestamp     => 1380019061000,
-        ttl           => '10m',
-        version       => 1,
-        version_type  => 'external',
-        doc           => { foo => 'bar' },
-        doc_as_upsert => 1,
-    },
-    {   index        => 'foo',
-        type         => 'bar',
-        id           => 1,
-        routing      => 1,
-        parent       => 1,
-        timestamp    => 1380019061000,
-        ttl          => '10m',
-        version      => 1,
-        version_type => 'external',
-        upsert       => { counter => 0 },
-        script       => '_ctx.source.counter+=incr',
-        lang         => 'mvel',
-        params       => { incr => 1 },
-    },
-    {   _index        => 'foo',
-        _type         => 'bar',
-        _id           => 1,
-        _routing      => 1,
-        _parent       => 1,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        doc           => { foo => 'bar' },
-        doc_as_upsert => 1,
-    },
-    {   _index        => 'foo',
-        _type         => 'bar',
-        _id           => 1,
-        _routing      => 1,
-        _parent       => 1,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        upsert        => { counter => 0 },
-        script        => '_ctx.source.counter+=incr',
-        lang          => 'mvel',
-        params        => { incr => 1 },
-    },
-    {   _index        => 'foo',
-        _type         => 'bar',
-        _id           => 1,
-        _routing      => 1,
-        _parent       => 1,
-        _timestamp    => 1380019061000,
-        _ttl          => '10m',
-        _version      => 1,
-        _version_type => 'external',
-        doc           => { foo => 'bar' },
-        doc_as_upsert => 1,
-        detect_noop   => 1,
-    },
-    {   _index             => 'foo',
-        _type              => 'bar',
-        _id                => 1,
-        _routing           => 1,
-        _parent            => 1,
-        _timestamp         => 1380019061000,
-        _ttl               => '10m',
-        _version           => 1,
-        _version_type      => 'external',
-        upsert             => { counter => 0 },
-        script             => '_ctx.source.counter+=incr',
-        lang               => 'mvel',
-        params             => { incr => 1 },
-        detect_noop        => 1,
-        _retry_on_conflict => 3,
-    },
-    ),
-    'Update';
-
-cmp_deeply $b->_buffer,
-    [
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"doc":{"foo":"bar"},"doc_as_upsert":1}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"lang":"mvel","params":{"incr":1},"script":"_ctx.source.counter+=incr","upsert":{"counter":0}}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"doc":{"foo":"bar"},"doc_as_upsert":1}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"lang":"mvel","params":{"incr":1},"script":"_ctx.source.counter+=incr","upsert":{"counter":0}}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"detect_noop":1,"doc":{"foo":"bar"},"doc_as_upsert":1}),
-    q({"update":{"_id":1,"_index":"foo","_parent":1,"_routing":1,"_timestamp":1380019061000,"_ttl":"10m","_type":"bar","_version":1,"_version_type":"external"}}),
-    q({"_retry_on_conflict":3,"detect_noop":1,"lang":"mvel","params":{"incr":1},"script":"_ctx.source.counter+=incr","upsert":{"counter":0}}),
-    ],
-    "Update in buffer";
-
-is $b->_buffer_size,  1393, "Update buffer size";
-is $b->_buffer_count, 6,    "Update buffer count";
-
-$b->clear_buffer;
-
-done_testing;
diff --git a/t/Client_2_0/34_bulk_cxn_errors.t b/t/Client_2_0/34_bulk_cxn_errors.t
deleted file mode 100644
index 7fd0cfa..0000000
--- a/t/Client_2_0/34_bulk_cxn_errors.t
+++ /dev/null
@@ -1,36 +0,0 @@
-use Test::More;
-use Test::Deep;
-use Test::Exception;
-
-use strict;
-use warnings;
-use lib 't/lib';
-use Log::Any::Adapter;
-
-$ENV{ES_VERSION} = '2_0';
-$ENV{ES}           = '10.255.255.1:9200';
-$ENV{ES_SKIP_PING} = 1;
-$ENV{ES_CXN_POOL}  = 'Static';
-$ENV{ES_TIMEOUT}   = 1;
-
-my $es = do "es_sync.pl" or die( $@ || $! );
-SKIP: {
-    skip
-        "IO::Socket::IP doesn't respect timeout: https://rt.cpan.org/Ticket/Display.html?id=103878",
-        3
-        if $es->transport->cxn_pool->cxn_factory->cxn_class eq
-        'Search::Elasticsearch::Cxn::HTTPTiny'
-        && $^V =~ /^v5.2\d/;
-
-    # Check that the buffer is not cleared on a NoNodes exception
-
-    my $b = $es->bulk_helper( index => 'foo', type => 'bar' );
-    $b->create_docs( { foo => 'bar' } );
-
-    is $b->_buffer_count, 1, "Buffer count pre-flush";
-    throws_ok { $b->flush } 'Search::Elasticsearch::Error::NoNodes';
-    is $b->_buffer_count, 1, "Buffer count post-flush";
-
-}
-
-done_testing;
diff --git a/t/Client_2_0/50_reindex.t b/t/Client_2_0/50_reindex.t
deleted file mode 100644
index 4161609..0000000
--- a/t/Client_2_0/50_reindex.t
+++ /dev/null
@@ -1,150 +0,0 @@
-use Test::More;
-use Test::Deep;
-use Test::Exception;
-use lib 't/lib';
-
-use strict;
-use warnings;
-
-$ENV{ES_VERSION} = '2_0';
-our $es = do "es_sync.pl" or die( $@ || $! );
-
-$es->indices->delete( index => '_all', ignore => 404 );
-do "index_test_data.pl" or die( $@ || $! );
-
-my $b;
-
-# Reindex to new index and new type
-$b = $es->bulk_helper(
-    index => 'test2',
-    type  => 'test2'
-);
-$b->reindex( source => { index => 'test' } );
-$es->indices->refresh;
-
-is $es->count(
-    index => 'test2',
-    type  => 'test2'
-    )->{count}, 100,
-    'Reindexed to new index and type';
-
-# Reindex to same index
-$b = $es->bulk_helper();
-$b->reindex( source => { index => 'test' } );
-$es->indices->refresh;
-
-is $es->count(
-    index => 'test',
-    type  => 'test'
-    )->{count}, 100,
-    'Reindexed to same index';
-
-is $es->get( index => 'test', type => 'test', id => 1 )->{_version}, 2,
-    "Reindexed to same index - version updated";
-
-# Reindex from generic source
-
-my @docs = map {
-    { _index => 'foo', _type => 'bar', _id => $_, _source => { num => $_ } }
-} ( 1 .. 10 );
-
-$es->indices->delete( index => 'test2' );
-
-$b = $es->bulk_helper( index => 'test2' );
-$b->reindex( index => 'test2', source => sub { shift @docs } );
-$es->indices->refresh;
-
-is $es->count(
-    index => 'test2',
-    type  => 'bar'
-    )->{count}, 10,
-    'Reindexed from generic source';
-
-# Reindex with transform
-$es->indices->delete( index => 'test2' );
-
-$b = $es->bulk_helper( index => 'test2' );
-$b->reindex(
-    source    => { index => 'test' },
-    transform => sub {
-        my $doc = shift;
-        return if $doc->{_source}{color} eq 'red';
-        $doc->{_source}{transformed} = 1;
-        return $doc;
-    }
-);
-$es->indices->refresh;
-
-is $es->count(
-    index => 'test2',
-    type  => 'test'
-    )->{count}, 50,
-    'Transfrom - removed docs';
-
-my $query = {
-    query => {
-        bool => {
-            must => [
-                { term => { color       => 'green' } },
-                { term => { transformed => 1 } }
-            ]
-        }
-    }
-};
-
-is $es->count(
-    index => 'test2',
-    type  => 'test',
-    body  => $query,
-    )->{count}, 50,
-    'Transfrom - transformed docs';
-
-# Reindex with parent & routing
-$es->indices->delete( index => '_all', ignore => 404 );
-for ( 'test', 'test2' ) {
-    $es->indices->create(
-        index => $_,
-        body  => { mappings => { test => { _parent => { type => 'foo' } } } }
-    );
-}
-$es->cluster->health( wait_for_status => 'yellow' );
-
-for ( 1 .. 5 ) {
-    $es->index(
-        index        => 'test',
-        type         => 'test',
-        version_type => 'external',
-        version      => $_,
-        id           => $_,
-        parent       => 1,
-        routing      => 2,
-        body         => { count => $_ },
-    );
-}
-$es->indices->refresh;
-
-$b = $es->bulk_helper( index => 'test2' );
-ok $b->reindex(
-    version_type => 'external',
-    source       => {
-        index   => 'test',
-        version => 1,
-        fields  => [ '_parent', '_routing', '_source' ]
-    }
-    ),
-    "Advanced";
-
-$es->indices->refresh;
-my $results = $es->search(
-    index   => 'test2',
-    type    => 'test',
-    sort    => 'count',
-    fields  => [ '_parent', '_routing' ],
-    version => 1,
-)->{hits}{hits};
-
-is $results->[3]{_parent},  1, "Advanced - parent";
-is $results->[3]{_routing}, 2, "Advanced - routing";
-is $results->[3]{_version}, 4, "Advanced - version";
-
-done_testing;
diff --git a/t/Client_2_0/60_auth_httptiny.t b/t/Client_2_0/60_auth_httptiny.t
deleted file mode 100644
index 7a2c5e0..0000000
--- a/t/Client_2_0/60_auth_httptiny.t
+++ /dev/null
@@ -1,15 +0,0 @@
-use IO::Socket::SSL;
-use lib 't/lib';
-
-$ENV{ES_VERSION} = '2_0';
-$ENV{ES_CXN} = 'HTTPTiny';
-our $Throws_SSL = "SSL";
-
-sub ssl_options {
-    return {
-        SSL_verify_mode => SSL_VERIFY_PEER,
-        SSL_ca_file     => $_[0]
-    };
-}
-
-do "es_sync_auth.pl" or die( $@ || $! );
diff --git a/t/Client_2_0/61_auth_lwp.t b/t/Client_2_0/61_auth_lwp.t
deleted file mode 100644
index ff9f0e6..0000000
--- a/t/Client_2_0/61_auth_lwp.t
+++ /dev/null
@@ -1,14 +0,0 @@
-use lib 't/lib';
-
-$ENV{ES_VERSION} = '2_0';
-$ENV{ES_CXN} = 'LWP';
-our $Throws_SSL = "Cxn";
-
-sub ssl_options {
-    return {
-        verify_hostname => 1,
-        SSL_ca_file     => $_[0]
-    };
-}
-
-do "es_sync_auth.pl" or die( $@ || $! );
diff --git a/t/Client_7_0/00_print_version.t b/t/Client_7_0/00_print_version.t
new file mode 100644
index 0000000..e4f81d6
--- /dev/null
+++ b/t/Client_7_0/00_print_version.t
@@ -0,0 +1,40 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use lib 't/lib';
+$ENV{ES_VERSION} = '7_0';
+my $es = do "es_sync.pl" or die( $@ || $! );
+
+eval {
+    my $v = $es->info->{version};
+    diag "";
+    diag "";
+    diag "Testing against Elasticsearch v" . $v->{number};
+    for ( sort keys %$v ) {
+        diag sprintf "%-20s: %s", $_, $v->{$_};
+    }
+    diag "";
+    diag "Client:   " . ref($es);
+    diag "Cxn:      " . $es->transport->cxn_pool->cxn_factory->cxn_class;
+    diag "GET Body: " . $es->transport->send_get_body_as;
+    diag "";
+    pass "ES Version";
+} or fail "ES Version";
+
+done_testing;
+
diff --git a/t/Client_7_0/10_live.t b/t/Client_7_0/10_live.t
new file mode 100644
index 0000000..df5f5eb
--- /dev/null
+++ b/t/Client_7_0/10_live.t
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+my $es;
+$ENV{ES_VERSION} = '7_0';
+local $ENV{ES_CXN_POOL};
+
+$ENV{ES_CXN_POOL} = 'Static';
+$es = do "es_sync.pl" or die( $@ || $! );
+is $es->info->{tagline}, "You Know, for Search", 'CxnPool::Static';
+
+$ENV{ES_CXN_POOL} = 'Static::NoPing';
+$es = do "es_sync.pl" or die( $@ || $! );
+is $es->info->{tagline}, "You Know, for Search", 'CxnPool::Static::NoPing';
+
+unless ($ENV{ES} =~ /https/) {
+    $ENV{ES_CXN_POOL} = 'Sniff';
+    $es = do "es_sync.pl" or die( $@ || $! );
+    is $es->info->{tagline}, "You Know, for Search", 'CxnPool::Sniff';
+
+    my ($node) = values %{ $es->transport->cxn_pool->next_cxn->sniff };
+    ok $node->{http}{max_content_length_in_bytes}, 'Sniffs max_content length';
+}
+done_testing;
diff --git a/t/Client_7_0/20_fork_httptiny.t b/t/Client_7_0/20_fork_httptiny.t
new file mode 100644
index 0000000..69794d7
--- /dev/null
+++ b/t/Client_7_0/20_fork_httptiny.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'HTTPTiny';
+do "es_sync_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_7_0/21_fork_lwp.t b/t/Client_7_0/21_fork_lwp.t
new file mode 100644
index 0000000..1323f1e
--- /dev/null
+++ b/t/Client_7_0/21_fork_lwp.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'LWP';
+do "es_sync_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_7_0/23_fork_netcurl.t b/t/Client_7_0/23_fork_netcurl.t
new file mode 100644
index 0000000..b38b49f
--- /dev/null
+++ b/t/Client_7_0/23_fork_netcurl.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'NetCurl';
+do "es_sync_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_7_0/30_bulk_add_action.t b/t/Client_7_0/30_bulk_add_action.t
new file mode 100644
index 0000000..126de0a
--- /dev/null
+++ b/t/Client_7_0/30_bulk_add_action.t
@@ -0,0 +1,234 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+my $es = do "es_sync.pl" or die( $@ || $! );
+my $b = $es->bulk_helper;
+
+$b->_serializer->_set_canonical;
+
+## EMPTY
+
+ok $b->add_action(), 'Empty add action';
+
+## INDEX ACTIONS ##
+
+ok $b->add_action(
+    index => {
+        index        => 'foo',
+        id           => 1,
+        pipeline     => 'foo',
+        routing      => 1,
+        parent       => 1,
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    index => {
+        _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+
+    }
+    ),
+    'Add index actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"index":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"index":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Index actions in buffer";
+
+is $b->_buffer_size,  313, "Index actions buffer size";
+is $b->_buffer_count, 2,   "Index actions buffer count";
+
+$b->clear_buffer;
+
+## CREATE ACTIONS ##
+
+ok $b->add_action(
+    create => {
+        index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        pipeline     => 'foo',
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    create => {
+        _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+    }
+    ),
+    'Add create actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"create":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"create":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Create actions in buffer";
+
+is $b->_buffer_size,  315, "Create actions buffer size";
+is $b->_buffer_count, 2,   "Create actions buffer count";
+
+$b->clear_buffer;
+
+## DELETE ACTIONS ##
+
+ok $b->add_action(
+    delete => {
+        index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        version      => 1,
+        version_type => 'external',
+    },
+    delete => {
+        _index       => 'foo',
+        _id          => 2,
+        _routing     => 2,
+        _parent      => 2,
+        _version     => 1,
+        version_type => 'external',
+    }
+    ),
+    'Add delete actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1,"_index":"foo","parent":1,"routing":1,"version":1,"version_type":"external"}}),
+    q({"delete":{"_id":2,"_index":"foo","parent":2,"routing":2,"version":1,"version_type":"external"}}),
+    ],
+    "Delete actions in buffer";
+
+is $b->_buffer_size,  194, "Delete actions buffer size";
+is $b->_buffer_count, 2,   "Delete actions buffer count";
+
+$b->clear_buffer;
+
+## UPDATE ACTIONS ##
+
+ok $b->add_action(
+    update => {
+        index             => 'foo',
+        id                => 1,
+        routing           => 1,
+        parent            => 1,
+        timestamp         => 1380019061000,
+        ttl               => '10m',
+        version           => 1,
+        version_type      => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => ['foo'],
+        _source_excludes  => ['bar'],
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    },
+    update => {
+        _index            => 'foo',
+        _id               => 1,
+        _routing          => 1,
+        _parent           => 1,
+        _timestamp        => 1380019061000,
+        _ttl              => '10m',
+        _version          => 1,
+        _version_type     => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => ['foo'],
+        _source_excludes  => ['bar'],
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    }
+    ),
+    'Add update actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":["bar"],"_source_includes":["foo"],"detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"}),
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":["bar"],"_source_includes":["foo"],"detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"})
+    ],
+    "Update actions in buffer";
+
+is $b->_buffer_size,  710, "Update actions buffer size";
+is $b->_buffer_count, 2,   "Update actions buffer count";
+
+$b->clear_buffer;
+
+## ERRORS ##
+throws_ok { $b->add_action( 'foo' => {} ) } qr/Unrecognised action/,
+    'Bad action';
+
+throws_ok { $b->add_action( 'index', 'bar' ) } qr/Missing <params>/,
+    'Missing params';
+
+throws_ok { $b->add_action( index => { } ) }
+qr/Missing .*<index>/, 'Missing index';
+throws_ok { $b->add_action( index => { index => 'i' } ) }
+qr/Missing <source>/, 'Missing source';
+
+throws_ok {
+    $b->add_action(
+        index => { index => 'i', source => {}, foo => 1 } );
+}
+qr/Unknown params/, 'Unknown params';
+
+done_testing;
diff --git a/t/Client_7_0/31_bulk_helpers.t b/t/Client_7_0/31_bulk_helpers.t
new file mode 100644
index 0000000..44ef0d0
--- /dev/null
+++ b/t/Client_7_0/31_bulk_helpers.t
@@ -0,0 +1,243 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+my $es = do "es_sync.pl" or die( $@ || $! );
+my $b = $es->bulk_helper(
+    index => 'i'
+);
+my $s = $b->_serializer;
+$s->_set_canonical;
+
+## INDEX ##
+
+ok $b->index(), 'Empty index';
+
+ok $b->index(
+    {   index        => 'foo',
+        id           => 1,
+        pipeline     => 'foo',
+        routing      => 1,
+        parent       => 1,
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    {   _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+
+    }
+    ),
+    'Index';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"index":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"index":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Index in buffer";
+
+is $b->_buffer_size,  313, "Index buffer size";
+is $b->_buffer_count, 2,   "Index buffer count";
+
+$b->clear_buffer;
+
+## CREATE ##
+
+ok $b->create(), 'Create empty';
+
+ok $b->create(
+    {   index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        pipeline     => 'foo',
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    {   _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+    }
+    ),
+    'Create';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"create":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"create":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Create actions in buffer";
+
+is $b->_buffer_size,  315, "Create actions buffer size";
+is $b->_buffer_count, 2,   "Create actions buffer count";
+
+$b->clear_buffer;
+
+## CREATE DOCS##
+
+ok $b->create_docs(), 'Create_docs empty';
+
+ok $b->create_docs( { foo => 'bar' }, { foo => 'baz' } ), 'Create docs';
+
+cmp_deeply $b->_buffer,
+    [ q({"create":{}}), q({"foo":"bar"}), q({"create":{}}), q({"foo":"baz"}) ],
+    "Create docs in buffer";
+
+is $b->_buffer_size,  56, "Create docs buffer size";
+is $b->_buffer_count, 2,  "Create docs buffer count";
+
+$b->clear_buffer;
+
+## DELETE ##
+ok $b->delete(), 'Delete empty';
+
+ok $b->delete(
+    {   index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        version      => 1,
+        version_type => 'external',
+    },
+    {   _index       => 'foo',
+        _id          => 2,
+        _routing     => 2,
+        _parent      => 2,
+        _version     => 1,
+        version_type => 'external',
+    }
+    ),
+    'Delete';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1,"_index":"foo","parent":1,"routing":1,"version":1,"version_type":"external"}}),
+    q({"delete":{"_id":2,"_index":"foo","parent":2,"routing":2,"version":1,"version_type":"external"}}),
+    ],
+    "Delete actions in buffer";
+
+is $b->_buffer_size,  194, "Delete actions buffer size";
+is $b->_buffer_count, 2,   "Delete actions buffer count";
+
+$b->clear_buffer;
+
+## DELETE IDS ##
+ok $b->delete_ids(), 'Delete IDs empty';
+
+ok $b->delete_ids( 1, 2, 3 ), 'Delete IDs';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1}}), q({"delete":{"_id":2}}),
+    q({"delete":{"_id":3}}),
+    ],
+    "Delete IDs in buffer";
+
+is $b->_buffer_size,  63, "Delete IDs buffer size";
+is $b->_buffer_count, 3,  "Delete IDS buffer count";
+
+$b->clear_buffer;
+
+## UPDATE ACTIONS ##
+ok $b->update(), 'Update empty';
+ok $b->update(
+    {   index             => 'foo',
+        id                => 1,
+        routing           => 1,
+        parent            => 1,
+        timestamp         => 1380019061000,
+        ttl               => '10m',
+        version           => 1,
+        version_type      => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => ['foo'],
+        _source_excludes  => ['bar'],
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    },
+    {   _index            => 'foo',
+        _id               => 1,
+        _routing          => 1,
+        _parent           => 1,
+        _timestamp        => 1380019061000,
+        _ttl              => '10m',
+        _version          => 1,
+        _version_type     => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => ['foo'],
+        _source_excludes  => ['bar'],
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    }
+    ),
+    'Update';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":["bar"],"_source_includes":["foo"],"detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"}),
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":["bar"],"_source_includes":["foo"],"detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"})
+    ],
+    "Update actions in buffer";
+
+is $b->_buffer_size,  710, "Update actions buffer size";
+is $b->_buffer_count, 2,   "Update actions buffer count";
+
+$b->clear_buffer;
+
+done_testing;
diff --git a/t/Client_2_0/32_bulk_flush.t b/t/Client_7_0/32_bulk_flush.t
similarity index 71%
rename from t/Client_2_0/32_bulk_flush.t
rename to t/Client_7_0/32_bulk_flush.t
index 07cc133..143e35d 100644
--- a/t/Client_2_0/32_bulk_flush.t
+++ b/t/Client_7_0/32_bulk_flush.t
@@ -1,10 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 use Test::More;
 use Test::Deep;
 use strict;
 use warnings;
 use lib 't/lib';
 
-$ENV{ES_VERSION} = '2_0';
+$ENV{ES_VERSION} = '7_0';
 my $es = do "es_sync.pl" or die( $@ || $! );
 
 $es->indices->delete( index => '_all' );
@@ -68,8 +85,7 @@ sub test_flush {
     my $params = shift;
     my $b      = $es->bulk_helper(
         %$params,
-        index => 'test',
-        type  => 'test'
+        index => 'test'
     );
 
     my @seq = @_;
diff --git a/t/Client_2_0/33_bulk_errors.t b/t/Client_7_0/33_bulk_errors.t
similarity index 68%
rename from t/Client_2_0/33_bulk_errors.t
rename to t/Client_7_0/33_bulk_errors.t
index 2ee6248..ae50680 100644
--- a/t/Client_2_0/33_bulk_errors.t
+++ b/t/Client_7_0/33_bulk_errors.t
@@ -1,3 +1,20 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 use Test::More;
 use Test::Deep;
 use Test::Exception;
@@ -7,7 +24,7 @@ use warnings;
 use lib 't/lib';
 use Log::Any::Adapter;
 
-$ENV{ES_VERSION} = '2_0';
+$ENV{ES_VERSION} = '7_0';
 my $es = do "es_sync.pl" or die( $@ || $! );
 my $TRUE = $es->transport->serializer->decode('{"true":true}')->{true};
 
@@ -16,63 +33,26 @@ $es->indices->delete( index => '_all' );
 my @Std = (
     { id => 1, source => { count => 1 } },
     { id => 1, source => { count => 'foo' } },
-    { id => 1, version => 10, source => {} },
 );
 
 my ( $b, $success_count, $error_count, $custom_count, $conflict_count );
 
 ## Default error handling
-$b = bulk( { index => 'test', type => 'test' }, @Std );
-test_flush( "Default", 0, 2, 0, 0 );
+$b = bulk( { index => 'test' }, @Std );
+test_flush( "Default", 0, 1, 0, 0 );
 
 ## Custom error handling
 $b = bulk(
     {   index    => 'test',
-        type     => 'test',
         on_error => sub { $custom_count++ }
     },
     @Std
 );
-test_flush( "Custom error", 0, 0, 2, 0 );
-
-# Conflict errors
-$b = bulk(
-    {   index       => 'test',
-        type        => 'test',
-        on_conflict => sub { $conflict_count++ }
-    },
-    @Std
-);
-test_flush( "Conflict error", 0, 1, 0, 1 );
-
-# Both error handling
-$b = bulk(
-    {   index       => 'test',
-        type        => 'test',
-        on_conflict => sub { $conflict_count++ },
-        on_error    => sub { $custom_count++ }
-    },
-    @Std
-);
-
-test_flush( "Conflict and custom", 0, 0, 1, 1 );
-
-# Conflict disable error
-$b = bulk(
-    {   index       => 'test',
-        type        => 'test',
-        on_conflict => sub { $conflict_count++ },
-        on_error    => undef
-    },
-    @Std
-);
-
-test_flush( "Conflict, error undef", 0, 0, 0, 1 );
+test_flush( "Custom error", 0, 0, 1, 0 );
 
 # Disable both
 $b = bulk(
     {   index       => 'test',
-        type        => 'test',
         on_conflict => undef,
         on_error    => undef
     },
@@ -84,34 +64,33 @@ test_flush( "Both undef", 0, 0, 0, 0 );
 # Success
 $b = bulk(
     {   index      => 'test',
-        type       => 'test',
         on_success => sub { $success_count++ },
     },
     @Std
 );
 
-test_flush( "Success", 1, 2, 0, 0 );
+test_flush( "Success", 1, 1, 0, 0 );
 
 # cbs have correct params
 $b = bulk(
     {   index      => 'test',
-        type       => 'test',
         on_success => test_params(
             'on_success',
-            {   _index   => 'test',
-                _type    => 'test',
-                _id      => 1,
-                _version => 1,
-                status   => 201,
-                ok       => $TRUE,
-                _shards  => { successful => 1, total => 2, failed => 0 }
+            {   _index        => 'test',
+                _id           => 1,
+                _version      => 1,
+                status        => 201,
+                created       => $TRUE,
+                result        => 'created',
+                _shards       => { successful => 1, total => 2, failed => 0 },
+                _primary_term => 1,
+                _seq_no => 0
             },
             0
         ),
         on_error => test_params(
             'on_error',
             {   _index => 'test',
-                _type  => 'test',
                 _id    => 1,
                 error  => any(
                     re('MapperParsingException'),
@@ -124,7 +103,6 @@ $b = bulk(
         on_conflict => test_params(
             'on_conflict',
             {   _index => 'test',
-                _type  => 'test',
                 _id    => 1,
                 error  => any(
                     re('version conflict'),
@@ -181,7 +159,6 @@ sub test_params {
 
     return sub {
         is $_[0], 'index', "$type - action";
-        cmp_deeply $_[1], subhashof($result), "$type - result";
         is $_[2], $j,       "$type - array index";
         is $_[3], $version, "$type - version";
     };
diff --git a/t/Client_7_0/34_bulk_cxn_errors.t b/t/Client_7_0/34_bulk_cxn_errors.t
new file mode 100644
index 0000000..0f8d787
--- /dev/null
+++ b/t/Client_7_0/34_bulk_cxn_errors.t
@@ -0,0 +1,44 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Log::Any::Adapter;
+
+$ENV{ES_VERSION}   = '7_0';
+$ENV{ES}           = '10.255.255.1:9200';
+$ENV{ES_SKIP_PING} = 1;
+$ENV{ES_CXN_POOL}  = 'Static';
+$ENV{ES_TIMEOUT}   = 1;
+
+my $es = do "es_sync.pl" or die( $@ || $! );
+
+# Check that the buffer is not cleared on a NoNodes exception
+
+my $b = $es->bulk_helper( index => 'foo', type => 'bar' );
+$b->create_docs( { foo => 'bar' } );
+
+is $b->_buffer_count, 1, "Buffer count pre-flush";
+throws_ok { $b->flush } 'Search::Elasticsearch::Error::NoNodes';
+is $b->_buffer_count, 1, "Buffer count post-flush";
+
+done_testing;
diff --git a/t/Client_2_0/40_scroll.t b/t/Client_7_0/40_scroll.t
similarity index 80%
rename from t/Client_2_0/40_scroll.t
rename to t/Client_7_0/40_scroll.t
index e1a3cd0..e1b0099 100644
--- a/t/Client_2_0/40_scroll.t
+++ b/t/Client_7_0/40_scroll.t
@@ -1,3 +1,20 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 use Test::More;
 use Test::Deep;
 use Test::Exception;
@@ -6,7 +23,7 @@ use lib 't/lib';
 use strict;
 use warnings;
 
-$ENV{ES_VERSION} = '2_0';
+$ENV{ES_VERSION} = '7_0';
 our $es = do "es_sync.pl" or die( $@ || $! );
 
 $es->indices->delete( index => '_all', ignore => 404 );
@@ -24,7 +41,7 @@ test_scroll(
     ]
 );
 
-do "index_test_data.pl" or die( $@ || $! );
+do "index_test_data_7.pl" or die( $@ || $! );
 
 test_scroll(
     "Match all",
@@ -57,29 +74,7 @@ test_scroll(
         }
     },
     total     => 50,
-    max_score => num( 1.6, 0.5 ),
-    aggs      => bool(1),
-    suggest   => bool(1),
-    steps     => [
-        next        => [1],
-        next_50     => [49],
-        is_finished => 1,
-    ]
-);
-
-test_scroll(
-    "Scroll in qs",
-    {   scroll_in_qs => 1,
-        body         => {
-            query   => { term => { color => 'red' } },
-            suggest => {
-                mysuggest => { text => 'green', term => { field => 'color' } }
-            },
-            aggs => { switch => { terms => { field => 'switch' } } },
-        }
-    },
-    total     => 50,
-    max_score => num( 1.6, 0.5 ),
+    max_score => num( 1, 0.5 ),
     aggs      => bool(1),
     suggest   => bool(1),
     steps     => [
@@ -89,27 +84,6 @@ test_scroll(
     ]
 );
 
-test_scroll(
-    "Scan",
-    {   search_type => 'scan',
-        body        => {
-            suggest => {
-                mysuggest => { text => 'green', term => { field => 'color' } }
-            },
-        }
-    },
-    total     => 100,
-    max_score => 0,
-    suggest   => bool(1),
-    steps     => [
-        buffer_size => 0,
-        next        => [1],
-        buffer_size => 49,
-        next_100    => [99],
-        is_finished => 1,
-    ]
-);
-
 test_scroll(
     "Finish",
     {},
@@ -209,7 +183,6 @@ sub test_scroll {
         cmp_deeply $s->max_score,    $tests{max_score}, "$title - max_score";
         cmp_deeply $s->suggest,      $tests{suggest},   "$title - suggest";
         cmp_deeply $s->aggregations, $tests{aggs},      "$title - aggs";
-
         my $i     = 1;
         my @steps = @{ $tests{steps} };
         while ( my $name = shift @steps ) {
diff --git a/t/Client_7_0/60_auth_httptiny.t b/t/Client_7_0/60_auth_httptiny.t
new file mode 100644
index 0000000..3f3d94a
--- /dev/null
+++ b/t/Client_7_0/60_auth_httptiny.t
@@ -0,0 +1,32 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use IO::Socket::SSL;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'HTTPTiny';
+our $Throws_SSL = "SSL";
+
+sub ssl_options {
+    return {
+        SSL_verify_mode => SSL_VERIFY_PEER,
+        SSL_ca_file     => $_[0]
+    };
+}
+
+do "es_sync_auth.pl" or die( $@ || $! );
diff --git a/t/Client_7_0/61_auth_lwp.t b/t/Client_7_0/61_auth_lwp.t
new file mode 100644
index 0000000..faf4f29
--- /dev/null
+++ b/t/Client_7_0/61_auth_lwp.t
@@ -0,0 +1,31 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'LWP';
+our $Throws_SSL = "Cxn";
+
+sub ssl_options {
+    return {
+        verify_hostname => 1,
+        SSL_ca_file     => $_[0]
+    };
+}
+
+do "es_sync_auth.pl" or die( $@ || $! );
diff --git a/t/Client_7_0/62_auth_netcurl.t b/t/Client_7_0/62_auth_netcurl.t
new file mode 100644
index 0000000..c2ccf5a
--- /dev/null
+++ b/t/Client_7_0/62_auth_netcurl.t
@@ -0,0 +1,32 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'NetCurl';
+use Net::Curl::Easy qw(
+    CURLOPT_CAINFO
+);
+
+our $Throws_SSL = "SSL";
+
+sub ssl_options {
+    return { CURLOPT_CAINFO() => $_[0] };
+}
+
+do "es_sync_auth.pl" or die( $@ || $! );
diff --git a/t/Client_7_0_Async/00_print_version.t b/t/Client_7_0_Async/00_print_version.t
new file mode 100644
index 0000000..037b50f
--- /dev/null
+++ b/t/Client_7_0_Async/00_print_version.t
@@ -0,0 +1,40 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use lib 't/lib';
+$ENV{ES_VERSION} = '7_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+
+eval {
+    my $v = wait_for( $es->info )->{version};
+    diag "";
+    diag "";
+    diag "Testing against Search::Elasticsearch::Async v" . $v->{number};
+    for ( sort keys %$v ) {
+        diag sprintf "%-20s: %s", $_, $v->{$_};
+    }
+    diag "";
+    diag "Client:   " . ref($es);
+    diag "Cxn:      " . $es->transport->cxn_pool->cxn_factory->cxn_class;
+    diag "GET Body: " . $es->transport->send_get_body_as;
+    diag "";
+    pass "ES Version";
+} or fail "ES Version";
+
+done_testing;
+
diff --git a/t/Client_7_0_Async/10_live.t b/t/Client_7_0_Async/10_live.t
new file mode 100644
index 0000000..48c5bd4
--- /dev/null
+++ b/t/Client_7_0_Async/10_live.t
@@ -0,0 +1,58 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use AE;
+use strict;
+use warnings;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+
+my $es;
+local $ENV{ES_CXN_POOL};
+
+$ENV{ES_CXN_POOL} = 'Async::Static';
+$es = do "es_async.pl" or die( $@ || $! );
+
+is wait_for( $es->info )->{tagline}, "You Know, for Search",
+    'CxnPool::Async::Static';
+
+$ENV{ES_CXN_POOL} = 'Async::Static::NoPing';
+$es = do "es_async.pl" or die( $@ || $! );
+is wait_for( $es->info )->{tagline}, "You Know, for Search",
+    'CxnPool::Async::Static::NoPing';
+
+$ENV{ES_CXN_POOL} = 'Async::Sniff';
+$es = do "es_async.pl" or die( $@ || $! );
+is wait_for( $es->info )->{tagline}, "You Know, for Search",
+    'CxnPool::Async::Sniff';
+
+my ($node) = values %{
+    (   wait_for(
+            $es->transport->cxn_pool->next_cxn->then(
+                sub { shift()->sniff }
+            )
+        )
+    )[1]
+};
+ok $node->{http}{max_content_length_in_bytes}, 'Sniffs max_content length';
+
+done_testing;
+
diff --git a/t/Client_7_0_Async/20_fork_aehttp.t b/t/Client_7_0_Async/20_fork_aehttp.t
new file mode 100644
index 0000000..4f13749
--- /dev/null
+++ b/t/Client_7_0_Async/20_fork_aehttp.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'AEHTTP';
+do "es_async_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_7_0_Async/21_fork_mojo.t b/t/Client_7_0_Async/21_fork_mojo.t
new file mode 100644
index 0000000..0185b31
--- /dev/null
+++ b/t/Client_7_0_Async/21_fork_mojo.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'Mojo';
+do "es_async_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_7_0_Async/30_bulk_add_action.t b/t/Client_7_0_Async/30_bulk_add_action.t
new file mode 100644
index 0000000..6579b24
--- /dev/null
+++ b/t/Client_7_0_Async/30_bulk_add_action.t
@@ -0,0 +1,233 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+my ( $error, $name );
+
+$ENV{ES_VERSION} = '7_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+
+my $b = $es->bulk_helper(
+    on_fatal => sub {
+        like shift(), qr/$error/, $name;
+    }
+);
+
+$b->_serializer->_set_canonical;
+
+## EMPTY
+
+ok $b->add_action(), 'Empty add action';
+
+## INDEX ACTIONS ##
+
+ok $b->add_action(
+    index => {
+        index        => 'foo',
+        id           => 1,
+        pipeline     => 'foo',
+        routing      => 1,
+        parent       => 1,
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    index => {
+        _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+
+    }
+    ),
+    'Add index actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"index":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"index":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Index actions in buffer";
+
+is $b->_buffer_size,  313, "Index actions buffer size";
+is $b->_buffer_count, 2,   "Index actions buffer count";
+
+$b->clear_buffer;
+
+## CREATE ACTIONS ##
+
+ok $b->add_action(
+    create => {
+        index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        pipeline     => 'foo',
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    create => {
+        _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+    }
+    ),
+    'Add create actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"create":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"create":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Create actions in buffer";
+
+is $b->_buffer_size,  315, "Create actions buffer size";
+is $b->_buffer_count, 2,   "Create actions buffer count";
+
+$b->clear_buffer;
+
+## DELETE ACTIONS ##
+
+ok $b->add_action(
+    delete => {
+        index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        version      => 1,
+        version_type => 'external',
+    },
+    delete => {
+        _index       => 'foo',
+        _id          => 2,
+        _routing     => 2,
+        _parent      => 2,
+        _version     => 1,
+        version_type => 'external',
+    }
+    ),
+    'Add delete actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1,"_index":"foo","parent":1,"routing":1,"version":1,"version_type":"external"}}),
+    q({"delete":{"_id":2,"_index":"foo","parent":2,"routing":2,"version":1,"version_type":"external"}}),
+    ],
+    "Delete actions in buffer";
+
+is $b->_buffer_size,  194, "Delete actions buffer size";
+is $b->_buffer_count, 2,   "Delete actions buffer count";
+
+$b->clear_buffer;
+
+## UPDATE ACTIONS ##
+
+ok $b->add_action(
+    update => {
+        index             => 'foo',
+        id                => 1,
+        routing           => 1,
+        _source           => 'true',
+        _source_includes  => 'foo',
+        _source_excludes  => 'bar',
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    },
+    update => {
+        _index            => 'foo',
+        _id               => 1,
+        _routing          => 1,
+        _source           => 'true',
+        _source_includes  => 'foo',
+        _source_excludes  => 'bar',
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    }
+    ),
+    'Add update actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"update":{"_id":1,"_index":"foo","routing":1}}),
+    q({"_source":"true","_source_excludes":"bar","_source_includes":"foo","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"}),
+    q({"update":{"_id":1,"_index":"foo","routing":1}}),
+    q({"_source":"true","_source_excludes":"bar","_source_includes":"foo","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"})
+    ],
+    "Update actions in buffer";
+
+is $b->_buffer_size,  486, "Update actions buffer size";
+is $b->_buffer_count, 2,   "Update actions buffer count";
+
+$b->clear_buffer;
+
+## ERRORS ##
+$error = "Unrecognised action";
+$name  = 'Bad action';
+$b->add_action( 'foo' => {} );
+
+$error = "Missing <params>";
+$name  = 'Missing params';
+$b->add_action( 'index', 'bar' );
+
+$error = "Missing .*<index>";
+$name  = 'Missing index';
+$b->add_action( index => { type => 't' } );
+
+$error = "Missing .*<source>";
+$name  = 'Missing source';
+$b->add_action( index => { index => 'i', type => 't' } );
+
+$error = "Unknown params";
+$name  = 'Unknown params';
+$b->add_action(
+    index => { index => 'i', type => 't', source => {}, foo => 1 } );
+
+done_testing;
diff --git a/t/Client_7_0_Async/31_bulk_helpers.t b/t/Client_7_0_Async/31_bulk_helpers.t
new file mode 100644
index 0000000..5d1c5fe
--- /dev/null
+++ b/t/Client_7_0_Async/31_bulk_helpers.t
@@ -0,0 +1,245 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+
+my $b = $es->bulk_helper(
+    index => 'i',
+    type  => 't'
+);
+my $s = $b->_serializer;
+$s->_set_canonical;
+
+## INDEX ##
+
+ok $b->index(), 'Empty index';
+
+ok $b->index(
+    {   index        => 'foo',
+        id           => 1,
+        pipeline     => 'foo',
+        routing      => 1,
+        parent       => 1,
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    {   _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+
+    }
+    ),
+    'Index';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"index":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"index":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Index in buffer";
+
+is $b->_buffer_size,  313, "Index buffer size";
+is $b->_buffer_count, 2,   "Index buffer count";
+
+$b->clear_buffer;
+
+## CREATE ##
+
+ok $b->create(), 'Create empty';
+
+ok $b->create(
+    {   index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        pipeline     => 'foo',
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    {   _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+    }
+    ),
+    'Create';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"create":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"create":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Create actions in buffer";
+
+is $b->_buffer_size,  315, "Create actions buffer size";
+is $b->_buffer_count, 2,   "Create actions buffer count";
+
+$b->clear_buffer;
+
+## CREATE DOCS##
+
+ok $b->create_docs(), 'Create_docs empty';
+
+ok $b->create_docs( { foo => 'bar' }, { foo => 'baz' } ), 'Create docs';
+
+cmp_deeply $b->_buffer,
+    [ q({"create":{}}), q({"foo":"bar"}), q({"create":{}}), q({"foo":"baz"}) ],
+    "Create docs in buffer";
+
+is $b->_buffer_size,  56, "Create docs buffer size";
+is $b->_buffer_count, 2,  "Create docs buffer count";
+
+$b->clear_buffer;
+
+## DELETE ##
+ok $b->delete(), 'Delete empty';
+
+ok $b->delete(
+    {   index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        version      => 1,
+        version_type => 'external',
+    },
+    {   _index       => 'foo',
+        _id          => 2,
+        _routing     => 2,
+        _parent      => 2,
+        _version     => 1,
+        version_type => 'external',
+    }
+    ),
+    'Delete';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1,"_index":"foo","parent":1,"routing":1,"version":1,"version_type":"external"}}),
+    q({"delete":{"_id":2,"_index":"foo","parent":2,"routing":2,"version":1,"version_type":"external"}}),
+    ],
+    "Delete actions in buffer";
+
+is $b->_buffer_size,  194, "Delete actions buffer size";
+is $b->_buffer_count, 2,   "Delete actions buffer count";
+
+$b->clear_buffer;
+
+## DELETE IDS ##
+ok $b->delete_ids(), 'Delete IDs empty';
+
+ok $b->delete_ids( 1, 2, 3 ), 'Delete IDs';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1}}), q({"delete":{"_id":2}}),
+    q({"delete":{"_id":3}}),
+    ],
+    "Delete IDs in buffer";
+
+is $b->_buffer_size,  63, "Delete IDs buffer size";
+is $b->_buffer_count, 3,  "Delete IDS buffer count";
+
+$b->clear_buffer;
+
+## UPDATE ACTIONS ##
+ok $b->update(), 'Update empty';
+ok $b->update(
+    {   index             => 'foo',
+        id                => 1,
+        routing           => 1,
+        parent            => 1,
+        timestamp         => 1380019061000,
+        ttl               => '10m',
+        version           => 1,
+        version_type      => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => 'foo',
+        _source_excludes  => 'bar',
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    },
+    {   _index            => 'foo',
+        _id               => 1,
+        _routing          => 1,
+        _parent           => 1,
+        _timestamp        => 1380019061000,
+        _ttl              => '10m',
+        _version          => 1,
+        _version_type     => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => 'foo',
+        _source_excludes  => 'bar',
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    }
+    ),
+    'Update';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":"bar","_source_includes":"foo","detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"}),
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":"bar","_source_includes":"foo","detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"})
+    ],
+    "Update actions in buffer";
+
+is $b->_buffer_size,  702, "Update actions buffer size";
+is $b->_buffer_count, 2,   "Update actions buffer count";
+
+$b->clear_buffer;
+
+done_testing;
diff --git a/t/Client_7_0_Async/32_bulk_flush.t b/t/Client_7_0_Async/32_bulk_flush.t
new file mode 100644
index 0000000..77b29fa
--- /dev/null
+++ b/t/Client_7_0_Async/32_bulk_flush.t
@@ -0,0 +1,147 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use strict;
+use warnings;
+use lib 't/lib';
+use AE;
+use Promises qw(deferred);
+
+$ENV{ES_VERSION} = '7_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+
+wait_for( $es->indices->delete( index => '_all' ) );
+
+test_flush(
+    "max count",    #
+    { max_count => 3 },    #
+    1, 2, 0, 1, 2, 0, 1, 2, 0, 1
+);
+
+test_flush(
+    "max size",            #
+    { max_size => 95 },    #
+    1, 2, 3, 0, 1, 2, 3, 0, 1, 2
+);
+
+test_flush(
+    "max size > max_count",
+    { max_size => 95, max_count => 3 },
+    1, 2, 0, 1, 2, 0, 1, 2, 0, 1
+);
+
+test_flush(
+    "max size < max_count",
+    { max_size => 95, max_count => 5 },
+    1, 2, 3, 0, 1, 2, 3, 0, 1, 2
+);
+
+test_flush(
+    "max size = 0, max_count",
+    { max_size => 0, max_count => 5 },
+    1, 2, 3, 4, 0, 1, 2, 3, 4, 0
+);
+
+test_flush(
+    "max count = 0, max_size",
+    { max_size => 95, max_count => 0 },
+    1, 2, 3, 0, 1, 2, 3, 0, 1, 2
+);
+
+test_flush(
+    "max count = 0, max_size = 0",
+    { max_size => 0, max_count => 0 },
+    1, 2, 3, 4, 5, 6, 7, 8, 9, 10
+);
+
+test_flush(
+    "max_count = 5, max_time = 5",
+    { max_count => 5, max_time => 5 },
+    1, 2, 0, 1, 2, 3, 4, 0, 0, 1
+);
+
+done_testing;
+
+wait_for( $es->indices->delete( index => 'test' ) );
+
+#===================================
+sub test_flush {
+#===================================
+    my $title  = shift;
+    my $params = shift;
+    my $b      = $es->bulk_helper(
+        %$params,
+        index => 'test'
+    );
+
+    my @seq = @_;
+    my $cv  = AE::cv;
+
+    my $i = 10;
+    my $loop;
+
+    my $index_doc = sub {
+        $b->index( { id => $i, source => {} } );
+    };
+
+    my $check_buffer = sub {
+        is $b->_buffer_count, shift @seq, "$title - " . ( $i - 9 );
+        $i++;
+    };
+
+    my $d = deferred;
+    my $w;
+
+    $loop = sub {
+        if ( $i == 20 ) {
+            return $b->flush->then( sub { $d->resolve } );
+        }
+
+        # sleep on 12 or 18 if max_time specified
+        if ( $params->{max_time} && ( $i == 12 || $i == 18 ) ) {
+            $b->_last_flush( time - $params->{max_time} - 1 );
+        }
+        $index_doc->()->then($check_buffer)->then($loop);
+    };
+
+    $es->indices->delete( index => 'test', ignore => 404 )
+        ->then( sub { $es->indices->create( index => 'test' ) } )
+        ->then( sub { $es->cluster->health( wait_for_status => 'yellow' ) } )
+        ->then($loop);
+
+    $d->promise->then(
+        sub {
+            is $b->_buffer_count, 0, "$title - final flush";
+            $es->indices->refresh;
+        }
+        )->then(
+        sub {
+            $es->count;
+        }
+        )->then(
+        sub {
+            is shift()->{count}, 10, "$title - all docs indexed";
+        }
+        )->then(
+        sub {
+            $cv->send;
+        }
+        )->catch( sub { $cv->croak(@_) } );
+    $cv->recv;
+}
diff --git a/t/Client_7_0_Async/33_bulk_errors.t b/t/Client_7_0_Async/33_bulk_errors.t
new file mode 100644
index 0000000..d2cdb70
--- /dev/null
+++ b/t/Client_7_0_Async/33_bulk_errors.t
@@ -0,0 +1,212 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use AE;
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Log::Any::Adapter;
+
+$ENV{ES_VERSION} = '7_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+my $TRUE = $es->transport->serializer->decode('{"true":true}')->{true};
+
+my $cv = AE::cv;
+
+wait_for( $es->indices->delete( index => '_all' ) );
+
+my @Std = (
+    { id => 1, source => { count => 1 } },
+    { id => 1, source => { count => 'foo' } },
+    { id => 1, source => {} },
+);
+
+my ( $b, $error, $success_count, $error_count, $custom_count, $conflict_count );
+
+## Default error handling
+$b = bulk( { index => 'test'}, @Std );
+test_flush( "Default", 0, 1, 0, 0 );
+
+## Custom error handling
+$b = bulk(
+    {   index    => 'test',
+        on_error => sub { $custom_count++ }
+    },
+    @Std
+);
+test_flush( "Custom error", 0, 0, 1, 0 );
+
+# Conflict errors
+$b = bulk(
+    {   index       => 'test',
+        on_conflict => sub { $conflict_count++ }
+    },
+    @Std
+);
+test_flush( "Conflict error", 0, 1, 0, 0 );
+
+# Both error handling
+$b = bulk(
+    {   index       => 'test',
+        on_conflict => sub { $conflict_count++ },
+        on_error    => sub { $custom_count++ }
+    },
+    @Std
+);
+
+test_flush( "Conflict and custom", 0, 0, 1, 0 );
+
+# Conflict disable error
+$b = bulk(
+    {   index       => 'test',
+        on_conflict => sub { $conflict_count++ },
+        on_error    => undef
+    },
+    @Std
+);
+
+test_flush( "Conflict, error undef", 0, 0, 0, 0 );
+
+# Disable both
+$b = bulk(
+    {   index       => 'test',
+        on_conflict => undef,
+        on_error    => undef
+    },
+    @Std
+);
+
+test_flush( "Both undef", 0, 0, 0, 0 );
+
+# Success
+$b = bulk(
+    {   index      => 'test',
+        on_success => sub { $success_count++ },
+    },
+    @Std
+);
+
+test_flush( "Success", 2, 1, 0, 0 );
+
+# cbs have correct params
+$b = bulk(
+    {   index      => 'test',
+        on_success => test_params(
+            'on_success',
+            {   _index        => 'test',
+                _type         => '_doc',
+                _id           => 1,
+                _version      => 1,
+                created       => $TRUE,
+                _shards       => { successful => 1, total => 2, failed => 0 },
+                _primary_term => 1,
+                _seq_no       => 0,
+            },
+            0
+        ),
+        on_error => test_params(
+            'on_error',
+            {   _index => 'test',
+                _type  => '_doc',
+                _id    => 1,
+                error  => any(
+                    re('MapperParsingException'),
+                    superhashof( { type => 'mapper_parsing_exception' } )
+                ),
+                status => 400,
+            },
+            1
+        ),
+        on_conflict => test_params(
+            'on_conflict',
+            {   _index => 'test',
+                _type  => '_doc',
+                _id    => 1,
+                error  => any(
+                    re('version conflict'),
+                    superhashof(
+                        { type => 'version_conflict_engine_exception' }
+                    )
+                ),
+                status => 409,
+            },
+            2, 1
+        ),
+    },
+    @Std
+);
+wait_for( $b->flush );
+
+done_testing;
+
+wait_for( $es->indices->delete( index => '_all' ) );
+
+#===================================
+sub bulk {
+#===================================
+    my ( $params, @docs ) = @_;
+    my $b = $es->bulk_helper(
+        on_fatal => sub { $error = shift(); $error_count++ },
+        %$params,
+    );
+
+    $error = '';
+
+    wait_for(
+        $es->indices->delete( index => 'test', ignore => 404 )    #
+            ->then( sub { $es->indices->create( index => 'test' ) } )    #
+            ->then(
+            sub { $es->cluster->health( wait_for_status => 'yellow' ) }
+            )                                                            #
+            ->then( sub { $b->index(@docs) } )
+    );
+    return $b;
+}
+
+#===================================
+sub test_flush {
+#===================================
+    my ( $title, $success, $default, $custom, $conflict ) = @_;
+    $success_count = $custom_count = $error_count = $conflict_count = 0;
+    {
+        local $SIG{__WARN__} = sub { $error_count++ };
+        wait_for( $b->flush );
+    }
+    is $success_count,  $success,  "$title - success";
+    is $error_count,    $default,  "$title - default";
+    is $custom_count,   $custom,   "$title - custom";
+    is $conflict_count, $conflict, "$title - conflict";
+
+}
+
+#===================================
+sub test_params {
+#===================================
+    my ( $type, $result, $j, $version ) = @_;
+
+    return sub {
+        is $_[0], 'index', "$type - action";
+        cmp_deeply subhashof($result), $_[1], "$type - result";
+        is $_[2], $j,       "$type - array index";
+        is $_[3], $version, "$type - version";
+    };
+}
+
diff --git a/t/Client_7_0_Async/34_bulk_cxn_errors.t b/t/Client_7_0_Async/34_bulk_cxn_errors.t
new file mode 100644
index 0000000..5ba61fe
--- /dev/null
+++ b/t/Client_7_0_Async/34_bulk_cxn_errors.t
@@ -0,0 +1,53 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use AE;
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Log::Any::Adapter;
+
+$ENV{ES_VERSION}   = '7_0';
+$ENV{ES}           = '10.255.255.1:9200';
+$ENV{ES_SKIP_PING} = 1;
+$ENV{ES_CXN_POOL}  = 'Async::Static';
+
+my $es = do "es_async.pl" or die( $@ || $! );
+my $error;
+my $b = $es->bulk_helper( index => 'foo', type => 'bar' );
+$b->create_docs( { foo => 'bar' } );
+
+# Check that the buffer is not cleared on a NoNodes exception
+
+is $b->_buffer_count, 1, "Buffer count pre-flush";
+
+wait_for(
+    $b->flush->catch(
+        sub {
+            my $error = shift;
+            isa_ok $error, 'Search::Elasticsearch::Error::NoNodes';
+        }
+    )
+);
+
+is $b->_buffer_count, 1, "Buffer count post-flush";
+
+done_testing;
diff --git a/t/Client_7_0_Async/40_scroll.t b/t/Client_7_0_Async/40_scroll.t
new file mode 100644
index 0000000..ba99fe0
--- /dev/null
+++ b/t/Client_7_0_Async/40_scroll.t
@@ -0,0 +1,205 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use lib 't/lib';
+
+use strict;
+use warnings;
+
+our ( $s, $total_seen, $max_seen );
+$ENV{ES_VERSION} = '7_0';
+our $es = do "es_async.pl" or die( $@ || $! );
+
+wait_for( $es->indices->delete( index => '_all', ignore => 404 ) );
+
+test_scroll(
+    "No indices",
+    { on_results => \&on_results },
+    total      => 0,
+    max_score  => 0,
+    total_seen => 0,
+    max_seen   => 0,
+);
+
+do "index_test_data_7.pl" or die( $@ || $! );
+
+test_scroll(
+    "Match all - on_result",
+    { on_result => \&on_results, size => 10 },
+    total      => 100,
+    max_score  => 1,
+    total_seen => 100,
+    max_seen   => 1
+);
+
+test_scroll(
+    "Match all - on_results",
+    { on_results => \&on_results, size => 10 },
+    total      => 100,
+    max_score  => 1,
+    total_seen => 100,
+    max_seen   => 10
+);
+
+test_scroll(
+    "Query",
+    {   body => {
+            query   => { term => { color => 'red' } },
+            suggest => {
+                mysuggest => { text => 'green', term => { field => 'color' } }
+            },
+            aggs => { switch => { terms => { field => 'switch' } } },
+        },
+        size       => 10,
+        on_results => \&on_results
+    },
+    total      => 50,
+    max_score  => num( 1.0, 0.5 ),
+    aggs       => bool(1),
+    suggest    => bool(1),
+    total_seen => 50,
+    max_seen   => 10
+);
+
+test_scroll(
+    "Finish",
+    {   on_results => sub {
+            on_results(@_);
+            $s->finish if $total_seen == 30;
+        },
+        size => 10
+    },
+    total      => 100,
+    max_score  => 1,
+    total_seen => 30,
+    max_seen   => 10
+);
+
+{
+    # Test auto finish fork protection.
+    my $count = 0;
+    my $s = $es->scroll_helper( size => 5, on_result => sub { $count++ } );
+
+    my $pid = fork();
+    unless ( defined($pid) ) { die "Cannot fork. Lack of resources?"; }
+    unless ($pid) {
+
+        # Child. Call finish check that its not finished
+        # (the call to finish did nothing).
+        wait_for( $s->finish() );
+        exit 0;
+    }
+    else {
+        # Wait for children
+        waitpid( $pid, 0 );
+        is $?, 0, "Child exited without errors";
+    }
+    ok !$s->is_finished(), "Our Scroll is not finished";
+    wait_for( $s->start );
+    is $count, 100, "All documents retrieved";
+    ok $s->is_finished, "Our scroll is finished";
+}
+
+# {
+#     # Test Scroll usage attempt in a different process.
+#     my $count = 0;
+#     my $s     = $es->scroll_helper(
+#         size      => 5,
+#         on_result => sub { $count++ },
+#         on_error  => sub { die @_ }
+#     );
+
+#     my $pid = fork();
+#     unless ( defined($pid) ) { die "Cannot fork. Lack of resources?"; }
+#     unless ($pid) {
+
+#         eval { wait_for( $s->start ) };
+#         my $err = $@;
+#         exit( eval { $err->is('Illegal') && 123 } || 999 );
+#     }
+#     else {
+#         # Wait for children
+#         waitpid( $pid, 0 );
+#         is $? >> 8, 123, "Child threw Illegal exception";
+#     }
+# }
+
+# {
+#     # Test valid Scroll usage after initial fork
+#     my $pid = fork();
+#     unless ( defined($pid) ) { die "Cannot fork. Lack of resources?"; }
+#     unless ($pid) {
+
+#         my $count = 0;
+#         my $s     = $es->scroll_helper(
+#             size      => 5,
+#             on_result => sub { $count++ },
+#             on_error  => sub { die @_ }
+#         );
+
+#         wait_for( $s->start );
+#         exit 0;
+#     }
+#     else {
+#         # Wait for children
+#         waitpid( $pid, 0 );
+#         is $? , 0, "Scroll completed successfully";
+#     }
+# }
+
+done_testing;
+
+wait_for( $es->indices->delete( index => 'test' ) );
+
+#===================================
+sub test_scroll {
+#===================================
+    my ( $title, $params, %tests ) = @_;
+    $max_seen = $total_seen = 0;
+    subtest $title => sub {
+        $s = $es->scroll_helper(
+            on_start => sub { test_start( $title, \%tests, @_ ) },
+            %$params
+        );
+        wait_for( $s->start );
+
+        is $total_seen, $tests{total_seen}, "$title - total seen";
+        is $max_seen,   $tests{max_seen},   "$title - max seen";
+
+    };
+}
+
+#===================================
+sub test_start {
+#===================================
+    my ( $title, $tests, $s ) = @_;
+    is $s->total,                $tests->{total},     "$title - total";
+    cmp_deeply $s->max_score,    $tests->{max_score}, "$title - max_score";
+    cmp_deeply $s->suggest,      $tests->{suggest},   "$title - suggest";
+    cmp_deeply $s->aggregations, $tests->{aggs},      "$title - aggs";
+
+}
+
+#===================================
+sub on_results {
+#===================================
+    $max_seen = @_ if @_ > $max_seen;
+    $total_seen += @_;
+}
diff --git a/t/Client_7_0_Async/60_auth_aehttp.t b/t/Client_7_0_Async/60_auth_aehttp.t
new file mode 100644
index 0000000..6323079
--- /dev/null
+++ b/t/Client_7_0_Async/60_auth_aehttp.t
@@ -0,0 +1,31 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'AEHTTP';
+
+sub ssl_options {
+    return {
+        verify          => 1,
+        verify_peername => 'https',
+        ca_file         => $_[0]
+    };
+}
+
+do "es_async_auth.pl" or die( $@ || $! );
diff --git a/t/Client_7_0_Async/61_auth_mojo.t b/t/Client_7_0_Async/61_auth_mojo.t
new file mode 100644
index 0000000..4d2cdd9
--- /dev/null
+++ b/t/Client_7_0_Async/61_auth_mojo.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '7_0';
+$ENV{ES_CXN} = 'Mojo';
+
+sub ssl_options {
+    return { ca => $_[0] };
+}
+
+do "es_async_auth.pl" or die( $@ || $! );
diff --git a/t/Client_8_0/00_print_version.t b/t/Client_8_0/00_print_version.t
new file mode 100644
index 0000000..d0b7bd2
--- /dev/null
+++ b/t/Client_8_0/00_print_version.t
@@ -0,0 +1,40 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use lib 't/lib';
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_sync.pl" or die( $@ || $! );
+
+eval {
+    my $v = $es->info->{version};
+    diag "";
+    diag "";
+    diag "Testing against Elasticsearch v" . $v->{number};
+    for ( sort keys %$v ) {
+        diag sprintf "%-20s: %s", $_, $v->{$_};
+    }
+    diag "";
+    diag "Client:   " . ref($es);
+    diag "Cxn:      " . $es->transport->cxn_pool->cxn_factory->cxn_class;
+    diag "GET Body: " . $es->transport->send_get_body_as;
+    diag "";
+    pass "ES Version";
+} or fail "ES Version";
+
+done_testing;
+
diff --git a/t/Client_8_0/10_live.t b/t/Client_8_0/10_live.t
new file mode 100644
index 0000000..54abc57
--- /dev/null
+++ b/t/Client_8_0/10_live.t
@@ -0,0 +1,45 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+my $es;
+$ENV{ES_VERSION} = '8_0';
+local $ENV{ES_CXN_POOL};
+
+$ENV{ES_CXN_POOL} = 'Static';
+$es = do "es_sync.pl" or die( $@ || $! );
+is $es->info->{tagline}, "You Know, for Search", 'CxnPool::Static';
+
+$ENV{ES_CXN_POOL} = 'Static::NoPing';
+$es = do "es_sync.pl" or die( $@ || $! );
+is $es->info->{tagline}, "You Know, for Search", 'CxnPool::Static::NoPing';
+
+unless ($ENV{ES} =~ /https/) {
+    $ENV{ES_CXN_POOL} = 'Sniff';
+    $es = do "es_sync.pl" or die( $@ || $! );
+    is $es->info->{tagline}, "You Know, for Search", 'CxnPool::Sniff';
+
+    my ($node) = values %{ $es->transport->cxn_pool->next_cxn->sniff };
+    ok $node->{http}{max_content_length_in_bytes}, 'Sniffs max_content length';
+}
+done_testing;
diff --git a/t/Client_8_0/20_fork_httptiny.t b/t/Client_8_0/20_fork_httptiny.t
new file mode 100644
index 0000000..ccdf931
--- /dev/null
+++ b/t/Client_8_0/20_fork_httptiny.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'HTTPTiny';
+do "es_sync_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_8_0/21_fork_lwp.t b/t/Client_8_0/21_fork_lwp.t
new file mode 100644
index 0000000..a362ff9
--- /dev/null
+++ b/t/Client_8_0/21_fork_lwp.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'LWP';
+do "es_sync_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_8_0/23_fork_netcurl.t b/t/Client_8_0/23_fork_netcurl.t
new file mode 100644
index 0000000..23182ff
--- /dev/null
+++ b/t/Client_8_0/23_fork_netcurl.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'NetCurl';
+do "es_sync_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_8_0/30_bulk_add_action.t b/t/Client_8_0/30_bulk_add_action.t
new file mode 100644
index 0000000..6a8015b
--- /dev/null
+++ b/t/Client_8_0/30_bulk_add_action.t
@@ -0,0 +1,234 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_sync.pl" or die( $@ || $! );
+my $b = $es->bulk_helper;
+
+$b->_serializer->_set_canonical;
+
+## EMPTY
+
+ok $b->add_action(), 'Empty add action';
+
+## INDEX ACTIONS ##
+
+ok $b->add_action(
+    index => {
+        index        => 'foo',
+        id           => 1,
+        pipeline     => 'foo',
+        routing      => 1,
+        parent       => 1,
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    index => {
+        _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+
+    }
+    ),
+    'Add index actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"index":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"index":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Index actions in buffer";
+
+is $b->_buffer_size,  313, "Index actions buffer size";
+is $b->_buffer_count, 2,   "Index actions buffer count";
+
+$b->clear_buffer;
+
+## CREATE ACTIONS ##
+
+ok $b->add_action(
+    create => {
+        index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        pipeline     => 'foo',
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    create => {
+        _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+    }
+    ),
+    'Add create actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"create":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"create":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Create actions in buffer";
+
+is $b->_buffer_size,  315, "Create actions buffer size";
+is $b->_buffer_count, 2,   "Create actions buffer count";
+
+$b->clear_buffer;
+
+## DELETE ACTIONS ##
+
+ok $b->add_action(
+    delete => {
+        index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        version      => 1,
+        version_type => 'external',
+    },
+    delete => {
+        _index       => 'foo',
+        _id          => 2,
+        _routing     => 2,
+        _parent      => 2,
+        _version     => 1,
+        version_type => 'external',
+    }
+    ),
+    'Add delete actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1,"_index":"foo","parent":1,"routing":1,"version":1,"version_type":"external"}}),
+    q({"delete":{"_id":2,"_index":"foo","parent":2,"routing":2,"version":1,"version_type":"external"}}),
+    ],
+    "Delete actions in buffer";
+
+is $b->_buffer_size,  194, "Delete actions buffer size";
+is $b->_buffer_count, 2,   "Delete actions buffer count";
+
+$b->clear_buffer;
+
+## UPDATE ACTIONS ##
+
+ok $b->add_action(
+    update => {
+        index             => 'foo',
+        id                => 1,
+        routing           => 1,
+        parent            => 1,
+        timestamp         => 1380019061000,
+        ttl               => '10m',
+        version           => 1,
+        version_type      => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => ['foo'],
+        _source_excludes  => ['bar'],
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    },
+    update => {
+        _index            => 'foo',
+        _id               => 1,
+        _routing          => 1,
+        _parent           => 1,
+        _timestamp        => 1380019061000,
+        _ttl              => '10m',
+        _version          => 1,
+        _version_type     => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => ['foo'],
+        _source_excludes  => ['bar'],
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    }
+    ),
+    'Add update actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":["bar"],"_source_includes":["foo"],"detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"}),
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":["bar"],"_source_includes":["foo"],"detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"})
+    ],
+    "Update actions in buffer";
+
+is $b->_buffer_size,  710, "Update actions buffer size";
+is $b->_buffer_count, 2,   "Update actions buffer count";
+
+$b->clear_buffer;
+
+## ERRORS ##
+throws_ok { $b->add_action( 'foo' => {} ) } qr/Unrecognised action/,
+    'Bad action';
+
+throws_ok { $b->add_action( 'index', 'bar' ) } qr/Missing <params>/,
+    'Missing params';
+
+throws_ok { $b->add_action( index => { } ) }
+qr/Missing .*<index>/, 'Missing index';
+throws_ok { $b->add_action( index => { index => 'i' } ) }
+qr/Missing <source>/, 'Missing source';
+
+throws_ok {
+    $b->add_action(
+        index => { index => 'i', source => {}, foo => 1 } );
+}
+qr/Unknown params/, 'Unknown params';
+
+done_testing;
diff --git a/t/Client_8_0/31_bulk_helpers.t b/t/Client_8_0/31_bulk_helpers.t
new file mode 100644
index 0000000..5574cf9
--- /dev/null
+++ b/t/Client_8_0/31_bulk_helpers.t
@@ -0,0 +1,243 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_sync.pl" or die( $@ || $! );
+my $b = $es->bulk_helper(
+    index => 'i'
+);
+my $s = $b->_serializer;
+$s->_set_canonical;
+
+## INDEX ##
+
+ok $b->index(), 'Empty index';
+
+ok $b->index(
+    {   index        => 'foo',
+        id           => 1,
+        pipeline     => 'foo',
+        routing      => 1,
+        parent       => 1,
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    {   _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+
+    }
+    ),
+    'Index';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"index":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"index":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Index in buffer";
+
+is $b->_buffer_size,  313, "Index buffer size";
+is $b->_buffer_count, 2,   "Index buffer count";
+
+$b->clear_buffer;
+
+## CREATE ##
+
+ok $b->create(), 'Create empty';
+
+ok $b->create(
+    {   index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        pipeline     => 'foo',
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    {   _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+    }
+    ),
+    'Create';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"create":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"create":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Create actions in buffer";
+
+is $b->_buffer_size,  315, "Create actions buffer size";
+is $b->_buffer_count, 2,   "Create actions buffer count";
+
+$b->clear_buffer;
+
+## CREATE DOCS##
+
+ok $b->create_docs(), 'Create_docs empty';
+
+ok $b->create_docs( { foo => 'bar' }, { foo => 'baz' } ), 'Create docs';
+
+cmp_deeply $b->_buffer,
+    [ q({"create":{}}), q({"foo":"bar"}), q({"create":{}}), q({"foo":"baz"}) ],
+    "Create docs in buffer";
+
+is $b->_buffer_size,  56, "Create docs buffer size";
+is $b->_buffer_count, 2,  "Create docs buffer count";
+
+$b->clear_buffer;
+
+## DELETE ##
+ok $b->delete(), 'Delete empty';
+
+ok $b->delete(
+    {   index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        version      => 1,
+        version_type => 'external',
+    },
+    {   _index       => 'foo',
+        _id          => 2,
+        _routing     => 2,
+        _parent      => 2,
+        _version     => 1,
+        version_type => 'external',
+    }
+    ),
+    'Delete';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1,"_index":"foo","parent":1,"routing":1,"version":1,"version_type":"external"}}),
+    q({"delete":{"_id":2,"_index":"foo","parent":2,"routing":2,"version":1,"version_type":"external"}}),
+    ],
+    "Delete actions in buffer";
+
+is $b->_buffer_size,  194, "Delete actions buffer size";
+is $b->_buffer_count, 2,   "Delete actions buffer count";
+
+$b->clear_buffer;
+
+## DELETE IDS ##
+ok $b->delete_ids(), 'Delete IDs empty';
+
+ok $b->delete_ids( 1, 2, 3 ), 'Delete IDs';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1}}), q({"delete":{"_id":2}}),
+    q({"delete":{"_id":3}}),
+    ],
+    "Delete IDs in buffer";
+
+is $b->_buffer_size,  63, "Delete IDs buffer size";
+is $b->_buffer_count, 3,  "Delete IDS buffer count";
+
+$b->clear_buffer;
+
+## UPDATE ACTIONS ##
+ok $b->update(), 'Update empty';
+ok $b->update(
+    {   index             => 'foo',
+        id                => 1,
+        routing           => 1,
+        parent            => 1,
+        timestamp         => 1380019061000,
+        ttl               => '10m',
+        version           => 1,
+        version_type      => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => ['foo'],
+        _source_excludes  => ['bar'],
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    },
+    {   _index            => 'foo',
+        _id               => 1,
+        _routing          => 1,
+        _parent           => 1,
+        _timestamp        => 1380019061000,
+        _ttl              => '10m',
+        _version          => 1,
+        _version_type     => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => ['foo'],
+        _source_excludes  => ['bar'],
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    }
+    ),
+    'Update';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":["bar"],"_source_includes":["foo"],"detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"}),
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":["bar"],"_source_includes":["foo"],"detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"})
+    ],
+    "Update actions in buffer";
+
+is $b->_buffer_size,  710, "Update actions buffer size";
+is $b->_buffer_count, 2,   "Update actions buffer count";
+
+$b->clear_buffer;
+
+done_testing;
diff --git a/t/Client_8_0/32_bulk_flush.t b/t/Client_8_0/32_bulk_flush.t
new file mode 100644
index 0000000..5a8ed9f
--- /dev/null
+++ b/t/Client_8_0/32_bulk_flush.t
@@ -0,0 +1,111 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use strict;
+use warnings;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_sync.pl" or die( $@ || $! );
+
+$es->indices->delete( index => '_all' );
+
+test_flush(
+    "max count",    #
+    { max_count => 3 },    #
+    1, 2, 0, 1, 2, 0, 1, 2, 0, 1
+);
+
+test_flush(
+    "max size",            #
+    { max_size => 95 },    #
+    1, 2, 3, 0, 1, 2, 3, 0, 1, 2
+);
+
+test_flush(
+    "max size > max_count",
+    { max_size => 95, max_count => 3 },
+    1, 2, 0, 1, 2, 0, 1, 2, 0, 1
+);
+
+test_flush(
+    "max size < max_count",
+    { max_size => 95, max_count => 5 },
+    1, 2, 3, 0, 1, 2, 3, 0, 1, 2
+);
+
+test_flush(
+    "max size = 0, max_count",
+    { max_size => 0, max_count => 5 },
+    1, 2, 3, 4, 0, 1, 2, 3, 4, 0
+);
+
+test_flush(
+    "max count = 0, max_size",
+    { max_size => 95, max_count => 0 },
+    1, 2, 3, 0, 1, 2, 3, 0, 1, 2
+);
+
+test_flush(
+    "max count = 0, max_size = 0",
+    { max_size => 0, max_count => 0 },
+    1, 2, 3, 4, 5, 6, 7, 8, 9, 10
+);
+
+test_flush(
+    "max_count = 5, max_time = 5",
+    { max_count => 5, max_time => 5 },
+    1, 2, 0, 1, 2, 3, 4, 0, 0, 1
+);
+
+done_testing;
+
+$es->indices->delete( index => 'test' );
+
+#===================================
+sub test_flush {
+#===================================
+    my $title  = shift;
+    my $params = shift;
+    my $b      = $es->bulk_helper(
+        %$params,
+        index => 'test'
+    );
+
+    my @seq = @_;
+
+    $es->indices->delete( index => 'test', ignore => 404 );
+    $es->indices->create( index => 'test' );
+    $es->cluster->health( wait_for_status => 'yellow' );
+
+    for my $i ( 10 .. 19 ) {
+
+        # sleep on 12 or 18 if max_time specified
+        if ( $params->{max_time} && ( $i == 12 || $i == 18 ) ) {
+            $b->_last_flush( time - $params->{max_time} - 1 );
+        }
+        $b->index( { id => $i, source => {} } );
+        is $b->_buffer_count, shift @seq, "$title - " . ( $i - 9 );
+    }
+    $b->flush;
+    is $b->_buffer_count, 0, "$title - final flush";
+    $es->indices->refresh;
+    is $es->count->{count}, 10, "$title - all docs indexed";
+
+}
diff --git a/t/Client_8_0/33_bulk_errors.t b/t/Client_8_0/33_bulk_errors.t
new file mode 100644
index 0000000..36fdbc7
--- /dev/null
+++ b/t/Client_8_0/33_bulk_errors.t
@@ -0,0 +1,165 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Log::Any::Adapter;
+
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_sync.pl" or die( $@ || $! );
+my $TRUE = $es->transport->serializer->decode('{"true":true}')->{true};
+
+$es->indices->delete( index => '_all' );
+
+my @Std = (
+    { id => 1, source => { count => 1 } },
+    { id => 1, source => { count => 'foo' } },
+);
+
+my ( $b, $success_count, $error_count, $custom_count, $conflict_count );
+
+## Default error handling
+$b = bulk( { index => 'test' }, @Std );
+test_flush( "Default", 0, 1, 0, 0 );
+
+## Custom error handling
+$b = bulk(
+    {   index    => 'test',
+        on_error => sub { $custom_count++ }
+    },
+    @Std
+);
+test_flush( "Custom error", 0, 0, 1, 0 );
+
+# Disable both
+$b = bulk(
+    {   index       => 'test',
+        on_conflict => undef,
+        on_error    => undef
+    },
+    @Std
+);
+
+test_flush( "Both undef", 0, 0, 0, 0 );
+
+# Success
+$b = bulk(
+    {   index      => 'test',
+        on_success => sub { $success_count++ },
+    },
+    @Std
+);
+
+test_flush( "Success", 1, 1, 0, 0 );
+
+# cbs have correct params
+$b = bulk(
+    {   index      => 'test',
+        on_success => test_params(
+            'on_success',
+            {   _index        => 'test',
+                _id           => 1,
+                _version      => 1,
+                status        => 201,
+                created       => $TRUE,
+                result        => 'created',
+                _shards       => { successful => 1, total => 2, failed => 0 },
+                _primary_term => 1,
+                _seq_no => 0
+            },
+            0
+        ),
+        on_error => test_params(
+            'on_error',
+            {   _index => 'test',
+                _id    => 1,
+                error  => any(
+                    re('MapperParsingException'),
+                    superhashof( { type => 'mapper_parsing_exception' } )
+                ),
+                status => 400,
+            },
+            1
+        ),
+        on_conflict => test_params(
+            'on_conflict',
+            {   _index => 'test',
+                _id    => 1,
+                error  => any(
+                    re('version conflict'),
+                    superhashof(
+                        { type => 'version_conflict_engine_exception' }
+                    )
+                ),
+                status => 409,
+            },
+            2,
+            1
+        ),
+    },
+    @Std
+);
+$b->flush;
+
+done_testing;
+
+$es->indices->delete( index => 'test' );
+
+#===================================
+sub bulk {
+#===================================
+    my $params = shift;
+    my $b      = $es->bulk_helper($params);
+    $es->indices->delete( index => 'test', ignore => 404 );
+    $es->indices->create( index => 'test' );
+    $es->cluster->health( wait_for_status => 'yellow' );
+    $b->index(@_);
+    return $b;
+}
+
+#===================================
+sub test_flush {
+#===================================
+    my ( $title, $success, $default, $custom, $conflict ) = @_;
+    $success_count = $custom_count = $error_count = $conflict_count = 0;
+    {
+        local $SIG{__WARN__} = sub { $error_count++ };
+        $b->flush;
+    }
+    is $success_count,  $success,  "$title - success";
+    is $error_count,    $default,  "$title - default";
+    is $custom_count,   $custom,   "$title - custom";
+    is $conflict_count, $conflict, "$title - conflict";
+
+}
+
+#===================================
+sub test_params {
+#===================================
+    my ( $type, $result, $j, $version ) = @_;
+
+    return sub {
+        is $_[0], 'index', "$type - action";
+        is $_[2], $j,       "$type - array index";
+        is $_[3], $version, "$type - version";
+    };
+}
diff --git a/t/Client_8_0/34_bulk_cxn_errors.t b/t/Client_8_0/34_bulk_cxn_errors.t
new file mode 100644
index 0000000..5f61fed
--- /dev/null
+++ b/t/Client_8_0/34_bulk_cxn_errors.t
@@ -0,0 +1,44 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Log::Any::Adapter;
+
+$ENV{ES_VERSION}   = '8_0';
+$ENV{ES}           = '10.255.255.1:9200';
+$ENV{ES_SKIP_PING} = 1;
+$ENV{ES_CXN_POOL}  = 'Static';
+$ENV{ES_TIMEOUT}   = 1;
+
+my $es = do "es_sync.pl" or die( $@ || $! );
+
+# Check that the buffer is not cleared on a NoNodes exception
+
+my $b = $es->bulk_helper( index => 'foo', type => 'bar' );
+$b->create_docs( { foo => 'bar' } );
+
+is $b->_buffer_count, 1, "Buffer count pre-flush";
+throws_ok { $b->flush } 'Search::Elasticsearch::Error::NoNodes';
+is $b->_buffer_count, 1, "Buffer count post-flush";
+
+done_testing;
diff --git a/t/Client_8_0/40_scroll.t b/t/Client_8_0/40_scroll.t
new file mode 100644
index 0000000..4ffda93
--- /dev/null
+++ b/t/Client_8_0/40_scroll.t
@@ -0,0 +1,213 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use lib 't/lib';
+
+use strict;
+use warnings;
+
+$ENV{ES_VERSION} = '8_0';
+our $es = do "es_sync.pl" or die( $@ || $! );
+
+$es->indices->delete( index => '_all', ignore => 404 );
+
+test_scroll(
+    "No indices",
+    {},
+    total     => 0,
+    max_score => 0,
+    steps     => [
+        is_finished   => 1,
+        next          => [0],
+        refill_buffer => 0,
+        drain_buffer  => [0],
+    ]
+);
+
+do "index_test_data_7.pl" or die( $@ || $! );
+
+test_scroll(
+    "Match all",
+    {},
+    total     => 100,
+    max_score => 1,
+    steps     => [
+        is_finished   => '',
+        buffer_size   => 10,
+        next          => [1],
+        drain_buffer  => [9],
+        refill_buffer => 10,
+        refill_buffer => 20,
+        is_finished   => '',
+        next_81       => [81],
+        next_20       => [9],
+        next          => [0],
+        is_finished   => 1,
+    ]
+);
+
+test_scroll(
+    "Query",
+    {   body => {
+            query   => { term => { color => 'red' } },
+            suggest => {
+                mysuggest => { text => 'green', term => { field => 'color' } }
+            },
+            aggs => { switch => { terms => { field => 'switch' } } },
+        }
+    },
+    total     => 50,
+    max_score => num( 1, 0.5 ),
+    aggs      => bool(1),
+    suggest   => bool(1),
+    steps     => [
+        next        => [1],
+        next_50     => [49],
+        is_finished => 1,
+    ]
+);
+
+test_scroll(
+    "Finish",
+    {},
+    total     => 100,
+    max_score => 1,
+    steps     => [
+        is_finished => '',
+        next        => [1],
+        finish      => 1,
+        is_finished => 1,
+        buffer_size => 0,
+        next        => [0]
+
+    ]
+);
+
+my $s = $es->scroll_helper;
+my $d = $s->next;
+ok ref $d && $d->{_source}, 'next() in scalar context';
+
+{
+    # Test auto finish fork protection.
+    my $s = $es->scroll_helper( size => 5 );
+
+    my $pid = fork();
+    unless ( defined($pid) ) { die "Cannot fork. Lack of resources?"; }
+    unless ($pid) {
+
+        # Child. Call finish check that its not finished
+        # (the call to finish did nothing).
+        $s->finish();
+        exit;
+    }
+    else {
+        # Wait for children
+        waitpid( $pid, 0 );
+        is $?, 0, "Child exited without errors";
+    }
+    ok !$s->is_finished(), "Our Scroll is not finished";
+    my $count = 0;
+    while ( $s->next ) { $count++ }
+    is $count, 100, "All documents retrieved";
+    ok $s->is_finished, "Our scroll is finished";
+}
+
+{
+    # Test Scroll usage attempt in a different process.
+    my $s = $es->scroll_helper( size => 5 );
+    my $pid = fork();
+    unless ( defined($pid) ) { die "Cannot fork. Lack of resources?"; }
+    unless ($pid) {
+
+        # Calling this next should crash, not exiting this process with 0
+        eval {
+            while ( $s->next ) { }
+        };
+        my $err = $@;
+        exit( eval { $err->is('Illegal') && 123 } || 999 );
+    }
+    else {
+        # Wait for children
+        waitpid( $pid, 0 );
+        is $? >> 8, 123, "Child threw Illegal exception";
+    }
+}
+
+{
+    # Test valid Scroll usage after initial fork
+    my $pid = fork();
+    unless ( defined($pid) ) { die "Cannot fork. Lack of resources?"; }
+    unless ($pid) {
+
+        my $s = $es->scroll_helper( size => 5 );
+
+        while ( $s->next ) { }
+        exit 0;
+    }
+    else {
+        # Wait for children
+        waitpid( $pid, 0 );
+        is $? , 0, "Scroll completed successfully";
+    }
+}
+
+done_testing;
+$es->indices->delete( index => 'test' );
+
+#===================================
+sub test_scroll {
+#===================================
+    my ( $title, $params, %tests ) = @_;
+
+    subtest $title => sub {
+        my $s = $es->scroll_helper($params);
+
+        is $s->total,                $tests{total},     "$title - total";
+        cmp_deeply $s->max_score,    $tests{max_score}, "$title - max_score";
+        cmp_deeply $s->suggest,      $tests{suggest},   "$title - suggest";
+        cmp_deeply $s->aggregations, $tests{aggs},      "$title - aggs";
+        my $i     = 1;
+        my @steps = @{ $tests{steps} };
+        while ( my $name = shift @steps ) {
+            my $expect = shift @steps;
+            my ( $method, $result, @p );
+            if ( $name =~ /next(?:_(\d+))?/ ) {
+                $method = 'next';
+                @p      = $1;
+            }
+            else {
+                $method = $name;
+            }
+
+            if ( ref $expect eq 'ARRAY' ) {
+                my @result = $s->$method(@p);
+                $result = 0 + @result;
+                $expect = $expect->[0];
+            }
+            else {
+                $result = $s->$method(@p);
+            }
+
+            is $result, $expect, "$title - Step $i: $name";
+            $i++;
+        }
+        }
+}
+
diff --git a/t/Client_8_0/60_auth_httptiny.t b/t/Client_8_0/60_auth_httptiny.t
new file mode 100644
index 0000000..3fc5e7b
--- /dev/null
+++ b/t/Client_8_0/60_auth_httptiny.t
@@ -0,0 +1,32 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use IO::Socket::SSL;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'HTTPTiny';
+our $Throws_SSL = "SSL";
+
+sub ssl_options {
+    return {
+        SSL_verify_mode => SSL_VERIFY_PEER,
+        SSL_ca_file     => $_[0]
+    };
+}
+
+do "es_sync_auth.pl" or die( $@ || $! );
diff --git a/t/Client_8_0/61_auth_lwp.t b/t/Client_8_0/61_auth_lwp.t
new file mode 100644
index 0000000..d2d2ad7
--- /dev/null
+++ b/t/Client_8_0/61_auth_lwp.t
@@ -0,0 +1,31 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'LWP';
+our $Throws_SSL = "Cxn";
+
+sub ssl_options {
+    return {
+        verify_hostname => 1,
+        SSL_ca_file     => $_[0]
+    };
+}
+
+do "es_sync_auth.pl" or die( $@ || $! );
diff --git a/t/Client_8_0/62_auth_netcurl.t b/t/Client_8_0/62_auth_netcurl.t
new file mode 100644
index 0000000..92977db
--- /dev/null
+++ b/t/Client_8_0/62_auth_netcurl.t
@@ -0,0 +1,32 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'NetCurl';
+use Net::Curl::Easy qw(
+    CURLOPT_CAINFO
+);
+
+our $Throws_SSL = "SSL";
+
+sub ssl_options {
+    return { CURLOPT_CAINFO() => $_[0] };
+}
+
+do "es_sync_auth.pl" or die( $@ || $! );
diff --git a/t/Client_8_0_Async/00_print_version.t b/t/Client_8_0_Async/00_print_version.t
new file mode 100644
index 0000000..5bb0bed
--- /dev/null
+++ b/t/Client_8_0_Async/00_print_version.t
@@ -0,0 +1,40 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use lib 't/lib';
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+
+eval {
+    my $v = wait_for( $es->info )->{version};
+    diag "";
+    diag "";
+    diag "Testing against Search::Elasticsearch::Async v" . $v->{number};
+    for ( sort keys %$v ) {
+        diag sprintf "%-20s: %s", $_, $v->{$_};
+    }
+    diag "";
+    diag "Client:   " . ref($es);
+    diag "Cxn:      " . $es->transport->cxn_pool->cxn_factory->cxn_class;
+    diag "GET Body: " . $es->transport->send_get_body_as;
+    diag "";
+    pass "ES Version";
+} or fail "ES Version";
+
+done_testing;
+
diff --git a/t/Client_8_0_Async/10_live.t b/t/Client_8_0_Async/10_live.t
new file mode 100644
index 0000000..4c50199
--- /dev/null
+++ b/t/Client_8_0_Async/10_live.t
@@ -0,0 +1,58 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use AE;
+use strict;
+use warnings;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+
+my $es;
+local $ENV{ES_CXN_POOL};
+
+$ENV{ES_CXN_POOL} = 'Async::Static';
+$es = do "es_async.pl" or die( $@ || $! );
+
+is wait_for( $es->info )->{tagline}, "You Know, for Search",
+    'CxnPool::Async::Static';
+
+$ENV{ES_CXN_POOL} = 'Async::Static::NoPing';
+$es = do "es_async.pl" or die( $@ || $! );
+is wait_for( $es->info )->{tagline}, "You Know, for Search",
+    'CxnPool::Async::Static::NoPing';
+
+$ENV{ES_CXN_POOL} = 'Async::Sniff';
+$es = do "es_async.pl" or die( $@ || $! );
+is wait_for( $es->info )->{tagline}, "You Know, for Search",
+    'CxnPool::Async::Sniff';
+
+my ($node) = values %{
+    (   wait_for(
+            $es->transport->cxn_pool->next_cxn->then(
+                sub { shift()->sniff }
+            )
+        )
+    )[1]
+};
+ok $node->{http}{max_content_length_in_bytes}, 'Sniffs max_content length';
+
+done_testing;
+
diff --git a/t/Client_8_0_Async/20_fork_aehttp.t b/t/Client_8_0_Async/20_fork_aehttp.t
new file mode 100644
index 0000000..698c022
--- /dev/null
+++ b/t/Client_8_0_Async/20_fork_aehttp.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'AEHTTP';
+do "es_async_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_8_0_Async/21_fork_mojo.t b/t/Client_8_0_Async/21_fork_mojo.t
new file mode 100644
index 0000000..aa353fd
--- /dev/null
+++ b/t/Client_8_0_Async/21_fork_mojo.t
@@ -0,0 +1,23 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'Mojo';
+do "es_async_fork.pl" or die( $@ || $! );
+
diff --git a/t/Client_8_0_Async/30_bulk_add_action.t b/t/Client_8_0_Async/30_bulk_add_action.t
new file mode 100644
index 0000000..0b5f2bb
--- /dev/null
+++ b/t/Client_8_0_Async/30_bulk_add_action.t
@@ -0,0 +1,233 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+my ( $error, $name );
+
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+
+my $b = $es->bulk_helper(
+    on_fatal => sub {
+        like shift(), qr/$error/, $name;
+    }
+);
+
+$b->_serializer->_set_canonical;
+
+## EMPTY
+
+ok $b->add_action(), 'Empty add action';
+
+## INDEX ACTIONS ##
+
+ok $b->add_action(
+    index => {
+        index        => 'foo',
+        id           => 1,
+        pipeline     => 'foo',
+        routing      => 1,
+        parent       => 1,
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    index => {
+        _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+
+    }
+    ),
+    'Add index actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"index":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"index":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Index actions in buffer";
+
+is $b->_buffer_size,  313, "Index actions buffer size";
+is $b->_buffer_count, 2,   "Index actions buffer count";
+
+$b->clear_buffer;
+
+## CREATE ACTIONS ##
+
+ok $b->add_action(
+    create => {
+        index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        pipeline     => 'foo',
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    create => {
+        _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+    }
+    ),
+    'Add create actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"create":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"create":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Create actions in buffer";
+
+is $b->_buffer_size,  315, "Create actions buffer size";
+is $b->_buffer_count, 2,   "Create actions buffer count";
+
+$b->clear_buffer;
+
+## DELETE ACTIONS ##
+
+ok $b->add_action(
+    delete => {
+        index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        version      => 1,
+        version_type => 'external',
+    },
+    delete => {
+        _index       => 'foo',
+        _id          => 2,
+        _routing     => 2,
+        _parent      => 2,
+        _version     => 1,
+        version_type => 'external',
+    }
+    ),
+    'Add delete actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1,"_index":"foo","parent":1,"routing":1,"version":1,"version_type":"external"}}),
+    q({"delete":{"_id":2,"_index":"foo","parent":2,"routing":2,"version":1,"version_type":"external"}}),
+    ],
+    "Delete actions in buffer";
+
+is $b->_buffer_size,  194, "Delete actions buffer size";
+is $b->_buffer_count, 2,   "Delete actions buffer count";
+
+$b->clear_buffer;
+
+## UPDATE ACTIONS ##
+
+ok $b->add_action(
+    update => {
+        index             => 'foo',
+        id                => 1,
+        routing           => 1,
+        _source           => 'true',
+        _source_includes  => 'foo',
+        _source_excludes  => 'bar',
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    },
+    update => {
+        _index            => 'foo',
+        _id               => 1,
+        _routing          => 1,
+        _source           => 'true',
+        _source_includes  => 'foo',
+        _source_excludes  => 'bar',
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    }
+    ),
+    'Add update actions';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"update":{"_id":1,"_index":"foo","routing":1}}),
+    q({"_source":"true","_source_excludes":"bar","_source_includes":"foo","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"}),
+    q({"update":{"_id":1,"_index":"foo","routing":1}}),
+    q({"_source":"true","_source_excludes":"bar","_source_includes":"foo","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"})
+    ],
+    "Update actions in buffer";
+
+is $b->_buffer_size,  486, "Update actions buffer size";
+is $b->_buffer_count, 2,   "Update actions buffer count";
+
+$b->clear_buffer;
+
+## ERRORS ##
+$error = "Unrecognised action";
+$name  = 'Bad action';
+$b->add_action( 'foo' => {} );
+
+$error = "Missing <params>";
+$name  = 'Missing params';
+$b->add_action( 'index', 'bar' );
+
+$error = "Missing .*<index>";
+$name  = 'Missing index';
+$b->add_action( index => { type => 't' } );
+
+$error = "Missing .*<source>";
+$name  = 'Missing source';
+$b->add_action( index => { index => 'i', type => 't' } );
+
+$error = "Unknown params";
+$name  = 'Unknown params';
+$b->add_action(
+    index => { index => 'i', type => 't', source => {}, foo => 1 } );
+
+done_testing;
diff --git a/t/Client_8_0_Async/31_bulk_helpers.t b/t/Client_8_0_Async/31_bulk_helpers.t
new file mode 100644
index 0000000..8423717
--- /dev/null
+++ b/t/Client_8_0_Async/31_bulk_helpers.t
@@ -0,0 +1,245 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use strict;
+use warnings;
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+
+my $b = $es->bulk_helper(
+    index => 'i',
+    type  => 't'
+);
+my $s = $b->_serializer;
+$s->_set_canonical;
+
+## INDEX ##
+
+ok $b->index(), 'Empty index';
+
+ok $b->index(
+    {   index        => 'foo',
+        id           => 1,
+        pipeline     => 'foo',
+        routing      => 1,
+        parent       => 1,
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    {   _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+
+    }
+    ),
+    'Index';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"index":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"index":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Index in buffer";
+
+is $b->_buffer_size,  313, "Index buffer size";
+is $b->_buffer_count, 2,   "Index buffer count";
+
+$b->clear_buffer;
+
+## CREATE ##
+
+ok $b->create(), 'Create empty';
+
+ok $b->create(
+    {   index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        pipeline     => 'foo',
+        timestamp    => 1380019061000,
+        ttl          => '10m',
+        version      => 1,
+        version_type => 'external',
+        source       => { foo => 'bar' },
+    },
+    {   _index        => 'foo',
+        _id           => 2,
+        _routing      => 2,
+        _parent       => 2,
+        _timestamp    => 1380019061000,
+        _ttl          => '10m',
+        _version      => 1,
+        _version_type => 'external',
+        source        => { foo => 'bar' },
+    }
+    ),
+    'Create';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"create":{"_id":1,"_index":"foo","parent":1,"pipeline":"foo","routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"}),
+    q({"create":{"_id":2,"_index":"foo","parent":2,"routing":2,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"foo":"bar"})
+    ],
+    "Create actions in buffer";
+
+is $b->_buffer_size,  315, "Create actions buffer size";
+is $b->_buffer_count, 2,   "Create actions buffer count";
+
+$b->clear_buffer;
+
+## CREATE DOCS##
+
+ok $b->create_docs(), 'Create_docs empty';
+
+ok $b->create_docs( { foo => 'bar' }, { foo => 'baz' } ), 'Create docs';
+
+cmp_deeply $b->_buffer,
+    [ q({"create":{}}), q({"foo":"bar"}), q({"create":{}}), q({"foo":"baz"}) ],
+    "Create docs in buffer";
+
+is $b->_buffer_size,  56, "Create docs buffer size";
+is $b->_buffer_count, 2,  "Create docs buffer count";
+
+$b->clear_buffer;
+
+## DELETE ##
+ok $b->delete(), 'Delete empty';
+
+ok $b->delete(
+    {   index        => 'foo',
+        id           => 1,
+        routing      => 1,
+        parent       => 1,
+        version      => 1,
+        version_type => 'external',
+    },
+    {   _index       => 'foo',
+        _id          => 2,
+        _routing     => 2,
+        _parent      => 2,
+        _version     => 1,
+        version_type => 'external',
+    }
+    ),
+    'Delete';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1,"_index":"foo","parent":1,"routing":1,"version":1,"version_type":"external"}}),
+    q({"delete":{"_id":2,"_index":"foo","parent":2,"routing":2,"version":1,"version_type":"external"}}),
+    ],
+    "Delete actions in buffer";
+
+is $b->_buffer_size,  194, "Delete actions buffer size";
+is $b->_buffer_count, 2,   "Delete actions buffer count";
+
+$b->clear_buffer;
+
+## DELETE IDS ##
+ok $b->delete_ids(), 'Delete IDs empty';
+
+ok $b->delete_ids( 1, 2, 3 ), 'Delete IDs';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"delete":{"_id":1}}), q({"delete":{"_id":2}}),
+    q({"delete":{"_id":3}}),
+    ],
+    "Delete IDs in buffer";
+
+is $b->_buffer_size,  63, "Delete IDs buffer size";
+is $b->_buffer_count, 3,  "Delete IDS buffer count";
+
+$b->clear_buffer;
+
+## UPDATE ACTIONS ##
+ok $b->update(), 'Update empty';
+ok $b->update(
+    {   index             => 'foo',
+        id                => 1,
+        routing           => 1,
+        parent            => 1,
+        timestamp         => 1380019061000,
+        ttl               => '10m',
+        version           => 1,
+        version_type      => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => 'foo',
+        _source_excludes  => 'bar',
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    },
+    {   _index            => 'foo',
+        _id               => 1,
+        _routing          => 1,
+        _parent           => 1,
+        _timestamp        => 1380019061000,
+        _ttl              => '10m',
+        _version          => 1,
+        _version_type     => 'external',
+        detect_noop       => 'true',
+        _source           => 'true',
+        _source_includes  => 'foo',
+        _source_excludes  => 'bar',
+        doc               => { foo => 'bar' },
+        doc_as_upsert     => 1,
+        fields            => ["*"],
+        script            => 'ctx._source+=1',
+        scripted_upsert   => 'true',
+        retry_on_conflict => 3,
+    }
+    ),
+    'Update';
+
+cmp_deeply $b->_buffer,
+    [
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":"bar","_source_includes":"foo","detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"}),
+    q({"update":{"_id":1,"_index":"foo","parent":1,"routing":1,"timestamp":1380019061000,"ttl":"10m","version":1,"version_type":"external"}}),
+    q({"_source":"true","_source_excludes":"bar","_source_includes":"foo","detect_noop":"true","doc":{"foo":"bar"},"doc_as_upsert":1,"fields":["*"],"retry_on_conflict":3,"script":"ctx._source+=1","scripted_upsert":"true"})
+    ],
+    "Update actions in buffer";
+
+is $b->_buffer_size,  702, "Update actions buffer size";
+is $b->_buffer_count, 2,   "Update actions buffer count";
+
+$b->clear_buffer;
+
+done_testing;
diff --git a/t/Client_8_0_Async/32_bulk_flush.t b/t/Client_8_0_Async/32_bulk_flush.t
new file mode 100644
index 0000000..a1fda97
--- /dev/null
+++ b/t/Client_8_0_Async/32_bulk_flush.t
@@ -0,0 +1,147 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use strict;
+use warnings;
+use lib 't/lib';
+use AE;
+use Promises qw(deferred);
+
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+
+wait_for( $es->indices->delete( index => '_all' ) );
+
+test_flush(
+    "max count",    #
+    { max_count => 3 },    #
+    1, 2, 0, 1, 2, 0, 1, 2, 0, 1
+);
+
+test_flush(
+    "max size",            #
+    { max_size => 95 },    #
+    1, 2, 3, 0, 1, 2, 3, 0, 1, 2
+);
+
+test_flush(
+    "max size > max_count",
+    { max_size => 95, max_count => 3 },
+    1, 2, 0, 1, 2, 0, 1, 2, 0, 1
+);
+
+test_flush(
+    "max size < max_count",
+    { max_size => 95, max_count => 5 },
+    1, 2, 3, 0, 1, 2, 3, 0, 1, 2
+);
+
+test_flush(
+    "max size = 0, max_count",
+    { max_size => 0, max_count => 5 },
+    1, 2, 3, 4, 0, 1, 2, 3, 4, 0
+);
+
+test_flush(
+    "max count = 0, max_size",
+    { max_size => 95, max_count => 0 },
+    1, 2, 3, 0, 1, 2, 3, 0, 1, 2
+);
+
+test_flush(
+    "max count = 0, max_size = 0",
+    { max_size => 0, max_count => 0 },
+    1, 2, 3, 4, 5, 6, 7, 8, 9, 10
+);
+
+test_flush(
+    "max_count = 5, max_time = 5",
+    { max_count => 5, max_time => 5 },
+    1, 2, 0, 1, 2, 3, 4, 0, 0, 1
+);
+
+done_testing;
+
+wait_for( $es->indices->delete( index => 'test' ) );
+
+#===================================
+sub test_flush {
+#===================================
+    my $title  = shift;
+    my $params = shift;
+    my $b      = $es->bulk_helper(
+        %$params,
+        index => 'test'
+    );
+
+    my @seq = @_;
+    my $cv  = AE::cv;
+
+    my $i = 10;
+    my $loop;
+
+    my $index_doc = sub {
+        $b->index( { id => $i, source => {} } );
+    };
+
+    my $check_buffer = sub {
+        is $b->_buffer_count, shift @seq, "$title - " . ( $i - 9 );
+        $i++;
+    };
+
+    my $d = deferred;
+    my $w;
+
+    $loop = sub {
+        if ( $i == 20 ) {
+            return $b->flush->then( sub { $d->resolve } );
+        }
+
+        # sleep on 12 or 18 if max_time specified
+        if ( $params->{max_time} && ( $i == 12 || $i == 18 ) ) {
+            $b->_last_flush( time - $params->{max_time} - 1 );
+        }
+        $index_doc->()->then($check_buffer)->then($loop);
+    };
+
+    $es->indices->delete( index => 'test', ignore => 404 )
+        ->then( sub { $es->indices->create( index => 'test' ) } )
+        ->then( sub { $es->cluster->health( wait_for_status => 'yellow' ) } )
+        ->then($loop);
+
+    $d->promise->then(
+        sub {
+            is $b->_buffer_count, 0, "$title - final flush";
+            $es->indices->refresh;
+        }
+        )->then(
+        sub {
+            $es->count;
+        }
+        )->then(
+        sub {
+            is shift()->{count}, 10, "$title - all docs indexed";
+        }
+        )->then(
+        sub {
+            $cv->send;
+        }
+        )->catch( sub { $cv->croak(@_) } );
+    $cv->recv;
+}
diff --git a/t/Client_8_0_Async/33_bulk_errors.t b/t/Client_8_0_Async/33_bulk_errors.t
new file mode 100644
index 0000000..2bf269e
--- /dev/null
+++ b/t/Client_8_0_Async/33_bulk_errors.t
@@ -0,0 +1,212 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use AE;
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Log::Any::Adapter;
+
+$ENV{ES_VERSION} = '8_0';
+my $es = do "es_async.pl" or die( $@ || $! );
+my $TRUE = $es->transport->serializer->decode('{"true":true}')->{true};
+
+my $cv = AE::cv;
+
+wait_for( $es->indices->delete( index => '_all' ) );
+
+my @Std = (
+    { id => 1, source => { count => 1 } },
+    { id => 1, source => { count => 'foo' } },
+    { id => 1, source => {} },
+);
+
+my ( $b, $error, $success_count, $error_count, $custom_count, $conflict_count );
+
+## Default error handling
+$b = bulk( { index => 'test'}, @Std );
+test_flush( "Default", 0, 1, 0, 0 );
+
+## Custom error handling
+$b = bulk(
+    {   index    => 'test',
+        on_error => sub { $custom_count++ }
+    },
+    @Std
+);
+test_flush( "Custom error", 0, 0, 1, 0 );
+
+# Conflict errors
+$b = bulk(
+    {   index       => 'test',
+        on_conflict => sub { $conflict_count++ }
+    },
+    @Std
+);
+test_flush( "Conflict error", 0, 1, 0, 0 );
+
+# Both error handling
+$b = bulk(
+    {   index       => 'test',
+        on_conflict => sub { $conflict_count++ },
+        on_error    => sub { $custom_count++ }
+    },
+    @Std
+);
+
+test_flush( "Conflict and custom", 0, 0, 1, 0 );
+
+# Conflict disable error
+$b = bulk(
+    {   index       => 'test',
+        on_conflict => sub { $conflict_count++ },
+        on_error    => undef
+    },
+    @Std
+);
+
+test_flush( "Conflict, error undef", 0, 0, 0, 0 );
+
+# Disable both
+$b = bulk(
+    {   index       => 'test',
+        on_conflict => undef,
+        on_error    => undef
+    },
+    @Std
+);
+
+test_flush( "Both undef", 0, 0, 0, 0 );
+
+# Success
+$b = bulk(
+    {   index      => 'test',
+        on_success => sub { $success_count++ },
+    },
+    @Std
+);
+
+test_flush( "Success", 2, 1, 0, 0 );
+
+# cbs have correct params
+$b = bulk(
+    {   index      => 'test',
+        on_success => test_params(
+            'on_success',
+            {   _index        => 'test',
+                _type         => '_doc',
+                _id           => 1,
+                _version      => 1,
+                created       => $TRUE,
+                _shards       => { successful => 1, total => 2, failed => 0 },
+                _primary_term => 1,
+                _seq_no       => 0,
+            },
+            0
+        ),
+        on_error => test_params(
+            'on_error',
+            {   _index => 'test',
+                _type  => '_doc',
+                _id    => 1,
+                error  => any(
+                    re('MapperParsingException'),
+                    superhashof( { type => 'mapper_parsing_exception' } )
+                ),
+                status => 400,
+            },
+            1
+        ),
+        on_conflict => test_params(
+            'on_conflict',
+            {   _index => 'test',
+                _type  => '_doc',
+                _id    => 1,
+                error  => any(
+                    re('version conflict'),
+                    superhashof(
+                        { type => 'version_conflict_engine_exception' }
+                    )
+                ),
+                status => 409,
+            },
+            2, 1
+        ),
+    },
+    @Std
+);
+wait_for( $b->flush );
+
+done_testing;
+
+wait_for( $es->indices->delete( index => '_all' ) );
+
+#===================================
+sub bulk {
+#===================================
+    my ( $params, @docs ) = @_;
+    my $b = $es->bulk_helper(
+        on_fatal => sub { $error = shift(); $error_count++ },
+        %$params,
+    );
+
+    $error = '';
+
+    wait_for(
+        $es->indices->delete( index => 'test', ignore => 404 )    #
+            ->then( sub { $es->indices->create( index => 'test' ) } )    #
+            ->then(
+            sub { $es->cluster->health( wait_for_status => 'yellow' ) }
+            )                                                            #
+            ->then( sub { $b->index(@docs) } )
+    );
+    return $b;
+}
+
+#===================================
+sub test_flush {
+#===================================
+    my ( $title, $success, $default, $custom, $conflict ) = @_;
+    $success_count = $custom_count = $error_count = $conflict_count = 0;
+    {
+        local $SIG{__WARN__} = sub { $error_count++ };
+        wait_for( $b->flush );
+    }
+    is $success_count,  $success,  "$title - success";
+    is $error_count,    $default,  "$title - default";
+    is $custom_count,   $custom,   "$title - custom";
+    is $conflict_count, $conflict, "$title - conflict";
+
+}
+
+#===================================
+sub test_params {
+#===================================
+    my ( $type, $result, $j, $version ) = @_;
+
+    return sub {
+        is $_[0], 'index', "$type - action";
+        cmp_deeply subhashof($result), $_[1], "$type - result";
+        is $_[2], $j,       "$type - array index";
+        is $_[3], $version, "$type - version";
+    };
+}
+
diff --git a/t/Client_8_0_Async/34_bulk_cxn_errors.t b/t/Client_8_0_Async/34_bulk_cxn_errors.t
new file mode 100644
index 0000000..190b31c
--- /dev/null
+++ b/t/Client_8_0_Async/34_bulk_cxn_errors.t
@@ -0,0 +1,53 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use AE;
+
+use strict;
+use warnings;
+use lib 't/lib';
+use Log::Any::Adapter;
+
+$ENV{ES_VERSION}   = '8_0';
+$ENV{ES}           = '10.255.255.1:9200';
+$ENV{ES_SKIP_PING} = 1;
+$ENV{ES_CXN_POOL}  = 'Async::Static';
+
+my $es = do "es_async.pl" or die( $@ || $! );
+my $error;
+my $b = $es->bulk_helper( index => 'foo', type => 'bar' );
+$b->create_docs( { foo => 'bar' } );
+
+# Check that the buffer is not cleared on a NoNodes exception
+
+is $b->_buffer_count, 1, "Buffer count pre-flush";
+
+wait_for(
+    $b->flush->catch(
+        sub {
+            my $error = shift;
+            isa_ok $error, 'Search::Elasticsearch::Error::NoNodes';
+        }
+    )
+);
+
+is $b->_buffer_count, 1, "Buffer count post-flush";
+
+done_testing;
diff --git a/t/Client_8_0_Async/40_scroll.t b/t/Client_8_0_Async/40_scroll.t
new file mode 100644
index 0000000..6e7bdcc
--- /dev/null
+++ b/t/Client_8_0_Async/40_scroll.t
@@ -0,0 +1,205 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use lib 't/lib';
+
+use strict;
+use warnings;
+
+our ( $s, $total_seen, $max_seen );
+$ENV{ES_VERSION} = '8_0';
+our $es = do "es_async.pl" or die( $@ || $! );
+
+wait_for( $es->indices->delete( index => '_all', ignore => 404 ) );
+
+test_scroll(
+    "No indices",
+    { on_results => \&on_results },
+    total      => 0,
+    max_score  => 0,
+    total_seen => 0,
+    max_seen   => 0,
+);
+
+do "index_test_data_7.pl" or die( $@ || $! );
+
+test_scroll(
+    "Match all - on_result",
+    { on_result => \&on_results, size => 10 },
+    total      => 100,
+    max_score  => 1,
+    total_seen => 100,
+    max_seen   => 1
+);
+
+test_scroll(
+    "Match all - on_results",
+    { on_results => \&on_results, size => 10 },
+    total      => 100,
+    max_score  => 1,
+    total_seen => 100,
+    max_seen   => 10
+);
+
+test_scroll(
+    "Query",
+    {   body => {
+            query   => { term => { color => 'red' } },
+            suggest => {
+                mysuggest => { text => 'green', term => { field => 'color' } }
+            },
+            aggs => { switch => { terms => { field => 'switch' } } },
+        },
+        size       => 10,
+        on_results => \&on_results
+    },
+    total      => 50,
+    max_score  => num( 1.0, 0.5 ),
+    aggs       => bool(1),
+    suggest    => bool(1),
+    total_seen => 50,
+    max_seen   => 10
+);
+
+test_scroll(
+    "Finish",
+    {   on_results => sub {
+            on_results(@_);
+            $s->finish if $total_seen == 30;
+        },
+        size => 10
+    },
+    total      => 100,
+    max_score  => 1,
+    total_seen => 30,
+    max_seen   => 10
+);
+
+{
+    # Test auto finish fork protection.
+    my $count = 0;
+    my $s = $es->scroll_helper( size => 5, on_result => sub { $count++ } );
+
+    my $pid = fork();
+    unless ( defined($pid) ) { die "Cannot fork. Lack of resources?"; }
+    unless ($pid) {
+
+        # Child. Call finish check that its not finished
+        # (the call to finish did nothing).
+        wait_for( $s->finish() );
+        exit 0;
+    }
+    else {
+        # Wait for children
+        waitpid( $pid, 0 );
+        is $?, 0, "Child exited without errors";
+    }
+    ok !$s->is_finished(), "Our Scroll is not finished";
+    wait_for( $s->start );
+    is $count, 100, "All documents retrieved";
+    ok $s->is_finished, "Our scroll is finished";
+}
+
+# {
+#     # Test Scroll usage attempt in a different process.
+#     my $count = 0;
+#     my $s     = $es->scroll_helper(
+#         size      => 5,
+#         on_result => sub { $count++ },
+#         on_error  => sub { die @_ }
+#     );
+
+#     my $pid = fork();
+#     unless ( defined($pid) ) { die "Cannot fork. Lack of resources?"; }
+#     unless ($pid) {
+
+#         eval { wait_for( $s->start ) };
+#         my $err = $@;
+#         exit( eval { $err->is('Illegal') && 123 } || 999 );
+#     }
+#     else {
+#         # Wait for children
+#         waitpid( $pid, 0 );
+#         is $? >> 8, 123, "Child threw Illegal exception";
+#     }
+# }
+
+# {
+#     # Test valid Scroll usage after initial fork
+#     my $pid = fork();
+#     unless ( defined($pid) ) { die "Cannot fork. Lack of resources?"; }
+#     unless ($pid) {
+
+#         my $count = 0;
+#         my $s     = $es->scroll_helper(
+#             size      => 5,
+#             on_result => sub { $count++ },
+#             on_error  => sub { die @_ }
+#         );
+
+#         wait_for( $s->start );
+#         exit 0;
+#     }
+#     else {
+#         # Wait for children
+#         waitpid( $pid, 0 );
+#         is $? , 0, "Scroll completed successfully";
+#     }
+# }
+
+done_testing;
+
+wait_for( $es->indices->delete( index => 'test' ) );
+
+#===================================
+sub test_scroll {
+#===================================
+    my ( $title, $params, %tests ) = @_;
+    $max_seen = $total_seen = 0;
+    subtest $title => sub {
+        $s = $es->scroll_helper(
+            on_start => sub { test_start( $title, \%tests, @_ ) },
+            %$params
+        );
+        wait_for( $s->start );
+
+        is $total_seen, $tests{total_seen}, "$title - total seen";
+        is $max_seen,   $tests{max_seen},   "$title - max seen";
+
+    };
+}
+
+#===================================
+sub test_start {
+#===================================
+    my ( $title, $tests, $s ) = @_;
+    is $s->total,                $tests->{total},     "$title - total";
+    cmp_deeply $s->max_score,    $tests->{max_score}, "$title - max_score";
+    cmp_deeply $s->suggest,      $tests->{suggest},   "$title - suggest";
+    cmp_deeply $s->aggregations, $tests->{aggs},      "$title - aggs";
+
+}
+
+#===================================
+sub on_results {
+#===================================
+    $max_seen = @_ if @_ > $max_seen;
+    $total_seen += @_;
+}
diff --git a/t/Client_8_0_Async/60_auth_aehttp.t b/t/Client_8_0_Async/60_auth_aehttp.t
new file mode 100644
index 0000000..93f6bd4
--- /dev/null
+++ b/t/Client_8_0_Async/60_auth_aehttp.t
@@ -0,0 +1,31 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'AEHTTP';
+
+sub ssl_options {
+    return {
+        verify          => 1,
+        verify_peername => 'https',
+        ca_file         => $_[0]
+    };
+}
+
+do "es_async_auth.pl" or die( $@ || $! );
diff --git a/t/Client_8_0_Async/61_auth_mojo.t b/t/Client_8_0_Async/61_auth_mojo.t
new file mode 100644
index 0000000..2756149
--- /dev/null
+++ b/t/Client_8_0_Async/61_auth_mojo.t
@@ -0,0 +1,27 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use lib 't/lib';
+
+$ENV{ES_VERSION} = '8_0';
+$ENV{ES_CXN} = 'Mojo';
+
+sub ssl_options {
+    return { ca => $_[0] };
+}
+
+do "es_async_auth.pl" or die( $@ || $! );
diff --git a/t/author-eol.t b/t/author-eol.t
deleted file mode 100644
index e277938..0000000
--- a/t/author-eol.t
+++ /dev/null
@@ -1,61 +0,0 @@
-
-BEGIN {
-  unless ($ENV{AUTHOR_TESTING}) {
-    print qq{1..0 # SKIP these tests are for testing by the author\n};
-    exit
-  }
-}
-
-use strict;
-use warnings;
-
-# this test was generated with Dist::Zilla::Plugin::Test::EOL 0.19
-
-use Test::More 0.88;
-use Test::EOL;
-
-my @files = (
-    'lib/Search/Elasticsearch/Client/2_0.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Bulk.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Cat.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Cluster.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Indices.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Nodes.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Snapshot.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Tasks.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Role/API.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Role/Bulk.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Role/Scroll.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Scroll.pm',
-    'lib/Search/Elasticsearch/Client/2_0/TestServer.pm',
-    't/Client_2_0/00_print_version.t',
-    't/Client_2_0/10_live.t',
-    't/Client_2_0/15_conflict.t',
-    't/Client_2_0/20_fork_httptiny.t',
-    't/Client_2_0/21_fork_lwp.t',
-    't/Client_2_0/22_fork_hijk.t',
-    't/Client_2_0/30_bulk_add_action.t',
-    't/Client_2_0/31_bulk_helpers.t',
-    't/Client_2_0/32_bulk_flush.t',
-    't/Client_2_0/33_bulk_errors.t',
-    't/Client_2_0/34_bulk_cxn_errors.t',
-    't/Client_2_0/40_scroll.t',
-    't/Client_2_0/50_reindex.t',
-    't/Client_2_0/60_auth_httptiny.t',
-    't/Client_2_0/61_auth_lwp.t',
-    't/author-eol.t',
-    't/author-no-tabs.t',
-    't/author-pod-syntax.t',
-    't/lib/LogCallback.pl',
-    't/lib/MockCxn.pm',
-    't/lib/bad_cacert.pem',
-    't/lib/default_cxn.pl',
-    't/lib/es_sync.pl',
-    't/lib/es_sync_auth.pl',
-    't/lib/es_sync_fork.pl',
-    't/lib/index_test_data.pl'
-);
-
-eol_unix_ok($_, { trailing_whitespace => 1 }) foreach @files;
-done_testing;
diff --git a/t/author-no-tabs.t b/t/author-no-tabs.t
deleted file mode 100644
index a4d35b7..0000000
--- a/t/author-no-tabs.t
+++ /dev/null
@@ -1,61 +0,0 @@
-
-BEGIN {
-  unless ($ENV{AUTHOR_TESTING}) {
-    print qq{1..0 # SKIP these tests are for testing by the author\n};
-    exit
-  }
-}
-
-use strict;
-use warnings;
-
-# this test was generated with Dist::Zilla::Plugin::Test::NoTabs 0.15
-
-use Test::More 0.88;
-use Test::NoTabs;
-
-my @files = (
-    'lib/Search/Elasticsearch/Client/2_0.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Bulk.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Cat.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Cluster.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Indices.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Nodes.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Snapshot.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Direct/Tasks.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Role/API.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Role/Bulk.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Role/Scroll.pm',
-    'lib/Search/Elasticsearch/Client/2_0/Scroll.pm',
-    'lib/Search/Elasticsearch/Client/2_0/TestServer.pm',
-    't/Client_2_0/00_print_version.t',
-    't/Client_2_0/10_live.t',
-    't/Client_2_0/15_conflict.t',
-    't/Client_2_0/20_fork_httptiny.t',
-    't/Client_2_0/21_fork_lwp.t',
-    't/Client_2_0/22_fork_hijk.t',
-    't/Client_2_0/30_bulk_add_action.t',
-    't/Client_2_0/31_bulk_helpers.t',
-    't/Client_2_0/32_bulk_flush.t',
-    't/Client_2_0/33_bulk_errors.t',
-    't/Client_2_0/34_bulk_cxn_errors.t',
-    't/Client_2_0/40_scroll.t',
-    't/Client_2_0/50_reindex.t',
-    't/Client_2_0/60_auth_httptiny.t',
-    't/Client_2_0/61_auth_lwp.t',
-    't/author-eol.t',
-    't/author-no-tabs.t',
-    't/author-pod-syntax.t',
-    't/lib/LogCallback.pl',
-    't/lib/MockCxn.pm',
-    't/lib/bad_cacert.pem',
-    't/lib/default_cxn.pl',
-    't/lib/es_sync.pl',
-    't/lib/es_sync_auth.pl',
-    't/lib/es_sync_fork.pl',
-    't/lib/index_test_data.pl'
-);
-
-notabs_ok($_) foreach @files;
-done_testing;
diff --git a/t/author-pod-syntax.t b/t/author-pod-syntax.t
deleted file mode 100644
index 2233af0..0000000
--- a/t/author-pod-syntax.t
+++ /dev/null
@@ -1,15 +0,0 @@
-#!perl
-
-BEGIN {
-  unless ($ENV{AUTHOR_TESTING}) {
-    print qq{1..0 # SKIP these tests are for testing by the author\n};
-    exit
-  }
-}
-
-# This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests.
-use strict; use warnings;
-use Test::More;
-use Test::Pod 1.41;
-
-all_pod_files_ok();
diff --git a/t/lib/LogCallback.pl b/t/lib/LogCallback.pl
index 151a79b..09728bd 100644
--- a/t/lib/LogCallback.pl
+++ b/t/lib/LogCallback.pl
@@ -1,3 +1,20 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 use Log::Any::Adapter::Callback 0.09;
 use Log::Any::Adapter;
 
diff --git a/t/lib/MockAsyncCxn.pm b/t/lib/MockAsyncCxn.pm
new file mode 100644
index 0000000..4d6b580
--- /dev/null
+++ b/t/lib/MockAsyncCxn.pm
@@ -0,0 +1,166 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+package MockAsyncCxn;
+
+use strict;
+use warnings;
+use Search::Elasticsearch::Role::Cxn qw(PRODUCT_CHECK_HEADER PRODUCT_CHECK_VALUE);
+
+our $PRODUCT_CHECK_VALUE = $Search::Elasticsearch::Role::Cxn::PRODUCT_CHECK_VALUE;
+our $PRODUCT_CHECK_HEADER = $Search::Elasticsearch::Role::Cxn::PRODUCT_CHECK_HEADER;
+our $VERSION = $Search::Elasticsearch::VERSION;
+
+use Promises qw(deferred);
+use Data::Dumper;
+use Moo;
+with 'Search::Elasticsearch::Role::Cxn::Async';
+with 'Search::Elasticsearch::Role::Cxn',
+    'Search::Elasticsearch::Role::Is_Async';
+
+use Sub::Exporter -setup => {
+    exports => [ qw(
+            mock_static_client
+            mock_sniff_client
+            mock_noping_client
+            )
+    ]
+};
+
+our $i = 0;
+
+has 'mock_responses' => ( is => 'rw', required => 1 );
+has 'marked_live'    => ( is => 'rw', default  => sub {0} );
+has 'node_num'       => ( is => 'ro', default  => sub { ++$i } );
+
+#===================================
+sub BUILD {
+#===================================
+    my $self = shift;
+    $self->logger->debugf( "[%s-%s] CREATED", $self->node_num, $self->host );
+}
+
+#===================================
+sub error_from_text { return $_[2] }
+#===================================
+
+#===================================
+sub perform_request {
+#===================================
+    my $self   = shift;
+    my $params = shift;
+
+    my $d = deferred;
+
+    eval {
+        my $response = shift @{ $self->mock_responses }
+            or die "Mock responses exhausted";
+
+        if ( my $node = $response->{node} ) {
+            die "Mock response handled by wrong node ["
+                . $self->node_num . "]: "
+                . Dumper($response)
+                unless $node eq $self->node_num;
+        }
+
+        my $log_msg;
+
+        # Sniff request
+        if ( my $nodes = $response->{sniff} ) {
+            $log_msg = "SNIFF: [" . ( join ", ", @$nodes ) . "]";
+            $response->{code} ||= 200;
+            my $i = 1;
+            unless ( $response->{error} ) {
+                $response->{content} = $self->serializer->encode(
+                    {   nodes => {
+                            map {
+                                'node_'
+                                    . $i++ => { http_address => "inet[/$_]" }
+                            } @$nodes
+                        }
+                    }
+                );
+            }
+        }
+
+        # Normal request
+        elsif ( $response->{code} ) {
+            $log_msg
+                = "REQUEST: " . ( $response->{error} || $response->{code} );
+        }
+
+        # Ping request
+        else {
+            $log_msg = "PING: " . ( $response->{ping} ? 'OK' : 'NOT_OK' );
+            $response
+                = $response->{ping}
+                ? { code => 200 }
+                : { code => 500, error => 'Cxn' };
+        }
+
+        $self->logger->debugf( "[%s-%s] %s", $self->node_num, $self->host,
+            $log_msg );
+
+        $d->resolve(
+            $self->process_response(
+                $params,                 # request
+                $response->{code},       # code
+                $response->{error},      # msg
+                $response->{content},    # body
+                {
+                    'content-type' => 'application/json',
+                    $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE
+                }
+            )
+        );
+        1;
+    } || do { $d->reject( $@ || 'Unknown error' ) };
+    return $d->promise;
+}
+
+#### EXPORTS ###
+
+my $trace
+    = !$ENV{TRACE}       ? undef
+    : $ENV{TRACE} eq '1' ? 'Stderr'
+    :                      [ 'File', $ENV{TRACE} ];
+
+#===================================
+sub mock_static_client { _mock_client( 'Async::Static',         @_ ) }
+sub mock_sniff_client  { _mock_client( 'Async::Sniff',          @_ ) }
+sub mock_noping_client { _mock_client( 'Async::Static::NoPing', @_ ) }
+#===================================
+
+#===================================
+sub _mock_client {
+#===================================
+    my $pool   = shift;
+    my $params = shift;
+    $i = 0;
+    return Search::Elasticsearch::Async->new(
+        cxn            => '+MockAsyncCxn',
+        transport      => '+MockAsyncTransport',
+        cxn_pool       => $pool,
+        mock_responses => \@_,
+        dead_timeout   => 500,
+        randomize_cxns => 0,
+        log_to         => $trace,
+        %$params,
+    )->transport;
+}
+
+1
diff --git a/t/lib/MockAsyncTransport.pm b/t/lib/MockAsyncTransport.pm
new file mode 100644
index 0000000..d44039b
--- /dev/null
+++ b/t/lib/MockAsyncTransport.pm
@@ -0,0 +1,25 @@
+package MockAsyncTransport;
+
+use strict;
+use warnings;
+
+our $VERSION = $Search::Elasticsearch::VERSION;
+
+use AE;
+use Moo;
+extends 'Search::Elasticsearch::Transport::Async';
+
+our $w;
+#===================================
+sub perform_sync_request {
+#===================================
+    my $self = shift;
+    my $cv   = AE::cv;
+    $w = AE::timer( 1, 0,
+        sub { $cv->croak('Response timed out'); undef $w } );
+    my $promise = $self->perform_request(@_);
+    $promise->then( sub { $cv->send(@_) }, sub { $cv->croak(@_) } );
+    $cv->recv;
+}
+
+1
diff --git a/t/lib/MockCxn.pm b/t/lib/MockCxn.pm
index 271c384..cc70e7b 100644
--- a/t/lib/MockCxn.pm
+++ b/t/lib/MockCxn.pm
@@ -1,8 +1,28 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 package MockCxn;
 
 use strict;
 use warnings;
+use Search::Elasticsearch::Role::Cxn qw(PRODUCT_CHECK_HEADER PRODUCT_CHECK_VALUE);
 
+our $PRODUCT_CHECK_VALUE = $Search::Elasticsearch::Role::Cxn::PRODUCT_CHECK_VALUE;
+our $PRODUCT_CHECK_HEADER = $Search::Elasticsearch::Role::Cxn::PRODUCT_CHECK_HEADER;
 our $VERSION = $Search::Elasticsearch::VERSION;
 
 use Data::Dumper;
@@ -93,7 +113,10 @@ sub perform_request {
         $response->{code},       # code
         $response->{error},      # msg
         $response->{content},    # body
-        { 'content-type' => 'application/json' }
+        {
+            'content-type' => 'application/json',
+            $PRODUCT_CHECK_HEADER => $PRODUCT_CHECK_VALUE
+        }
     );
 }
 
diff --git a/t/lib/default_async_cxn.pl b/t/lib/default_async_cxn.pl
new file mode 100644
index 0000000..30065a0
--- /dev/null
+++ b/t/lib/default_async_cxn.pl
@@ -0,0 +1,18 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+return 'AEHTTP';
diff --git a/t/lib/default_cxn.pl b/t/lib/default_cxn.pl
index 2a2431f..498e07c 100644
--- a/t/lib/default_cxn.pl
+++ b/t/lib/default_cxn.pl
@@ -1 +1,18 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 return 'HTTPTiny';
diff --git a/t/lib/es_async.pl b/t/lib/es_async.pl
new file mode 100644
index 0000000..604b87e
--- /dev/null
+++ b/t/lib/es_async.pl
@@ -0,0 +1,124 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+#!perl -d
+use EV;
+use AE;
+
+use Promises backend => ['EV'];
+use Search::Elasticsearch::Async;
+use Test::More;
+use strict;
+use warnings;
+
+my $trace
+    = !$ENV{TRACE}       ? undef
+    : $ENV{TRACE} eq '1' ? 'Stderr'
+    :                      [ 'File', $ENV{TRACE} ];
+
+unless ($ENV{CLIENT_VER}) {
+    plan skip_all => 'No $ENV{CLIENT_VER} specified';
+    exit;
+}
+unless ($ENV{ES}) {
+    plan skip_all => 'No Elasticsearch test node available';
+    exit;
+}
+
+my $cv = AE::cv;
+
+my $api      = "$ENV{CLIENT_VER}::Direct";
+my $body     = $ENV{ES_BODY} || 'GET';
+my $cxn      = $ENV{ES_CXN} || do "default_async_cxn.pl" || die( $@ || $! );
+my $cxn_pool = $ENV{ES_CXN_POOL} || 'Async::Static';
+my @plugins  = split /,/, ( $ENV{ES_PLUGINS} || '' );
+our %Auth;
+
+if ( $cxn eq 'Mojo' && !eval { require Mojo::UserAgent; 1 } ) {
+    plan skip_all => 'Mojo::UserAgent not installed';
+    exit;
+}
+
+{
+    no warnings 'redefine';
+
+#===================================
+    sub wait_for {
+#===================================
+        my $promise = shift;
+        my $cv      = AE::cv;
+        $promise->done( $cv, sub { $cv->croak(@_) } );
+        $cv->recv;
+    }
+}
+
+my $es;
+if ( $ENV{ES} ) {
+    eval {
+        $es = Search::Elasticsearch::Async->new(
+            nodes            => $ENV{ES},
+            trace_to         => $trace,
+            cxn              => $cxn,
+            cxn_pool         => $cxn_pool,
+            client           => $api,
+            send_get_body_as => $body,
+            plugins          => \@plugins,
+            %Auth
+        );
+        if ( $ENV{ES_SKIP_PING} ) {
+            $cv->send(1);
+        }
+        else {
+            $es->ping->then( sub { $cv->send(@_) }, sub { $cv->croak(@_) } );
+        }
+        $cv->recv;
+        1;
+    } or do {
+        diag $@;
+        undef $es;
+    };
+}
+
+unless ($es) {
+    plan skip_all => 'No Elasticsearch test node available';
+    exit;
+}
+
+unless ( $ENV{ES_SKIP_PING} ) {
+    my $version = wait_for( $es->info )->{version}{number};
+    my $api     = $es->api_version;
+    unless ( $version eq '8.0.0-SNAPSHOT' || ( $api eq '0_90' && $version =~ /^0\.9/
+        || substr( $api, 0, 1 ) eq substr( $version, 0, 1 ) ) )
+    {
+        plan skip_all =>
+            "Tests are for API version $api but Elasticsearch is version $version\n";
+        exit;
+    }
+}
+
+return $es;
+
+unless ( $ENV{ES_SKIP_PING} ) {
+    my $version = wait_for( $es->info )->{version}{number};
+    my $api     = $es->api_version;
+    diag "$version - $api\n";
+    die "Tests are for API version $api but Elasticsearch is version $version\n"
+        unless $api eq '0.90' && $version =~ /^0\.9/
+        || substr( $api, 0, 1 ) eq substr( $version, 0, 1 );
+}
+
+return $es;
diff --git a/t/lib/es_async_auth.pl b/t/lib/es_async_auth.pl
new file mode 100644
index 0000000..579d81b
--- /dev/null
+++ b/t/lib/es_async_auth.pl
@@ -0,0 +1,103 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+#! perl
+use Test::More;
+use Test::Deep;
+use Test::Exception;
+use AE;
+
+use strict;
+use warnings;
+use lib 't/lib';
+
+unless ( $ENV{ES_SSL} ) {
+    plan skip_all => "$ENV{ES_CXN} - No https server specified in ES_SSL";
+    exit;
+}
+
+unless ( $ENV{ES_USERINFO} ) {
+    plan skip_all => "$ENV{ES_CXN} - No user/pass specified in ES_USERINFO";
+    exit;
+}
+
+unless ( $ENV{ES_CA_PATH} ) {
+    plan skip_all => "$ENV{ES_CXN} - No cacert specified in ES_CA_PATH";
+    exit;
+}
+
+$ENV{ES}           = $ENV{ES_SSL};
+$ENV{ES_SKIP_PING} = 1;
+
+our %Auth = ( use_https => 1, userinfo => $ENV{ES_USERINFO} );
+
+# Test https connection with correct auth, without cacert
+$ENV{ES_CXN_POOL} = 'Async::Static';
+my $es = do "es_async.pl" or die( $@ || $! );
+ok wait_for( $es->cluster->health ),
+    "$ENV{ES_CXN} - Non-cert HTTPS with auth, cxn static";
+
+$ENV{ES_CXN_POOL} = 'Async::Sniff';
+$es = do "es_async.pl" or die( $@ || $! );
+ok wait_for( $es->cluster->health ),
+    "$ENV{ES_CXN} - Non-cert HTTPS with auth, cxn sniff";
+
+$ENV{ES_CXN_POOL} = 'Async::Static::NoPing';
+$es = do "es_async.pl" or die( $@ || $! );
+ok wait_for( $es->cluster->health ),
+    "$ENV{ES_CXN} - Non-cert HTTPS with auth, cxn noping";
+
+# Test forbidden action
+throws_ok { wait_for( $es->nodes->shutdown ) }
+"Search::Elasticsearch::Error::Forbidden",
+    "$ENV{ES_CXN} - Forbidden action";
+
+# Test https connection with correct auth, with valid cacert
+$Auth{ssl_options} = ssl_options( $ENV{ES_CA_PATH} );
+
+$es = do "es_async.pl" or die( $@ || $! );
+
+ok wait_for( $es->cluster->health ),
+    "$ENV{ES_CXN} - Valid cert HTTPS with auth";
+
+# Test invalid user credentials
+%Auth = ( userinfo => 'foobar:baz' );
+$es = do "es_async.pl" or die( $@ || $! );
+throws_ok { wait_for( $es->cluster->health ) }
+"Search::Elasticsearch::Error::Unauthorized", "$ENV{ES_CXN} - Bad userinfo";
+
+# Test https connection with correct auth, with invalid cacert
+$Auth{ssl_options} = ssl_options('t/lib/bad_cacert.pem');
+
+$es = do "es_async.pl" or die( $@ || $! );
+$ENV{ES} = "https://www.google.com";
+
+throws_ok { wait_for( $es->cluster->health ) }
+"Search::Elasticsearch::Error::SSL",
+    "$ENV{ES_CXN} - Invalid cert throws SSL";
+
+done_testing;
+
+#===================================
+sub wait_for {
+#===================================
+    my $promise = shift;
+    my $cv      = AE::cv;
+    $promise->done( $cv, sub { $cv->croak(@_) } );
+    $cv->recv;
+}
+
diff --git a/t/lib/es_async_fork.pl b/t/lib/es_async_fork.pl
new file mode 100644
index 0000000..6c42b18
--- /dev/null
+++ b/t/lib/es_async_fork.pl
@@ -0,0 +1,62 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use Test::More;
+use POSIX ":sys_wait_h";
+use AE;
+use Promises qw(deferred);
+
+my $es        = do "es_async.pl" or die( $@ || $! );
+my $cxn_class = ref $es->transport->cxn_pool->cxns->[0];
+ok wait_for( $es->info ), "$cxn_class - Info before fork";
+
+my $Kids = 4;
+my %pids;
+
+for my $child ( 1 .. $Kids ) {
+    my $pid = fork();
+    if ($pid) {
+        $pids{$pid} = $child;
+        next;
+    }
+    if ( !defined $pid ) {
+        skip "fork() not supported";
+        done_testing;
+        last;
+    }
+
+    for ( 1 .. 100 ) {
+        wait_for( $es->info );
+    }
+    exit;
+}
+
+my $ok = 0;
+for ( 1 .. 10 ) {
+    my $pid = waitpid( -1, WNOHANG );
+    if ( $pid > 0 ) {
+        delete $pids{$pid};
+        $ok++ unless $?;
+        redo;
+    }
+    last unless keys %pids;
+    sleep 1;
+}
+
+is $ok, $Kids, "$cxn_class - Fork";
+done_testing;
+
diff --git a/t/lib/es_sync.pl b/t/lib/es_sync.pl
index f397867..bb7d3ed 100644
--- a/t/lib/es_sync.pl
+++ b/t/lib/es_sync.pl
@@ -1,3 +1,20 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 use Search::Elasticsearch;
 use Test::More;
 use strict;
@@ -50,8 +67,8 @@ if ( $ENV{ES} ) {
 unless ( $ENV{ES_SKIP_PING} ) {
     my $version = $es->info->{version}{number};
     my $api     = $es->api_version;
-    unless ( $api eq '0_90' && $version =~ /^0\.9/
-        || substr( $api, 0, 1 ) eq substr( $version, 0, 1 ) )
+    unless ( $version eq '8.0.0-SNAPSHOT' || ( $api eq '0_90' && $version =~ /^0\.9/
+        || substr( $api, 0, 1 ) eq substr( $version, 0, 1 ) ) )
     {
         plan skip_all =>
             "Tests are for API version $api but Elasticsearch is version $version\n";
diff --git a/t/lib/es_sync_auth.pl b/t/lib/es_sync_auth.pl
index 928e098..14469af 100644
--- a/t/lib/es_sync_auth.pl
+++ b/t/lib/es_sync_auth.pl
@@ -1,3 +1,20 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 #! perl
 use Test::More;
 use Test::Deep;
diff --git a/t/lib/es_sync_fork.pl b/t/lib/es_sync_fork.pl
index 9e1a32c..5339980 100644
--- a/t/lib/es_sync_fork.pl
+++ b/t/lib/es_sync_fork.pl
@@ -1,3 +1,20 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 use Test::More;
 use POSIX ":sys_wait_h";
 
diff --git a/t/lib/index_test_data.pl b/t/lib/index_test_data.pl
index daaf826..6de0baf 100644
--- a/t/lib/index_test_data.pl
+++ b/t/lib/index_test_data.pl
@@ -1,3 +1,20 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
 use strict;
 use warnings;
 use lib 't/lib';
diff --git a/t/lib/index_test_data_7.pl b/t/lib/index_test_data_7.pl
new file mode 100644
index 0000000..591ea9f
--- /dev/null
+++ b/t/lib/index_test_data_7.pl
@@ -0,0 +1,105 @@
+# Licensed to Elasticsearch B.V. under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Elasticsearch B.V. licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+use strict;
+use warnings;
+use lib 't/lib';
+
+local $ENV{ES_CXN};
+local $ENV{ES_CXN_POOL};
+my $es = do 'es_sync.pl' or die( $@ || $! );
+
+$es->indices->delete( index => 'test', ignore => 404 );
+$es->indices->create( index => 'test' );
+$es->cluster->health( wait_for_status => 'yellow' );
+
+my $b = $es->bulk_helper(
+    index => 'test'
+);
+my $i = 1;
+for ( names() ) {
+    $b->index(
+        {   id     => $i,
+            source => {
+                name   => $_,
+                count  => $i,
+                color  => ( $i % 2 ? 'red' : 'green' ),
+                switch => ( $i % 2 ? 1 : 2 )
+            }
+        }
+    );
+    $i++;
+}
+$b->flush;
+$es->indices->refresh;
+
+#===================================
+sub names {
+#===================================
+    return (
+        'Adaptoid',                     'Alpha Ray',
+        'Alysande Stuart',              'Americop',
+        'Andrew Chord',                 'Android Man',
+        'Ani-Mator',                    'Aqueduct',
+        'Archangel',                    'Arena',
+        'Auric',                        'Barton, Clint',
+        'Behemoth',                     'Bereet',
+        'Black Death',                  'Black King',
+        'Blaze',                        'Cancer',
+        'Charlie-27',                   'Christians, Isaac',
+        'Clea',                         'Contemplator',
+        'Copperhead',                   'Darkdevil',
+        'Deathbird',                    'Diablo',
+        'Doctor Arthur Nagan',          'Doctor Droom',
+        'Doctor Octopus',               'Epoch',
+        'Eternity',                     'Feline',
+        'Firestar',                     'Flex',
+        'Garokk the Petrified Man',     'Gill, Donald "Donny"',
+        'Glitch',                       'Golden Girl',
+        'Grandmaster',                  'Grey, Elaine',
+        'Halloween Jack',               'Hannibal King',
+        'Hero for Hire',                'Hrimhari',
+        'Ikonn',                        'Infinity',
+        'Jack-in-the-Box',              'Jim Hammond',
+        'Joe Cartelli',                 'Juarez, Bonita',
+        'Judd, Eugene',                 'Korrek',
+        'Krang',                        'Kukulcan',
+        'Lizard',                       'Machinesmith',
+        'Master Man',                   'Match',
+        'Maur-Konn',                    'Mekano',
+        'Miguel Espinosa',              'Mister Sinister',
+        'Mogul of the Mystic Mountain', 'Mutant Master',
+        'Night Thrasher',               'Nital, Taj',
+        'Obituary',                     'Ogre',
+        'Owl',                          'Ozone',
+        'Paris',                        'Phastos',
+        'Piper',                        'Prodigy',
+        'Quagmire',                     'Quasar',
+        'Radioactive Man',              'Rankin, Calvin',
+        'Scarlet Scarab',               'Scarlet Witch',
+        'Seth',                         'Slug',
+        'Sluggo',                       'Smallwood, Marrina',
+        'Smith, Tabitha',               'St. Croix, Claudette',
+        'Stacy X',                      'Stallior',
+        'Star-Dancer',                  'Stitch',
+        'Storm, Susan',                 'Summers, Gabriel',
+        'Thane Ector',                  'Toad-In-Waiting',
+        'Ultron',                       'Urich, Phil',
+        'Vibro',                        'Victorius',
+        'Wolfsbane',                    'Yandroth'
+    );
+}

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/doc/libsearch-elasticsearch-client-2-0-perl/changelog.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Bulk.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Direct.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Direct::Cat.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Direct::Cluster.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Direct::Indices.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Direct::Nodes.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Direct::Snapshot.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Direct::Tasks.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Role::API.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Role::Bulk.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Role::Scroll.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::Scroll.3pm.gz
-rw-r--r--  root/root   /usr/share/man/man3/Search::Elasticsearch::Client::2_0::TestServer.3pm.gz
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Bulk.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Direct.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Direct/Cat.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Direct/Cluster.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Direct/Indices.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Direct/Nodes.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Direct/Snapshot.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Direct/Tasks.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Role/API.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Role/Bulk.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Role/Scroll.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/Scroll.pm
-rw-r--r--  root/root   /usr/share/perl5/Search/Elasticsearch/Client/2_0/TestServer.pm

Control files: lines which differ (wdiff format)

  • Depends: perl:any, libdevel-globaldestruction-perl, libmoo-perl, libnamespace-clean-perl, libsearch-elasticsearch-perl (>= 6.00), libtry-tiny-perl

More details

Full run details